Merge branch 'main' into feat/point-in-time-revamp

This commit is contained in:
carlosmonastyrski
2025-06-05 18:31:27 -03:00
757 changed files with 31611 additions and 4112 deletions
@@ -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"
@@ -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}
@@ -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"
@@ -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>
@@ -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}
@@ -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>
);
};
@@ -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>
@@ -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
@@ -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}
@@ -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}
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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>
@@ -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() })
});
@@ -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>
);
};
@@ -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;
@@ -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"
@@ -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">
@@ -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>;
@@ -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"
@@ -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>
@@ -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>
);
};
@@ -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>
);
};
@@ -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}>
@@ -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
@@ -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">
@@ -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>
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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>
@@ -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>;
@@ -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>
);
};