mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 04:58:28 +00:00
Merge branch 'main' into feat/point-in-time-revamp
This commit is contained in:
+30
-4
@@ -53,7 +53,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -93,7 +94,8 @@ export const AwsElastiCacheInputForm = ({
|
||||
"UserId": "{{username}}"
|
||||
}`
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -104,10 +106,13 @@ export const AwsElastiCacheInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider },
|
||||
@@ -116,7 +121,9 @@ export const AwsElastiCacheInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -270,6 +277,25 @@ export const AwsElastiCacheInputForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify ElastiCache Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
|
||||
+29
-4
@@ -42,7 +42,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -70,7 +71,8 @@ export const AwsIamInputForm = ({
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -81,12 +83,14 @@ export const AwsIamInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.AwsIam, inputs: provider },
|
||||
maxTTL,
|
||||
@@ -94,7 +98,9 @@ export const AwsIamInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -300,6 +306,25 @@ export const AwsIamInputForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
+31
-5
@@ -20,6 +20,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -54,8 +55,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -93,7 +95,8 @@ export const CassandraInputForm = ({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: getSqlStatements(),
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -104,11 +107,13 @@ export const CassandraInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Cassandra, inputs: provider },
|
||||
@@ -117,7 +122,9 @@ export const CassandraInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -298,6 +305,25 @@ export const CassandraInputForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify CQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
|
||||
+51
@@ -4,6 +4,7 @@ import {
|
||||
SiApachecassandra,
|
||||
SiElasticsearch,
|
||||
SiFiles,
|
||||
SiKubernetes,
|
||||
SiMongodb,
|
||||
SiRabbitmq,
|
||||
SiSap,
|
||||
@@ -24,6 +25,7 @@ import { AwsIamInputForm } from "./AwsIamInputForm";
|
||||
import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
|
||||
import { CassandraInputForm } from "./CassandraInputForm";
|
||||
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
|
||||
import { KubernetesInputForm } from "./KubernetesInputForm";
|
||||
import { LdapInputForm } from "./LdapInputForm";
|
||||
import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
||||
import { MongoDBDatabaseInputForm } from "./MongoDBInputForm";
|
||||
@@ -34,6 +36,7 @@ import { SapHanaInputForm } from "./SapHanaInputForm";
|
||||
import { SnowflakeInputForm } from "./SnowflakeInputForm";
|
||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||
import { TotpInputForm } from "./TotpInputForm";
|
||||
import { VerticaInputForm } from "./VerticaInputForm";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -124,6 +127,16 @@ const DYNAMIC_SECRET_LIST = [
|
||||
icon: <FontAwesomeIcon icon={faClock} size="lg" />,
|
||||
provider: DynamicSecretProviders.Totp,
|
||||
title: "TOTP"
|
||||
},
|
||||
{
|
||||
icon: <FontAwesomeIcon icon={faDatabase} size="lg" />,
|
||||
provider: DynamicSecretProviders.Vertica,
|
||||
title: "Vertica"
|
||||
},
|
||||
{
|
||||
icon: <SiKubernetes size="1.5rem" />,
|
||||
provider: DynamicSecretProviders.Kubernetes,
|
||||
title: "Kubernetes"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -472,6 +485,44 @@ export const CreateDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.Kubernetes && (
|
||||
<motion.div
|
||||
key="dynamic-kubernetes-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<KubernetesInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.Vertica && (
|
||||
<motion.div
|
||||
key="dynamic-vertica-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<VerticaInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+31
-5
@@ -21,6 +21,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const authMethods = [
|
||||
{
|
||||
@@ -75,8 +76,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -113,7 +115,8 @@ export const ElasticSearchInputForm = ({
|
||||
roles: ["superuser"],
|
||||
port: 443
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,10 +127,12 @@ export const ElasticSearchInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider },
|
||||
@@ -136,7 +141,9 @@ export const ElasticSearchInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -418,6 +425,25 @@ export const ElasticSearchInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
+493
@@ -0,0 +1,493 @@
|
||||
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faQuestionCircle,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionSubjects } from "@app/context/OrgPermissionContext";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
enum CredentialType {
|
||||
Dynamic = "dynamic",
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
const credentialTypes = [
|
||||
{
|
||||
label: "Static",
|
||||
value: CredentialType.Static
|
||||
}
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().default(false),
|
||||
credentialType: z.literal(CredentialType.Static),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
namespace: z.string().trim().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema> & FieldValues;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const KubernetesInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
secretPath,
|
||||
projectSlug,
|
||||
environments,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
watch
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
url: "",
|
||||
clusterToken: "",
|
||||
ca: "",
|
||||
sslEnabled: false,
|
||||
serviceAccountName: "",
|
||||
namespace: "",
|
||||
credentialType: CredentialType.Static,
|
||||
gatewayId: undefined,
|
||||
audiences: []
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "provider.audiences"
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const sslEnabled = watch("provider.sslEnabled");
|
||||
|
||||
const handleCreateDynamicSecret = async (formData: TForm) => {
|
||||
const { provider, ...rest } = formData;
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Kubernetes, inputs: provider },
|
||||
maxTTL: rest.maxTTL,
|
||||
name: rest.name,
|
||||
path: secretPath,
|
||||
defaultTTL: rest.defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: rest.environment.slug
|
||||
});
|
||||
|
||||
onCompleted();
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<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>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Cluster URL"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mb-2 flex items-center">
|
||||
<span className="mr-3 flex items-center text-sm text-mineshaft-400">
|
||||
Enable SSL
|
||||
<Tooltip
|
||||
className="ml-1 max-w-md"
|
||||
content={
|
||||
<span>
|
||||
If enabled, you can optionally provide a custom CA certificate. Leave
|
||||
blank to use the system/public CA.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<Controller
|
||||
name="provider.sslEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
aria-label="Enable SSL"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
placeholder="-----BEGIN CERTIFICATE----- ..."
|
||||
isDisabled={!sslEnabled}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.clusterToken"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Cluster Token"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.credentialType"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Credential Type"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
>
|
||||
{credentialTypes.map((credentialType) => (
|
||||
<SelectItem
|
||||
value={credentialType.value}
|
||||
key={`credential-type-${credentialType.value}`}
|
||||
>
|
||||
{credentialType.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.serviceAccountName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Name"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.namespace"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 w-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.audiences"
|
||||
render={({ fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audiences"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center space-x-2">
|
||||
<Input
|
||||
{...control.register(`provider.audiences.${index}`)}
|
||||
placeholder="Enter audience"
|
||||
className="flex-grow"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => remove(index)}
|
||||
variant="outline_bg"
|
||||
ariaLabel="Remove audience"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline_bg" onClick={() => append("")} type="button">
|
||||
Add Audience
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+31
-4
@@ -19,6 +19,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
enum CredentialType {
|
||||
Dynamic = "dynamic",
|
||||
@@ -78,8 +79,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
@@ -120,7 +122,8 @@ export const LdapInputForm = ({
|
||||
rollbackLdif: "",
|
||||
credentialType: CredentialType.Dynamic
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,10 +136,13 @@ export const LdapInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Ldap, inputs: provider },
|
||||
@@ -145,6 +151,8 @@ export const LdapInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate,
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
@@ -413,6 +421,25 @@ export const LdapInputForm = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+33
-6
@@ -24,6 +24,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -65,8 +66,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -114,7 +116,8 @@ export const MongoAtlasInputForm = ({
|
||||
provider: {
|
||||
roles: [{ databaseName: "", roleName: "" }]
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -135,10 +138,13 @@ export const MongoAtlasInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider },
|
||||
@@ -147,7 +153,9 @@ export const MongoAtlasInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -312,7 +320,7 @@ export const MongoAtlasInputForm = ({
|
||||
label="Role"
|
||||
className="text-xs text-mineshaft-400"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
Built-in: atlasAdmin, backup, clusterMonitor, dbAdmin, dbAdminAnyDatabase, enableSharding, read, readAnyDatabase, readWrite, readWriteAnyDatabase.`}
|
||||
/>
|
||||
)}
|
||||
@@ -362,6 +370,25 @@ export const MongoAtlasInputForm = ({
|
||||
<AccordionItem value="advance-section">
|
||||
<AccordionTrigger>Advanced</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel
|
||||
label="Scopes"
|
||||
isOptional
|
||||
|
||||
+33
-6
@@ -19,6 +19,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -55,8 +56,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -89,7 +91,8 @@ export const MongoDBDatabaseInputForm = ({
|
||||
provider: {
|
||||
roles: [{ roleName: "readWrite" }]
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,10 +108,13 @@ export const MongoDBDatabaseInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: {
|
||||
@@ -124,7 +130,9 @@ export const MongoDBDatabaseInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -272,7 +280,7 @@ export const MongoDBDatabaseInputForm = ({
|
||||
<FormLabel
|
||||
label="Roles"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
Built-in: atlasAdmin, backup, clusterMonitor, dbAdmin, dbAdminAnyDatabase, enableSharding, read, readAnyDatabase, readWrite, readWriteAnyDatabase.`}
|
||||
/>
|
||||
<div className="mb-3 mt-1 flex flex-col space-y-2">
|
||||
@@ -340,6 +348,25 @@ export const MongoDBDatabaseInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
+32
-5
@@ -19,6 +19,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -59,8 +60,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -102,7 +104,8 @@ export const RabbitMqInputForm = ({
|
||||
},
|
||||
tags: []
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,10 +116,13 @@ export const RabbitMqInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider },
|
||||
@@ -125,7 +131,9 @@ export const RabbitMqInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -424,6 +432,25 @@ export const RabbitMqInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
+30
-4
@@ -20,6 +20,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -52,8 +53,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -87,7 +89,8 @@ export const RedisInputForm = ({
|
||||
creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all",
|
||||
revocationStatement: "ACL DELUSER {{username}}"
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,11 +101,13 @@ export const RedisInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
usernameTemplate,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Redis, inputs: provider },
|
||||
maxTTL,
|
||||
@@ -110,7 +115,9 @@ export const RedisInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -265,6 +272,25 @@ export const RedisInputForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify Redis Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
|
||||
+31
-5
@@ -19,6 +19,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -50,8 +51,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -88,7 +90,8 @@ sp_role 'grant', 'mon_role', '{{username}}';`,
|
||||
revocationStatement: `sp_dropuser '{{username}}';
|
||||
sp_droplogin '{{username}}';`
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -99,11 +102,13 @@ sp_droplogin '{{username}}';`
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.SapAse, inputs: provider },
|
||||
maxTTL,
|
||||
@@ -111,7 +116,9 @@ sp_droplogin '{{username}}';`
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -262,6 +269,25 @@ sp_droplogin '{{username}}';`
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
|
||||
+29
-3
@@ -20,6 +20,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -52,8 +53,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -88,7 +90,8 @@ GRANT "MONITORING" TO {{username}};`,
|
||||
DROP USER {{username}};`,
|
||||
renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';"
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,6 +99,7 @@ DROP USER {{username}};`,
|
||||
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
usernameTemplate,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
@@ -104,6 +108,7 @@ DROP USER {{username}};`,
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.SapHana, inputs: provider },
|
||||
maxTTL,
|
||||
@@ -111,6 +116,8 @@ DROP USER {{username}};`,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate,
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
@@ -263,6 +270,25 @@ DROP USER {{username}};`,
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
|
||||
+31
-9
@@ -21,6 +21,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@@ -52,12 +53,9 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -91,7 +89,8 @@ export const SnowflakeInputForm = ({
|
||||
revocationStatement: "DROP USER {{username}};",
|
||||
renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};"
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,11 +101,13 @@ export const SnowflakeInputForm = ({
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Snowflake, inputs: provider },
|
||||
maxTTL,
|
||||
@@ -114,7 +115,9 @@ export const SnowflakeInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
@@ -266,6 +269,25 @@ export const SnowflakeInputForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
|
||||
+31
-5
@@ -29,6 +29,7 @@ import {
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { MetadataForm } from "../../DynamicSecretListView/MetadataForm";
|
||||
|
||||
@@ -88,7 +89,7 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
metadata: z
|
||||
.object({
|
||||
@@ -96,7 +97,8 @@ const formSchema = z.object({
|
||||
value: z.string().trim().default("")
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
.optional(),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
@@ -191,7 +193,8 @@ export const SqlDatabaseInputForm = ({
|
||||
allowedSymbols: "-_.~!*"
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,11 +207,13 @@ export const SqlDatabaseInputForm = ({
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment,
|
||||
metadata
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider },
|
||||
@@ -218,7 +223,9 @@ export const SqlDatabaseInputForm = ({
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug,
|
||||
metadata
|
||||
metadata,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@@ -474,6 +481,25 @@ export const SqlDatabaseInputForm = ({
|
||||
Creation, Revocation & Renew Statements (optional)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Customize SQL statements for managing database user lifecycle
|
||||
</div>
|
||||
|
||||
+2
-5
@@ -16,6 +16,7 @@ import {
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
enum ConfigType {
|
||||
URL = "url",
|
||||
@@ -56,11 +57,7 @@ const formSchema = z.object({
|
||||
digits: z.number().optional()
|
||||
})
|
||||
]),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
name: slugSchema(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
|
||||
+610
@@ -0,0 +1,610 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const passwordRequirementsSchema = z
|
||||
.object({
|
||||
length: z.number().min(8).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250;
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length");
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
database: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
gatewayId: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const VerticaInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
database: "",
|
||||
port: 5433,
|
||||
passwordRequirements: {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
},
|
||||
creationStatement: `CREATE USER {{username}} IDENTIFIED BY '{{password}}';
|
||||
GRANT CREATE ON SCHEMA public TO {{username}};`,
|
||||
revocationStatement: "DROP USER {{username}} CASCADE;"
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Vertica, inputs: provider },
|
||||
maxTTL,
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</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 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input placeholder="Vertica Host" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.port"
|
||||
defaultValue={5433}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.database"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Database"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Accordion type="multiple" className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="password-config">
|
||||
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Set constraints on the generated database password
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.length"
|
||||
defaultValue={48}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password Length"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={250}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
|
||||
<div className="text-sm text-gray-500">
|
||||
{(() => {
|
||||
const total = Object.values(
|
||||
watch("provider.passwordRequirements.required") || {}
|
||||
).reduce((sum, count) => sum + Number(count || 0), 0);
|
||||
const length = watch("provider.passwordRequirements.length") || 0;
|
||||
const isError = total > length;
|
||||
return (
|
||||
<span className={isError ? "text-red-500" : ""}>
|
||||
Total required characters: {total}{" "}
|
||||
{isError ? `(exceeds length of ${length})` : ""}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.lowercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Lowercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of lowercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.uppercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Uppercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of uppercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.digits"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digit Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of digits"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.symbols"
|
||||
defaultValue={0}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbol Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of symbols"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Allowed Symbols</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.allowedSymbols"
|
||||
defaultValue="-_.~!*"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbols to use in password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Default: -_.~!*"
|
||||
>
|
||||
<Input {...field} placeholder="-_.~!*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+15
@@ -137,6 +137,7 @@ const renderOutputForm = (
|
||||
provider === DynamicSecretProviders.Cassandra ||
|
||||
provider === DynamicSecretProviders.MongoAtlas ||
|
||||
provider === DynamicSecretProviders.MongoDB ||
|
||||
provider === DynamicSecretProviders.Vertica ||
|
||||
provider === DynamicSecretProviders.SapAse
|
||||
) {
|
||||
const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string };
|
||||
@@ -320,6 +321,20 @@ const renderOutputForm = (
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === DynamicSecretProviders.Kubernetes) {
|
||||
const { TOKEN } = data as { TOKEN: string };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<OutputDisplay
|
||||
label="Service Account JWT"
|
||||
value={TOKEN}
|
||||
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === DynamicSecretProviders.Totp) {
|
||||
const { TOTP, TIME_REMAINING } = data as {
|
||||
TOTP: string;
|
||||
|
||||
+32
-6
@@ -18,6 +18,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -53,10 +54,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -85,6 +84,7 @@ export const EditDynamicSecretAwsElastiCacheProviderForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -93,9 +93,16 @@ export const EditDynamicSecretAwsElastiCacheProviderForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -106,7 +113,8 @@ export const EditDynamicSecretAwsElastiCacheProviderForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -261,6 +269,24 @@ export const EditDynamicSecretAwsElastiCacheProviderForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify ElastiCache Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
|
||||
+32
-6
@@ -8,6 +8,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -43,10 +44,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -75,6 +74,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -83,9 +83,16 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -96,7 +103,8 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -297,6 +305,24 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
|
||||
+2
-4
@@ -8,6 +8,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, SecretInput } from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z.object({
|
||||
@@ -37,10 +38,7 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
|
||||
+33
-7
@@ -18,6 +18,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -55,10 +56,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -89,15 +88,23 @@ export const EditDynamicSecretCassandraForm = ({
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
},
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}"
|
||||
}
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -108,7 +115,8 @@ export const EditDynamicSecretCassandraForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -292,6 +300,24 @@ export const EditDynamicSecretCassandraForm = ({
|
||||
<AccordionItem value="modify-sql-statement">
|
||||
<AccordionTrigger>Modify CQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
|
||||
+32
-7
@@ -19,6 +19,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const authMethods = [
|
||||
{
|
||||
@@ -73,10 +74,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -107,6 +106,7 @@ export const EditDynamicSecretElasticSearchForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -115,9 +115,16 @@ export const EditDynamicSecretElasticSearchForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -128,7 +135,8 @@ export const EditDynamicSecretElasticSearchForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -390,7 +398,6 @@ export const EditDynamicSecretElasticSearchForm = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -410,6 +417,24 @@ export const EditDynamicSecretElasticSearchForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+36
@@ -9,6 +9,7 @@ import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm";
|
||||
import { EditDynamicSecretAzureEntraIdForm } from "./EditDynamicSecretAzureEntraIdForm";
|
||||
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
|
||||
import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm";
|
||||
import { EditDynamicSecretKubernetesForm } from "./EditDynamicSecretKubernetesForm";
|
||||
import { EditDynamicSecretLdapForm } from "./EditDynamicSecretLdapForm";
|
||||
import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm";
|
||||
import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm";
|
||||
@@ -19,6 +20,7 @@ import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
||||
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||
import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm";
|
||||
import { EditDynamicSecretVerticaForm } from "./EditDynamicSecretVertica";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -312,6 +314,40 @@ export const EditDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.Kubernetes && (
|
||||
<motion.div
|
||||
key="kubernetes-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretKubernetesForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.Vertica && (
|
||||
<motion.div
|
||||
key="vertica-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretVerticaForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faQuestionCircle,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionSubjects } from "@app/context/OrgPermissionContext";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
enum CredentialType {
|
||||
Dynamic = "dynamic",
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
const credentialTypes = [
|
||||
{
|
||||
label: "Static",
|
||||
value: CredentialType.Static
|
||||
}
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().default(false),
|
||||
credentialType: z.literal(CredentialType.Static),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
namespace: z.string().trim().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: slugSchema().optional()
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema> & FieldValues;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretKubernetesForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
watch
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
newName: dynamicSecret.name,
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
inputs: dynamicSecret.inputs as TForm["inputs"]
|
||||
}
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "inputs.audiences" as const
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const sslEnabled = watch("inputs.sslEnabled");
|
||||
|
||||
const handleUpdateDynamicSecret = async (formData: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
inputs: formData.inputs,
|
||||
newName: formData.newName === dynamicSecret.name ? undefined : formData.newName,
|
||||
defaultTTL: formData.defaultTTL,
|
||||
maxTTL: formData.maxTTL
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: err instanceof Error ? err.message : "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<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)}
|
||||
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>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Cluster URL"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mb-2 flex items-center">
|
||||
<span className="mr-3 flex items-center text-sm text-mineshaft-400">
|
||||
Enable SSL
|
||||
<Tooltip
|
||||
className="ml-1 max-w-md"
|
||||
content={
|
||||
<span>
|
||||
If enabled, you can optionally provide a custom CA certificate. Leave
|
||||
blank to use the system/public CA.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<Controller
|
||||
name="inputs.sslEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
aria-label="Enable SSL"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
placeholder="-----BEGIN CERTIFICATE----- ..."
|
||||
isDisabled={!sslEnabled}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.clusterToken"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Cluster Token"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.credentialType"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Credential Type"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
>
|
||||
{credentialTypes.map((credentialType) => (
|
||||
<SelectItem
|
||||
value={credentialType.value}
|
||||
key={`credential-type-${credentialType.value}`}
|
||||
>
|
||||
{credentialType.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.serviceAccountName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Name"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.namespace"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 w-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.audiences"
|
||||
render={({ fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audiences"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center space-x-2">
|
||||
<Input
|
||||
{...control.register(`inputs.audiences.${index}`)}
|
||||
placeholder="Enter audience"
|
||||
className="flex-grow"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => remove(index)}
|
||||
variant="outline_bg"
|
||||
ariaLabel="Remove audience"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline_bg" onClick={() => append("")} type="button">
|
||||
Add Audience
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+33
-6
@@ -8,6 +8,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
enum CredentialType {
|
||||
Dynamic = "dynamic",
|
||||
@@ -67,10 +68,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -101,6 +100,7 @@ export const EditDynamicSecretLdapForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -110,9 +110,17 @@ export const EditDynamicSecretLdapForm = ({
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
const selectedCredentialType = watch("inputs.credentialType");
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -123,7 +131,8 @@ export const EditDynamicSecretLdapForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -329,6 +338,24 @@ export const EditDynamicSecretLdapForm = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
|
||||
+34
-7
@@ -22,6 +22,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -66,10 +67,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -115,6 +114,7 @@ export const EditDynamicSecretMongoAtlasForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -133,9 +133,17 @@ export const EditDynamicSecretMongoAtlasForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -146,7 +154,8 @@ export const EditDynamicSecretMongoAtlasForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -312,7 +321,7 @@ export const EditDynamicSecretMongoAtlasForm = ({
|
||||
label="Role"
|
||||
className="text-xs text-mineshaft-400"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
Built-in: atlasAdmin, backup, clusterMonitor, dbAdmin, dbAdminAnyDatabase, enableSharding, read, readAnyDatabase, readWrite, readWriteAnyDatabase.`}
|
||||
/>
|
||||
)}
|
||||
@@ -362,6 +371,24 @@ export const EditDynamicSecretMongoAtlasForm = ({
|
||||
<AccordionItem value="advance-section">
|
||||
<AccordionTrigger>Advanced</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel
|
||||
label="Scopes"
|
||||
isOptional
|
||||
|
||||
+36
-6
@@ -10,6 +10,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -49,10 +50,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -83,6 +82,7 @@ export const EditDynamicSecretMongoDBForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"]),
|
||||
roles: (dynamicSecret.inputs as { roles: string[] }).roles?.map((roleName) => ({
|
||||
@@ -99,9 +99,17 @@ export const EditDynamicSecretMongoDBForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -116,6 +124,8 @@ export const EditDynamicSecretMongoDBForm = ({
|
||||
port: inputs?.port ? inputs.port : undefined,
|
||||
roles: inputs?.roles?.map((el) => el.roleName)
|
||||
},
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
@@ -265,7 +275,7 @@ export const EditDynamicSecretMongoDBForm = ({
|
||||
<FormLabel
|
||||
label="Roles"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
Built-in: atlasAdmin, backup, clusterMonitor, dbAdmin, dbAdminAnyDatabase, enableSharding, read, readAnyDatabase, readWrite, readWriteAnyDatabase.`}
|
||||
/>
|
||||
<div className="mb-3 mt-1 flex flex-col space-y-2">
|
||||
@@ -333,6 +343,26 @@ export const EditDynamicSecretMongoDBForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
|
||||
+35
-3
@@ -10,6 +10,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z.object({
|
||||
@@ -50,7 +51,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -80,6 +82,7 @@ export const EditDynamicSecretRabbitMqForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -88,9 +91,17 @@ export const EditDynamicSecretRabbitMqForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -101,7 +112,8 @@ export const EditDynamicSecretRabbitMqForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -405,6 +417,26 @@ export const EditDynamicSecretRabbitMqForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-5
@@ -18,6 +18,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -54,10 +55,8 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -86,6 +85,7 @@ export const EditDynamicSecretRedisProviderForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -94,9 +94,16 @@ export const EditDynamicSecretRedisProviderForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -104,6 +111,8 @@ export const EditDynamicSecretRedisProviderForm = ({
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate,
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
@@ -262,6 +271,24 @@ export const EditDynamicSecretRedisProviderForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify Redis Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
|
||||
+33
-3
@@ -18,6 +18,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -40,6 +41,7 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
usernameTemplate: z.string().trim().nullable().optional(),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -52,7 +54,7 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
newName: slugSchema().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -81,6 +83,7 @@ export const EditDynamicSecretSapAseForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -89,9 +92,17 @@ export const EditDynamicSecretSapAseForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -102,7 +113,8 @@ export const EditDynamicSecretSapAseForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -272,6 +284,24 @@ export const EditDynamicSecretSapAseForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
|
||||
+33
-3
@@ -18,6 +18,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -52,7 +53,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -81,6 +83,7 @@ export const EditDynamicSecretSapHanaForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -89,9 +92,17 @@ export const EditDynamicSecretSapHanaForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -102,7 +113,8 @@ export const EditDynamicSecretSapHanaForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -259,6 +271,24 @@ export const EditDynamicSecretSapHanaForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
|
||||
+32
-7
@@ -19,6 +19,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
@@ -52,11 +53,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -85,6 +83,7 @@ export const EditDynamicSecretSnowflakeForm = ({
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
@@ -93,10 +92,17 @@ export const EditDynamicSecretSnowflakeForm = ({
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
@@ -106,7 +112,8 @@ export const EditDynamicSecretSnowflakeForm = ({
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -262,6 +269,24 @@ export const EditDynamicSecretSnowflakeForm = ({
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
|
||||
+28
-7
@@ -25,6 +25,7 @@ import { OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { MetadataForm } from "../MetadataForm";
|
||||
|
||||
@@ -87,17 +88,15 @@ const formSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional(),
|
||||
newName: slugSchema().optional(),
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string().trim().default("")
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
.optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -139,6 +138,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
metadata: dynamicSecret.metadata,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"]),
|
||||
passwordRequirements:
|
||||
@@ -161,11 +161,13 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
metadata
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
@@ -179,7 +181,8 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
gatewayId: isGatewayInActive ? null : inputs.gatewayId
|
||||
},
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
metadata
|
||||
metadata,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
@@ -427,6 +430,24 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
Creation, Revocation & Renew Statements (optional)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Customize SQL statements for managing database user lifecycle
|
||||
</div>
|
||||
|
||||
+2
-5
@@ -8,6 +8,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
enum ConfigType {
|
||||
URL = "url",
|
||||
@@ -50,11 +51,7 @@ const formSchema = z.object({
|
||||
})
|
||||
])
|
||||
.optional(),
|
||||
newName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
newName: slugSchema().optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
|
||||
+598
@@ -0,0 +1,598 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
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";
|
||||
|
||||
const passwordRequirementsSchema = z
|
||||
.object({
|
||||
length: z.number().min(1).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250;
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length");
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
database: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
gatewayId: z.string().optional().nullable()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
usernameTemplate: z.string().trim().nullable().optional(),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretVerticaForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const getDefaultPasswordRequirements = () => ({
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"]),
|
||||
passwordRequirements:
|
||||
(dynamicSecret.inputs as TForm["inputs"])?.passwordRequirements ||
|
||||
getDefaultPasswordRequirements()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const selectedGatewayId = watch("inputs.gatewayId");
|
||||
const isGatewayInActive = gateways?.findIndex((el) => el.id === selectedGatewayId) === -1;
|
||||
|
||||
const handleUpdateDynamicSecret = async ({
|
||||
inputs,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
newName,
|
||||
usernameTemplate
|
||||
}: TForm) => {
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs: {
|
||||
...inputs,
|
||||
gatewayId: isGatewayInActive ? null : inputs.gatewayId
|
||||
},
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</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 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.host"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.port"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.database"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Database"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Accordion type="multiple" className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="password-config">
|
||||
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Set constraints on the generated database password
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.length"
|
||||
defaultValue={48}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password Length"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={250}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
|
||||
<div className="text-sm text-gray-500">
|
||||
{(() => {
|
||||
const total = Object.values(
|
||||
watch("inputs.passwordRequirements.required") || {}
|
||||
).reduce((sum, count) => sum + Number(count || 0), 0);
|
||||
const length = watch("inputs.passwordRequirements.length") || 0;
|
||||
const isError = total > length;
|
||||
return (
|
||||
<span className={isError ? "text-red-500" : ""}>
|
||||
Total required characters: {total}{" "}
|
||||
{isError ? `(exceeds length of ${length})` : ""}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.lowercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Lowercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of lowercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.uppercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Uppercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of uppercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.digits"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digit Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of digits"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.symbols"
|
||||
defaultValue={0}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbol Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of symbols"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Allowed Symbols</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.allowedSymbols"
|
||||
defaultValue="-_.~!*"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbols to use in password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Default: -_.~!*"
|
||||
>
|
||||
<Input {...field} placeholder="-_.~!*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user