feat(dynamic-secret): Add custom tags to AWS IAM dynamic secret

This commit is contained in:
carlosmonastyrski
2025-06-17 16:06:35 -03:00
parent bfee34f38d
commit 489bd124d2
12 changed files with 96 additions and 18 deletions

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.DynamicSecret, "tags");
if (!hasCol) {
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.jsonb("tags").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.DynamicSecret, "tags");
if (hasCol) {
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.dropColumn("tags");
});
}
}

View File

@@ -29,7 +29,8 @@ export const DynamicSecretsSchema = z.object({
encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional(),
usernameTemplate: z.string().nullable().optional()
usernameTemplate: z.string().nullable().optional(),
tags: z.unknown().nullable().optional()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -80,6 +80,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
metadata: ResourceMetadataSchema.optional(),
tags: ResourceMetadataSchema.optional(),
usernameTemplate: userTemplateSchema.optional()
}),
response: {
@@ -146,6 +147,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
.nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
metadata: ResourceMetadataSchema.optional(),
tags: ResourceMetadataSchema.optional(),
usernameTemplate: userTemplateSchema.nullable().optional()
})
}),

View File

@@ -166,6 +166,7 @@ export const dynamicSecretLeaseServiceFactory = ({
usernameTemplate: dynamicSecretCfg.usernameTemplate,
identity,
metadata: { projectId },
tags: dynamicSecretCfg.tags as Record<string, string>[] | undefined,
config
});
} catch (error: unknown) {

View File

@@ -79,6 +79,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
actorAuthMethod,
metadata,
tags,
usernameTemplate
}: TCreateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
@@ -165,7 +166,8 @@ export const dynamicSecretServiceFactory = ({
folderId: folder.id,
name,
gatewayId: selectedGatewayId,
usernameTemplate
usernameTemplate,
tags: JSON.stringify(tags)
},
tx
);
@@ -202,6 +204,7 @@ export const dynamicSecretServiceFactory = ({
actorOrgId,
actorAuthMethod,
metadata,
tags,
usernameTemplate
}: TUpdateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
@@ -315,7 +318,8 @@ export const dynamicSecretServiceFactory = ({
name: newName ?? name,
status: null,
gatewayId: selectedGatewayId,
usernameTemplate
usernameTemplate,
tags: JSON.stringify(tags)
},
tx
);

View File

@@ -23,6 +23,7 @@ export type TCreateDynamicSecretDTO = {
projectSlug: string;
metadata?: ResourceMetadataDTO;
usernameTemplate?: string | null;
tags?: ResourceMetadataDTO;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateDynamicSecretDTO = {
@@ -35,6 +36,7 @@ export type TUpdateDynamicSecretDTO = {
inputs?: TProvider["inputs"];
projectSlug: string;
metadata?: ResourceMetadataDTO;
tags?: ResourceMetadataDTO;
usernameTemplate?: string | null;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -119,20 +119,31 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
identity?: {
name: string;
};
tags?: Record<string, string>[];
metadata: { projectId: string };
}) => {
const { inputs, usernameTemplate, metadata, identity } = data;
const { inputs, usernameTemplate, metadata, identity, tags } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs, metadata.projectId);
const username = generateUsername(usernameTemplate, identity);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
if (tags && Array.isArray(tags)) {
const additionalTags = tags.map((tag) => ({
Key: tag.key,
Value: tag.value
}));
awsTags.push(...additionalTags);
}
const createUserRes = await client.send(
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }],
Tags: awsTags,
UserName: username
})
);

View File

@@ -525,6 +525,7 @@ export type TDynamicProviderFns = {
name: string;
};
metadata: { projectId: string };
tags?: Record<string, string>[];
config?: TDynamicSecretLeaseConfig;
}) => Promise<{ entityId: string; data: unknown }>;
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;

View File

@@ -15,6 +15,7 @@ export type TDynamicSecret = {
maxTTL: string;
usernameTemplate?: string | null;
metadata?: { key: string; value: string }[];
tags?: { key: string; value: string }[];
};
export enum DynamicSecretProviders {
@@ -89,6 +90,7 @@ export type TDynamicSecretProvider =
}
| {
type: DynamicSecretProviders.AwsIam;
tags?: { key: string; value: string }[];
inputs:
| {
method: DynamicSecretAwsIamAuth.AccessKey;
@@ -345,6 +347,7 @@ export type TCreateDynamicSecretDTO = {
name: string;
metadata?: { key: string; value: string }[];
usernameTemplate?: string;
tags?: { key: string; value: string }[];
};
export type TUpdateDynamicSecretDTO = {
@@ -359,6 +362,7 @@ export type TUpdateDynamicSecretDTO = {
maxTTL?: string | null;
inputs?: unknown;
usernameTemplate?: string | null;
tags?: { key: string; value: string }[];
};
};

View File

@@ -21,6 +21,8 @@ import {
} from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { MetadataForm } from "../../DynamicSecretListView/MetadataForm";
const formSchema = z.object({
provider: z.discriminatedUnion("method", [
z.object({
@@ -67,7 +69,10 @@ const formSchema = z.object({
}),
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()
usernameTemplate: z.string().nullable().optional(),
tags: z
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
.optional()
});
type TForm = z.infer<typeof formSchema>;
@@ -100,7 +105,8 @@ export const AwsIamInputForm = ({
usernameTemplate: "{{randomUsername}}",
provider: {
method: DynamicSecretAwsIamAuth.AssumeRole
}
},
tags: []
}
});
@@ -113,7 +119,8 @@ export const AwsIamInputForm = ({
provider,
defaultTTL,
environment,
usernameTemplate
usernameTemplate,
tags
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
@@ -129,7 +136,8 @@ export const AwsIamInputForm = ({
projectSlug,
environmentSlug: environment.slug,
usernameTemplate:
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate,
tags
});
onCompleted();
} catch {
@@ -398,6 +406,7 @@ export const AwsIamInputForm = ({
</FormControl>
)}
/>
<MetadataForm control={control} name="tags" isValueRequired />
{!isSingleEnvironmentMode && (
<Controller
control={control}

View File

@@ -10,6 +10,8 @@ import { useUpdateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { slugSchema } from "@app/lib/schemas";
import { MetadataForm } from "../MetadataForm";
const formSchema = z.object({
inputs: z.discriminatedUnion("method", [
z.object({
@@ -56,6 +58,9 @@ const formSchema = z.object({
})
.nullable(),
newName: slugSchema().optional(),
tags: z
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
.optional(),
usernameTemplate: z.string().trim().nullable().optional()
});
type TForm = z.infer<typeof formSchema>;
@@ -89,7 +94,8 @@ export const EditDynamicSecretAwsIamForm = ({
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
inputs: {
...(dynamicSecret.inputs as TForm["inputs"])
}
},
tags: dynamicSecret.tags
}
});
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
@@ -101,7 +107,8 @@ export const EditDynamicSecretAwsIamForm = ({
maxTTL,
defaultTTL,
newName,
usernameTemplate
usernameTemplate,
tags
}: TForm) => {
// wait till previous request is finished
if (updateDynamicSecret.isPending) return;
@@ -117,7 +124,9 @@ export const EditDynamicSecretAwsIamForm = ({
defaultTTL,
inputs,
newName: newName === dynamicSecret.name ? undefined : newName,
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
usernameTemplate:
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate,
tags
}
});
onClose();
@@ -380,6 +389,7 @@ export const EditDynamicSecretAwsIamForm = ({
</FormControl>
)}
/>
<MetadataForm control={control} name="tags" isValueRequired />
</div>
</div>
<div className="mt-4 flex items-center space-x-4">

View File

@@ -4,14 +4,22 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, FormLabel, IconButton, Input } from "@app/components/v2";
export const MetadataForm = ({ control }: { control: Control<any> }) => {
export const MetadataForm = ({
control,
name = "metadata",
isValueRequired = false
}: {
control: Control<any>;
name?: string;
isValueRequired?: boolean;
}) => {
const metadataFormFields = useFieldArray({
control,
name: "metadata"
name
});
return (
<FormControl label="Metadata">
<FormControl label={name.charAt(0).toUpperCase() + name.slice(1)}>
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
@@ -19,7 +27,7 @@ export const MetadataForm = ({ control }: { control: Control<any> }) => {
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`metadata.${i}.key`}
name={`${name}.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
@@ -33,11 +41,15 @@ export const MetadataForm = ({ control }: { control: Control<any> }) => {
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional={!isValueRequired}
/>
)}
<Controller
control={control}
name={`metadata.${i}.value`}
name={`${name}.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}