mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 06:26:27 +00:00
feat(dynamic-secret): Vertica option improvements
This commit is contained in:
@@ -42,5 +42,5 @@ export const buildDynamicSecretProviders = ({
|
|||||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||||
[DynamicSecretProviders.Vertica]: VerticaProvider()
|
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService })
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ export const DynamicSecretVerticaSchema = z.object({
|
|||||||
username: z.string().trim(),
|
username: z.string().trim(),
|
||||||
password: z.string().trim(),
|
password: z.string().trim(),
|
||||||
database: z.string().trim(),
|
database: z.string().trim(),
|
||||||
|
gatewayId: z.string().nullable().optional(),
|
||||||
creationStatement: z.string().trim(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string().trim(),
|
revocationStatement: z.string().trim(),
|
||||||
passwordRequirements: z
|
passwordRequirements: z
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import knex, { Knex } from "knex";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretVerticaSchema, PasswordRequirements, TDynamicProviderFns } from "./models";
|
import { DynamicSecretVerticaSchema, PasswordRequirements, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
interface VersionResult {
|
interface VersionResult {
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
@@ -125,11 +129,15 @@ const generateUsername = (usernameTemplate?: string | null) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VerticaProvider = (): TDynamicProviderFns => {
|
type TVerticaProviderDTO = {
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretVerticaSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretVerticaSchema.parseAsync(inputs);
|
||||||
|
|
||||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
|
||||||
validateHandlebarTemplate("Vertica creation", providerInputs.creationStatement, {
|
validateHandlebarTemplate("Vertica creation", providerInputs.creationStatement, {
|
||||||
allowedExpressions: (val) => ["username", "password"].includes(val)
|
allowedExpressions: (val) => ["username", "password"].includes(val)
|
||||||
});
|
});
|
||||||
@@ -152,6 +160,7 @@ export const VerticaProvider = (): TDynamicProviderFns => {
|
|||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
ssl: false
|
ssl: false
|
||||||
},
|
},
|
||||||
|
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
pool: {
|
pool: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
@@ -176,27 +185,65 @@ export const VerticaProvider = (): TDynamicProviderFns => {
|
|||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gatewayProxyWrapper = async (
|
||||||
|
providerInputs: z.infer<typeof DynamicSecretVerticaSchema>,
|
||||||
|
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||||
|
) => {
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
await withGatewayProxy(
|
||||||
|
async (port) => {
|
||||||
|
await gatewayCallback("localhost", port);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetHost: providerInputs.host,
|
||||||
|
targetPort: providerInputs.port,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
let client: VerticaKnexClient | null = null;
|
let isConnected = false;
|
||||||
|
|
||||||
try {
|
const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => {
|
||||||
client = await $getClient(providerInputs);
|
let client: VerticaKnexClient | null = null;
|
||||||
|
|
||||||
const clientResult: DatabaseQueryResult = await client.raw("SELECT version() AS version");
|
try {
|
||||||
|
client = await $getClient({ ...providerInputs, hostIp: host, port });
|
||||||
|
|
||||||
const resultFromSelectedDatabase = clientResult.rows?.[0] as VersionResult | undefined;
|
const clientResult: DatabaseQueryResult = await client.raw("SELECT version() AS version");
|
||||||
|
|
||||||
if (!resultFromSelectedDatabase?.version) {
|
const resultFromSelectedDatabase = clientResult.rows?.[0] as VersionResult | undefined;
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to validate Vertica connection, version query failed"
|
if (!resultFromSelectedDatabase?.version) {
|
||||||
});
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate Vertica connection, version query failed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected = true;
|
||||||
|
} finally {
|
||||||
|
if (client) await client.destroy();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return true;
|
if (providerInputs.gatewayId) {
|
||||||
} finally {
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
if (client) await client.destroy();
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isConnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||||
@@ -206,87 +253,103 @@ export const VerticaProvider = (): TDynamicProviderFns => {
|
|||||||
const username = generateUsername(usernameTemplate);
|
const username = generateUsername(usernameTemplate);
|
||||||
const password = generatePassword(providerInputs.passwordRequirements);
|
const password = generatePassword(providerInputs.passwordRequirements);
|
||||||
|
|
||||||
let client: VerticaKnexClient | null = null;
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
|
let client: VerticaKnexClient | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client = await $getClient(providerInputs);
|
client = await $getClient({ ...providerInputs, hostIp: host, port });
|
||||||
|
|
||||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
});
|
});
|
||||||
|
|
||||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||||
|
|
||||||
// Execute queries sequentially to maintain transaction integrity
|
// Execute queries sequentially to maintain transaction integrity
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
const trimmedQuery = query.trim();
|
const trimmedQuery = query.trim();
|
||||||
if (trimmedQuery) {
|
if (trimmedQuery) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await client.raw(trimmedQuery);
|
await client.raw(trimmedQuery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (client) await client.destroy();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
if (providerInputs.gatewayId) {
|
||||||
} finally {
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
if (client) await client.destroy();
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (inputs: unknown, username: string) => {
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
let client: VerticaKnexClient | null = null;
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
|
let client: VerticaKnexClient | null = null;
|
||||||
|
|
||||||
try {
|
|
||||||
client = await $getClient(providerInputs);
|
|
||||||
|
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({
|
|
||||||
username
|
|
||||||
});
|
|
||||||
|
|
||||||
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
|
||||||
|
|
||||||
// Check for active sessions and close them
|
|
||||||
try {
|
try {
|
||||||
const sessionResult: DatabaseQueryResult = await client.raw(
|
client = await $getClient({ ...providerInputs, hostIp: host, port });
|
||||||
"SELECT session_id FROM sessions WHERE user_name = ?",
|
|
||||||
[username]
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeSessions = (sessionResult.rows || []) as SessionResult[];
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
// Close all sessions in parallel since they're independent operations
|
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||||
if (activeSessions.length > 0) {
|
|
||||||
const sessionClosePromises = activeSessions.map(async (session) => {
|
|
||||||
try {
|
|
||||||
await client!.raw("SELECT close_session(?)", [session.session_id]);
|
|
||||||
} catch (error) {
|
|
||||||
// Continue if session is already closed
|
|
||||||
logger.error(error, `Failed to close session ${session.session_id}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.allSettled(sessionClosePromises);
|
// Check for active sessions and close them
|
||||||
|
try {
|
||||||
|
const sessionResult: DatabaseQueryResult = await client.raw(
|
||||||
|
"SELECT session_id FROM sessions WHERE user_name = ?",
|
||||||
|
[username]
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeSessions = (sessionResult.rows || []) as SessionResult[];
|
||||||
|
|
||||||
|
// Close all sessions in parallel since they're independent operations
|
||||||
|
if (activeSessions.length > 0) {
|
||||||
|
const sessionClosePromises = activeSessions.map(async (session) => {
|
||||||
|
try {
|
||||||
|
await client!.raw("SELECT close_session(?)", [session.session_id]);
|
||||||
|
} catch (error) {
|
||||||
|
// Continue if session is already closed
|
||||||
|
logger.error(error, `Failed to close session ${session.session_id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(sessionClosePromises);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue if we can't query sessions (permissions, etc.)
|
||||||
|
logger.error(error, "Could not query/close active sessions");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
// Continue if we can't query sessions (permissions, etc.)
|
|
||||||
logger.error(error, "Could not query/close active sessions");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute revocation queries sequentially to maintain transaction integrity
|
// Execute revocation queries sequentially to maintain transaction integrity
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
const trimmedQuery = query.trim();
|
const trimmedQuery = query.trim();
|
||||||
if (trimmedQuery) {
|
if (trimmedQuery) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await client.raw(trimmedQuery);
|
await client.raw(trimmedQuery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (client) await client.destroy();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return { entityId: username };
|
if (providerInputs.gatewayId) {
|
||||||
} finally {
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
if (client) await client.destroy();
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { entityId: username };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (_: unknown, username: string) => {
|
const renew = async (_: unknown, username: string) => {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ Create a user with the required permission in your Vertica instance. This user w
|
|||||||
Maximum time-to-live for a generated secret
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Gateway" type="string">
|
||||||
|
Select a gateway for private cluster access. If not specified, the Internet Gateway will be used.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Host" type="string" required>
|
<ParamField path="Host" type="string" required>
|
||||||
Vertica database host
|
Vertica database host
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -106,7 +110,7 @@ Create a user with the required permission in your Vertica instance. This user w
|
|||||||

|

|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
|
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret.
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||||
@@ -117,7 +121,7 @@ Create a user with the required permission in your Vertica instance. This user w
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## Audit or Revoke Leases
|
||||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||||
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
|
This will allow you to see the expiration time of the lease or delete the lease before its set time to live.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 187 KiB |
+68
-6
@@ -1,10 +1,12 @@
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -14,9 +16,16 @@ import {
|
|||||||
FilterableSelect,
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
TextArea
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||||
|
|
||||||
@@ -50,7 +59,8 @@ const formSchema = z.object({
|
|||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
passwordRequirements: passwordRequirementsSchema.optional(),
|
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||||
creationStatement: z.string().min(1),
|
creationStatement: z.string().min(1),
|
||||||
revocationStatement: z.string().min(1)
|
revocationStatement: z.string().min(1),
|
||||||
|
gatewayId: z.string().optional()
|
||||||
}),
|
}),
|
||||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
@@ -124,6 +134,7 @@ GRANT CREATE ON SCHEMA public TO {{username}};`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createDynamicSecret = useCreateDynamicSecret();
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const handleCreateDynamicSecret = async ({
|
const handleCreateDynamicSecret = async ({
|
||||||
name,
|
name,
|
||||||
@@ -184,7 +195,7 @@ GRANT CREATE ON SCHEMA public TO {{username}};`,
|
|||||||
defaultValue="1h"
|
defaultValue="1h"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label={<TtlFormLabel label="Max TTL" />}
|
label={<TtlFormLabel label="Default TTL" />}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
@@ -200,7 +211,7 @@ GRANT CREATE ON SCHEMA public TO {{username}};`,
|
|||||||
defaultValue="24h"
|
defaultValue="24h"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label={<TtlFormLabel label="Default TTL" />}
|
label={<TtlFormLabel label="Max TTL" />}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
@@ -214,6 +225,57 @@ GRANT CREATE ON SCHEMA public TO {{username}};`,
|
|||||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
Configuration
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -227,7 +289,7 @@ GRANT CREATE ON SCHEMA public TO {{username}};`,
|
|||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input placeholder="92.41.22.72" {...field} />
|
<Input placeholder="Vertica Host" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+75
-4
@@ -1,10 +1,12 @@
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -13,9 +15,14 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
TextArea
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
import { OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
const passwordRequirementsSchema = z
|
const passwordRequirementsSchema = z
|
||||||
@@ -49,7 +56,8 @@ const formSchema = z.object({
|
|||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
passwordRequirements: passwordRequirementsSchema.optional(),
|
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||||
creationStatement: z.string().min(1),
|
creationStatement: z.string().min(1),
|
||||||
revocationStatement: z.string().min(1)
|
revocationStatement: z.string().min(1),
|
||||||
|
gatewayId: z.string().optional().nullable()
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
@@ -122,8 +130,12 @@ export const EditDynamicSecretVerticaForm = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||||
|
|
||||||
|
const selectedGatewayId = watch("inputs.gatewayId");
|
||||||
|
const isGatewayInActive = gateways?.findIndex((el) => el.id === selectedGatewayId) === -1;
|
||||||
|
|
||||||
const handleUpdateDynamicSecret = async ({
|
const handleUpdateDynamicSecret = async ({
|
||||||
inputs,
|
inputs,
|
||||||
maxTTL,
|
maxTTL,
|
||||||
@@ -143,7 +155,10 @@ export const EditDynamicSecretVerticaForm = ({
|
|||||||
data: {
|
data: {
|
||||||
maxTTL: maxTTL || undefined,
|
maxTTL: maxTTL || undefined,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
inputs,
|
inputs: {
|
||||||
|
...inputs,
|
||||||
|
gatewayId: isGatewayInActive ? null : inputs.gatewayId
|
||||||
|
},
|
||||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||||
}
|
}
|
||||||
@@ -219,6 +234,62 @@ export const EditDynamicSecretVerticaForm = ({
|
|||||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
Configuration
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message) || isGatewayInActive}
|
||||||
|
errorText={
|
||||||
|
isGatewayInActive && selectedGatewayId
|
||||||
|
? `Project Gateway ${selectedGatewayId} is removed`
|
||||||
|
: error?.message
|
||||||
|
}
|
||||||
|
label="Gateway"
|
||||||
|
helperText=""
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value || undefined}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
Reference in New Issue
Block a user