diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5cac80c19..fb0bf84e3 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1727,6 +1727,11 @@ export const SecretSyncs = { keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", tags: "Optional resource tags to add to parameters synced by Infisical.", syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.` + }, + AWS_SECRETS_MANAGER: { + keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", + tags: "Optional tags to add to secrets synced by Infisical.", + syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.` } }, DESTINATION_CONFIG: { diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts index c42a44a7c..44296c306 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts @@ -38,7 +38,7 @@ const AwsParameterStoreSyncOptionsSchema = z.object({ "Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" ) .min(1, "Resource tag key required") - .max(128, "Resource tag name cannot exceed 128 characters"), + .max(128, "Resource tag key cannot exceed 128 characters"), value: z .string() .regex( @@ -50,7 +50,7 @@ const AwsParameterStoreSyncOptionsSchema = z.object({ .array() .max(50) .refine((items) => new Set(items.map((item) => item.key)).size === items.length, { - message: "AWS tag keys must be unique" + message: "Resource tag keys must be unique" }) .optional() .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags), diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts index 545e4d762..c374a209c 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -1,16 +1,28 @@ +import { UntagResourceCommandOutput } from "@aws-sdk/client-kms"; import { BatchGetSecretValueCommand, CreateSecretCommand, CreateSecretCommandInput, DeleteSecretCommand, DeleteSecretResponse, + DescribeSecretCommand, + DescribeSecretCommandInput, ListSecretsCommand, SecretsManagerClient, + TagResourceCommand, + TagResourceCommandOutput, + UntagResourceCommand, UpdateSecretCommand, UpdateSecretCommandInput } from "@aws-sdk/client-secrets-manager"; import { AWSError } from "aws-sdk"; -import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager"; +import { + CreateSecretResponse, + DescribeSecretResponse, + SecretListEntry, + SecretValueEntry, + Tag +} from "aws-sdk/clients/secretsmanager"; import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; @@ -21,6 +33,7 @@ import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-syn type TAwsSecretsRecord = Record; type TAwsSecretValuesRecord = Record; +type TAwsSecretDescriptionsRecord = Record; const MAX_RETRIES = 5; const BATCH_SIZE = 20; @@ -135,6 +148,46 @@ const getSecretValuesRecord = async ( return awsSecretValuesRecord; }; +const describeSecret = async ( + client: SecretsManagerClient, + input: DescribeSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new DescribeSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return describeSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const getSecretDescriptionsRecord = async ( + client: SecretsManagerClient, + awsSecretsRecord: TAwsSecretsRecord +): Promise => { + const awsSecretDescriptionsRecord: TAwsSecretValuesRecord = {}; + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + try { + awsSecretDescriptionsRecord[secretKey] = await describeSecret(client, { + SecretId: secretKey + }); + } catch (error) { + throw new SecretSyncError({ + secretKey, + error + }); + } + } + + return awsSecretDescriptionsRecord; +}; + const createSecret = async ( client: SecretsManagerClient, input: CreateSecretCommandInput, @@ -189,9 +242,71 @@ const deleteSecret = async ( } }; +const addTags = async ( + client: SecretsManagerClient, + secretKey: string, + tags: Tag[], + attempt = 0 +): Promise => { + try { + return await client.send(new TagResourceCommand({ SecretId: secretKey, Tags: tags })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return addTags(client, secretKey, tags, attempt + 1); + } + throw error; + } +}; + +const removeTags = async ( + client: SecretsManagerClient, + secretKey: string, + tagKeys: string[], + attempt = 0 +): Promise => { + try { + return await client.send(new UntagResourceCommand({ SecretId: secretKey, TagKeys: tagKeys })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return removeTags(client, secretKey, tagKeys, attempt + 1); + } + throw error; + } +}; + +const processTags = ({ + syncTagsRecord, + awsTagsRecord +}: { + syncTagsRecord: Record; + awsTagsRecord: Record; +}) => { + const tagsToAdd: Tag[] = []; + const tagKeysToRemove: string[] = []; + + for (const syncEntry of Object.entries(syncTagsRecord)) { + const [syncKey, syncValue] = syncEntry; + + if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey]) + tagsToAdd.push({ Key: syncKey, Value: syncValue }); + } + + for (const awsKey of Object.keys(awsTagsRecord)) { + if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey); + } + + return { tagsToAdd, tagKeysToRemove }; +}; + export const AwsSecretsManagerSyncFns = { syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig } = secretSync; + const { destinationConfig, syncOptions } = secretSync; const client = await getSecretsManagerClient(secretSync); @@ -199,9 +314,13 @@ export const AwsSecretsManagerSyncFns = { const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + const awsDescriptionsRecord = await getSecretDescriptionsRecord(client, awsSecretsRecord); + + const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []); + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { for await (const entry of Object.entries(secretMap)) { - const [key, { value }] = entry; + const [key, { value, secretMetadata }] = entry; // skip secrets that don't have a value set if (!value) { @@ -211,15 +330,29 @@ export const AwsSecretsManagerSyncFns = { if (awsSecretsRecord[key]) { // skip secrets that haven't changed - if (awsValuesRecord[key]?.SecretString === value) { - // eslint-disable-next-line no-continue - continue; + if ( + awsValuesRecord[key]?.SecretString !== value || + (syncOptions.keyId ?? "alias/aws/secretsmanager") !== awsDescriptionsRecord[key]?.KmsKeyId + ) { + try { + await updateSecret(client, { + SecretId: key, + SecretString: value, + KmsKeyId: syncOptions.keyId + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } } - + } else { try { - await updateSecret(client, { - SecretId: key, - SecretString: value + await createSecret(client, { + Name: key, + SecretString: value, + KmsKeyId: syncOptions.keyId }); } catch (error) { throw new SecretSyncError({ @@ -227,12 +360,34 @@ export const AwsSecretsManagerSyncFns = { secretKey: key }); } - } else { + } + + const { tagsToAdd, tagKeysToRemove } = processTags({ + syncTagsRecord: { + // configured sync tags take preference over secret metadata + ...(syncOptions.syncSecretMetadataAsTags && + Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), + ...syncTagsRecord + }, + awsTagsRecord: Object.fromEntries( + awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] + ) + }); + + if (tagsToAdd.length) { try { - await createSecret(client, { - Name: key, - SecretString: value + await addTags(client, key, tagsToAdd); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key }); + } + } + + if (tagKeysToRemove.length) { + try { + await removeTags(client, key, tagKeysToRemove); } catch (error) { throw new SecretSyncError({ error, @@ -264,15 +419,46 @@ export const AwsSecretsManagerSyncFns = { if (awsValuesRecord[destinationConfig.secretName]) { await updateSecret(client, { SecretId: destinationConfig.secretName, - SecretString: secretValue + SecretString: secretValue, + KmsKeyId: syncOptions.keyId }); } else { await createSecret(client, { Name: destinationConfig.secretName, - SecretString: secretValue + SecretString: secretValue, + KmsKeyId: syncOptions.keyId }); } + const { tagsToAdd, tagKeysToRemove } = processTags({ + syncTagsRecord, + awsTagsRecord: Object.fromEntries( + awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] + ) + }); + + if (tagsToAdd.length) { + try { + await addTags(client, destinationConfig.secretName, tagsToAdd); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: destinationConfig.secretName + }); + } + } + + if (tagKeysToRemove.length) { + try { + await removeTags(client, destinationConfig.secretName, tagKeysToRemove); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: destinationConfig.secretName + }); + } + } + for await (const secretKey of Object.keys(awsSecretsRecord)) { if (secretKey === destinationConfig.secretName) { // eslint-disable-next-line no-continue diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts index adcef458d..5e8ce2bad 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts @@ -39,11 +39,51 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z }) ); +const AwsSecretsManagerSyncOptionsSchema = z.object({ + keyId: z + .string() + .regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID") + .min(1, "Invalid KMS Key ID") + .max(256, "Invalid KMS Key ID") + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId), + tags: z + .object({ + key: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .min(1, "Tag key required") + .max(128, "Tag key cannot exceed 128 characters"), + value: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .max(256, "Tag value cannot exceed 256 characters") + }) + .array() + .max(50) + .refine((items) => new Set(items.map((item) => item.key)).size === items.length, { + message: "Tag keys must be unique" + }) + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.tags), + syncSecretMetadataAsTags: z + .boolean() + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.syncSecretMetadataAsTags) +}); + const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema( SecretSync.AWSSecretsManager, - AwsSecretsManagerSyncOptionsConfig + AwsSecretsManagerSyncOptionsConfig, + AwsSecretsManagerSyncOptionsSchema ).extend({ destination: z.literal(SecretSync.AWSSecretsManager), destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema @@ -51,17 +91,43 @@ export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema( export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema( SecretSync.AWSSecretsManager, - AwsSecretsManagerSyncOptionsConfig -).extend({ - destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema -}); + AwsSecretsManagerSyncOptionsConfig, + AwsSecretsManagerSyncOptionsSchema +) + .extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema + }) + .superRefine((sync, ctx) => { + if ( + sync.destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && + sync.syncOptions.syncSecretMetadataAsTags + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.' + }); + } + }); export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema( SecretSync.AWSSecretsManager, - AwsSecretsManagerSyncOptionsConfig -).extend({ - destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() -}); + AwsSecretsManagerSyncOptionsConfig, + AwsSecretsManagerSyncOptionsSchema +) + .extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() + }) + .superRefine((sync, ctx) => { + if ( + sync.destinationConfig?.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && + sync.syncOptions.syncSecretMetadataAsTags + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.' + }); + } + }); export const AwsSecretsManagerSyncListItemSchema = z.object({ name: z.literal("AWS Secrets Manager"), diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png index abd2c0ad1..89ec35e4d 100644 Binary files a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png differ diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index ece01374f..f7339d80c 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -88,10 +88,10 @@ Infisical supports two methods for connecting to AWS. "secretsmanager:DescribeSecret", "secretsmanager:TagResource", "secretsmanager:UntagResource", - "kms:ListKeys", - "kms:ListAliases", - "kms:Encrypt", - "kms:Decrypt" + "kms:ListKeys", // if you need to specify the KMS key + "kms:ListAliases", // if you need to specify the KMS key + "kms:Encrypt", // if you need to specify the KMS key + "kms:Decrypt" // if you need to specify the KMS key ], "Resource": "*" } @@ -231,10 +231,10 @@ Infisical supports two methods for connecting to AWS. "secretsmanager:DescribeSecret", "secretsmanager:TagResource", "secretsmanager:UntagResource", - "kms:ListKeys", - "kms:ListAliases", - "kms:Encrypt", - "kms:Decrypt" + "kms:ListKeys", // if you need to specify the KMS key + "kms:ListAliases", // if you need to specify the KMS key + "kms:Encrypt", // if you need to specify the KMS key + "kms:Decrypt" // if you need to specify the KMS key ], "Resource": "*" } diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 8a7d6394f..165998841 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. - - **Key ID**: The AWS KMS key ID or alias to encrypt parameters with. + - **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with. - **Tags**: Optional resource tags to add to parameters synced by Infisical. - **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical. Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index 50fa75828..b8df0e8b3 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -43,6 +43,9 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with. + - **Tags**: Optional tags to add to secrets synced by Infisical. + - **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical. - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. 6. Configure the **Details** of your Secrets Manager Sync, then click **Next**. diff --git a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx index 0a56714c5..8a9816312 100644 --- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx @@ -214,7 +214,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop errorText={error?.message} > { - const { control, watch } = useFormContext< + const { control, watch, setValue } = useFormContext< TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } >(); @@ -59,7 +59,10 @@ export const AwsSecretsManagerSyncFields = () => { > + + )} + /> + +
+ {i === 0 && ( + + )} + ( + + + + )} + /> +
+ + tagFields.remove(i)} + > + + + + + ))} + +
+ +
+ {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && ( + ( + + +

+ Sync Secret Metadata as Tags{" "} + +

+ If enabled, metadata attached to secrets will be added as tags to secrets + synced by Infisical. +

+

+ Manually configured tags from the field above will take precedence over + secret metadata when tag keys conflict. +

+ + } + > + + +

+
+
+ )} + /> + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 73a5256de..ad9a26408 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -9,6 +9,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields"; +import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields"; type Props = { hideInitialSync?: boolean; @@ -30,6 +31,8 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { AdditionalSyncOptionsFieldsComponent = ; break; case SecretSync.AWSSecretsManager: + AdditionalSyncOptionsFieldsComponent = ; + break; case SecretSync.GitHub: case SecretSync.GCPSecretManager: case SecretSync.AzureKeyVault: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx index d59fe9500..f492792de 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx @@ -1,8 +1,10 @@ import { useFormContext } from "react-hook-form"; +import { faEye } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; -import { Badge } from "@app/components/v2"; +import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2"; import { AWS_REGIONS } from "@app/helpers/appConnections"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; @@ -37,3 +39,55 @@ export const AwsSecretsManagerSyncReviewFields = () => { ); }; + +export const AwsSecretsManagerSyncOptionsReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } + >(); + + const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]); + + return ( + <> + {keyId && {keyId}} + {tags && tags.length > 0 && ( + + + + Key + Value + + + {tags.map((tag) => ( + + {tag.key} + {tag.value} + + ))} + + + } + > +
+ + + + {tags.length} Tag{tags.length > 1 ? "s" : ""} + + +
+
+
+ )} + {syncSecretMetadataAsTags && ( + + Enabled + + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 167fc830a..2846433ec 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -11,7 +11,10 @@ import { AwsParameterStoreDestinationReviewFields, AwsParameterStoreSyncOptionsReviewFields } from "./AwsParameterStoreSyncReviewFields"; -import { AwsSecretsManagerSyncReviewFields } from "./AwsSecretsManagerSyncReviewFields"; +import { + AwsSecretsManagerSyncOptionsReviewFields, + AwsSecretsManagerSyncReviewFields +} from "./AwsSecretsManagerSyncReviewFields"; import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; @@ -47,6 +50,7 @@ export const SecretSyncReviewFields = () => { break; case SecretSync.AWSSecretsManager: DestinationFieldsComponent = ; + AdditionalSyncOptionsFieldsComponent = ; break; case SecretSync.GitHub: DestinationFieldsComponent = ; diff --git a/frontend/src/components/secret-syncs/forms/schemas/aws-parameter-store-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/aws-parameter-store-sync-destination-schema.ts index 8c89baddd..e2ccb9854 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/aws-parameter-store-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/aws-parameter-store-sync-destination-schema.ts @@ -15,7 +15,7 @@ export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema( "Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" ) .min(1, "Key required") - .max(128, "AWS tag name cannot exceed 128 characters"), + .max(128, "Tag key cannot exceed 128 characters"), value: z .string() .regex( diff --git a/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts index e44dfd93a..1bb161034 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts @@ -4,7 +4,33 @@ import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas import { SecretSync } from "@app/hooks/api/secretSyncs"; import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; -export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema().merge( +export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema( + z.object({ + keyId: z.string().optional(), + tags: z + .object({ + key: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .min(1, "Key required") + .max(128, "Tag key cannot exceed 128 characters"), + value: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .max(256, "Tag value cannot exceed 256 characters") + }) + .array() + .max(50) + .optional(), + syncSecretMetadataAsTags: z.boolean().optional() + }) +).merge( z.object({ destination: z.literal(SecretSync.AWSSecretsManager), destinationConfig: z diff --git a/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts b/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts index 1bcf7c7df..0951adae0 100644 --- a/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts @@ -1,6 +1,6 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; +import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; export type TAwsSecretsManagerSync = TRootSecretSync & { destination: SecretSync.AWSSecretsManager; @@ -19,6 +19,11 @@ export type TAwsSecretsManagerSync = TRootSecretSync & { name: string; id: string; }; + syncOptions: RootSyncOptions & { + keyId?: string; + tags?: { key: string; value?: string }[]; + syncSecretMetadataAsTags?: boolean; + }; }; export enum AwsSecretsManagerSyncMappingBehavior { OneToOne = "one-to-one", diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/AwsSecretsManagerSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/AwsSecretsManagerSyncOptionsSection.tsx new file mode 100644 index 000000000..8e103ba18 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/AwsSecretsManagerSyncOptionsSection.tsx @@ -0,0 +1,60 @@ +import { faEye } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2"; +import { TAwsSecretsManagerSync } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +type Props = { + secretSync: TAwsSecretsManagerSync; +}; + +export const AwsSecretsManagerSyncOptionsSection = ({ secretSync }: Props) => { + const { + syncOptions: { keyId, tags, syncSecretMetadataAsTags } + } = secretSync; + + return ( + <> + {keyId && {keyId}} + {tags && tags.length > 0 && ( + + + + Key + Value + + + {tags.map((tag) => ( + + {tag.key} + {tag.value} + + ))} + + + } + > +
+ + + + {tags.length} Tag{tags.length > 1 ? "s" : ""} + + +
+
+
+ )} + {syncSecretMetadataAsTags && ( + + Enabled + + )} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index c0a4bdc3f..df6927b48 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -11,6 +11,7 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs" import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection"; +import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection"; type Props = { secretSync: TSecretSync; @@ -36,6 +37,10 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = ); break; case SecretSync.AWSSecretsManager: + AdditionalSyncOptionsComponent = ( + + ); + break; case SecretSync.GitHub: case SecretSync.GCPSecretManager: case SecretSync.AzureKeyVault: