feat: ui for aws dynamic secret

This commit is contained in:
=
2025-06-06 21:35:25 +05:30
parent 09a7346f32
commit d6c321d34d
3 changed files with 237 additions and 91 deletions
+25 -9
View File
@@ -44,6 +44,11 @@ export enum SqlProviders {
MsSQL = "mssql" MsSQL = "mssql"
} }
export enum DynamicSecretAwsIamAuth {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
export type TDynamicSecretProvider = export type TDynamicSecretProvider =
| { | {
type: DynamicSecretProviders.SqlDatabase; type: DynamicSecretProviders.SqlDatabase;
@@ -78,15 +83,26 @@ export type TDynamicSecretProvider =
} }
| { | {
type: DynamicSecretProviders.AwsIam; type: DynamicSecretProviders.AwsIam;
inputs: { inputs:
accessKey: string; | {
secretAccessKey: string; method: DynamicSecretAwsIamAuth.AccessKey;
region: string; accessKey: string;
awsPath?: string; secretAccessKey: string;
policyDocument?: string; region: string;
userGroups?: string; awsPath?: string;
policyArns?: string; policyDocument?: string;
}; userGroups?: string;
policyArns?: string;
}
| {
method: DynamicSecretAwsIamAuth.AssumeRole;
roleArn: string;
region: string;
awsPath?: string;
policyDocument?: string;
userGroups?: string;
policyArns?: string;
};
} }
| { | {
type: DynamicSecretProviders.Redis; type: DynamicSecretProviders.Redis;
@@ -5,22 +5,46 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; import {
Button,
FilterableSelect,
FormControl,
Input,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api"; import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import {
DynamicSecretAwsIamAuth,
DynamicSecretProviders
} from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({ const formSchema = z.object({
provider: z.object({ provider: z.discriminatedUnion("method", [
accessKey: z.string().trim().min(1), z.object({
secretAccessKey: z.string().trim().min(1), method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
region: z.string().trim().min(1), accessKey: z.string().trim().min(1),
awsPath: z.string().trim().optional(), secretAccessKey: z.string().trim().min(1),
permissionBoundaryPolicyArn: z.string().trim().optional(), region: z.string().trim().min(1),
policyDocument: z.string().trim().optional(), awsPath: z.string().trim().optional(),
userGroups: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
policyArns: z.string().trim().optional() policyDocument: z.string().trim().optional(),
}), userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional()
}),
z.object({
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
roleArn: z.string().trim().min(1),
region: z.string().trim().min(1),
awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional()
})
]),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -67,16 +91,21 @@ export const AwsIamInputForm = ({
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit,
watch
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
environment: isSingleEnvironmentMode ? environments[0] : undefined, environment: isSingleEnvironmentMode ? environments[0] : undefined,
usernameTemplate: "{{randomUsername}}" usernameTemplate: "{{randomUsername}}",
provider: {
method: DynamicSecretAwsIamAuth.AssumeRole
}
} }
}); });
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey;
const handleCreateDynamicSecret = async ({ const handleCreateDynamicSecret = async ({
name, name,
@@ -127,7 +156,7 @@ export const AwsIamInputForm = ({
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="dynamic-postgres" /> <Input {...field} placeholder="dynamic-aws-iam" />
</FormControl> </FormControl>
)} )}
/> />
@@ -170,38 +199,82 @@ export const AwsIamInputForm = ({
Configuration Configuration
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center space-x-2"> <Controller
<Controller name="provider.method"
control={control} control={control}
name="provider.accessKey" render={({ field: { value, onChange }, fieldState: { error } }) => (
defaultValue="" <FormControl
render={({ field, fieldState: { error } }) => ( errorText={error?.message}
<FormControl isError={Boolean(error?.message)}
label="AWS Access Key" label="Method"
className="flex-grow" >
isError={Boolean(error?.message)} <Select
errorText={error?.message} value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<Input {...field} /> <SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
</FormControl> Assume Role (Recommended)
)} </SelectItem>
/> <SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
<Controller </Select>
control={control} </FormControl>
name="provider.secretAccessKey" )}
defaultValue="" />
render={({ field, fieldState: { error } }) => ( {isAccessKeyMethod ? (
<FormControl <div className="flex items-center space-x-2">
label="AWS Secret Key" <Controller
className="flex-grow" control={control}
isError={Boolean(error?.message)} name="provider.accessKey"
errorText={error?.message} defaultValue=""
> render={({ field, fieldState: { error } }) => (
<Input {...field} type="password" /> <FormControl
</FormControl> label="AWS Access Key"
)} className="flex-grow"
/> isError={Boolean(error?.message)}
</div> errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.secretAccessKey"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="AWS Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="password" />
</FormControl>
)}
/>
</div>
) : (
<div className="flex items-center space-x-2">
<Controller
control={control}
name="provider.roleArn"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role ARN"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}
@@ -5,14 +5,15 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api"; import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({ const formSchema = z.object({
inputs: z inputs: z.discriminatedUnion("method", [
.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
accessKey: z.string().trim().min(1), accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
@@ -21,8 +22,18 @@ const formSchema = z.object({
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional() policyArns: z.string().trim().optional()
}),
z.object({
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
roleArn: z.string().trim().min(1),
region: z.string().trim().min(1),
awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional()
}) })
.partial(), ]),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -66,6 +77,7 @@ export const EditDynamicSecretAwsIamForm = ({
}: Props) => { }: Props) => {
const { const {
control, control,
watch,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
@@ -80,6 +92,7 @@ export const EditDynamicSecretAwsIamForm = ({
} }
} }
}); });
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
@@ -173,38 +186,82 @@ export const EditDynamicSecretAwsIamForm = ({
<div> <div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div> <div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center space-x-2"> <Controller
<Controller name="inputs.method"
control={control} control={control}
name="inputs.accessKey" render={({ field: { value, onChange }, fieldState: { error } }) => (
defaultValue="" <FormControl
render={({ field, fieldState: { error } }) => ( errorText={error?.message}
<FormControl isError={Boolean(error?.message)}
label="AWS Access Key" label="Method"
className="flex-grow" >
isError={Boolean(error?.message)} <Select
errorText={error?.message} value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<Input {...field} /> <SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
</FormControl> Assume Role (Recommended)
)} </SelectItem>
/> <SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
<Controller </Select>
control={control} </FormControl>
name="inputs.secretAccessKey" )}
defaultValue="" />
render={({ field, fieldState: { error } }) => ( {isAccessKeyMethod ? (
<FormControl <div className="flex items-center space-x-2">
label="AWS Secret Key" <Controller
className="flex-grow" control={control}
isError={Boolean(error?.message)} name="inputs.accessKey"
errorText={error?.message} defaultValue=""
> render={({ field, fieldState: { error } }) => (
<Input {...field} type="password" /> <FormControl
</FormControl> label="AWS Access Key"
)} className="flex-grow"
/> isError={Boolean(error?.message)}
</div> errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.secretAccessKey"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="AWS Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="password" />
</FormControl>
)}
/>
</div>
) : (
<div className="flex items-center space-x-2">
<Controller
control={control}
name="inputs.roleArn"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role ARN"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}