diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index eca11e983..2d491314a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1719,6 +1719,12 @@ export const SecretSyncs = { REGION: "The AWS region to sync secrets to.", PATH: "The Parameter Store path to sync secrets to." }, + AWS_SECRETS_MANAGER: { + REGION: "The AWS region to sync secrets to.", + MAPPING_BEHAVIOR: + "How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.", + SECRET_NAME: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one." + }, GITHUB: { ORG: "The name of the GitHub organization.", OWNER: "The name of the GitHub account owner of the repository.", diff --git a/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts new file mode 100644 index 000000000..ff2cc5a84 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts @@ -0,0 +1,17 @@ +import { + AwsSecretsManagerSyncSchema, + CreateAwsSecretsManagerSyncSchema, + UpdateAwsSecretsManagerSyncSchema +} from "@app/services/secret-sync/aws-secrets-manager"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAwsSecretsManagerSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AWSSecretsManager, + server, + responseSchema: AwsSecretsManagerSyncSchema, + createSchema: CreateAwsSecretsManagerSyncSchema, + updateSchema: UpdateAwsSecretsManagerSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 6b3d2f15f..20573719b 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -1,6 +1,7 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; +import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; @@ -8,6 +9,7 @@ export * from "./secret-sync-router"; export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { [SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter, + [SecretSync.AWSSecretsManager]: registerAwsSecretsManagerSyncRouter, [SecretSync.GitHub]: registerGitHubSyncRouter, [SecretSync.GCPSecretManager]: registerGcpSyncRouter }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 2703f8e44..52bb67428 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -9,17 +9,23 @@ import { AwsParameterStoreSyncListItemSchema, AwsParameterStoreSyncSchema } from "@app/services/secret-sync/aws-parameter-store"; +import { + AwsSecretsManagerSyncListItemSchema, + AwsSecretsManagerSyncSchema +} from "@app/services/secret-sync/aws-secrets-manager"; import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; const SecretSyncSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncSchema, + AwsSecretsManagerSyncSchema, GitHubSyncSchema, GcpSyncSchema ]); const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncListItemSchema, + AwsSecretsManagerSyncListItemSchema, GitHubSyncListItemSchema, GcpSyncListItemSchema ]); diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index a495f9e83..6d7739afa 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -69,6 +69,8 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise; +type TAwsSecretValuesRecord = Record; + +const MAX_RETRIES = 5; +const BATCH_SIZE = 20; + +const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => { + const { destinationConfig, connection } = secretSync; + + const config = await getAwsConnectionConfig(connection, destinationConfig.region); + + const secretsManagerClient = new SecretsManagerClient({ + region: config.region, + credentials: config.credentials! + }); + + return secretsManagerClient; +}; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + +const getSecretsRecord = async (client: SecretsManagerClient): Promise => { + const awsSecretsRecord: TAwsSecretsRecord = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const output = await client.send(new ListSecretsCommand({ NextToken: nextToken })); + + attempt = 0; + + if (output.SecretList) { + output.SecretList.forEach((secretEntry) => { + if (secretEntry.Name) { + awsSecretsRecord[secretEntry.Name] = secretEntry; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + + return awsSecretsRecord; +}; + +const getSecretValuesRecord = async ( + client: SecretsManagerClient, + awsSecretsRecord: TAwsSecretsRecord +): Promise => { + const awsSecretValuesRecord: TAwsSecretValuesRecord = {}; + let attempt = 0; + + const secretIdList = Object.keys(awsSecretsRecord); + + for (let i = 0; i < secretIdList.length; i += BATCH_SIZE) { + const batchSecretIds = secretIdList.slice(i, i + BATCH_SIZE); + let hasNext = true; + let nextToken: string | undefined; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const output = await client.send( + new BatchGetSecretValueCommand({ + SecretIdList: batchSecretIds, + NextToken: nextToken + }) + ); + + attempt = 0; + + if (output.SecretValues) { + output.SecretValues.forEach((secretValueEntry) => { + if (secretValueEntry.Name) { + awsSecretValuesRecord[secretValueEntry.Name] = secretValueEntry; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + } + + return awsSecretValuesRecord; +}; + +const createSecret = async ( + client: SecretsManagerClient, + input: CreateSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new CreateSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return createSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const updateSecret = async ( + client: SecretsManagerClient, + input: UpdateSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new UpdateSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return updateSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const deleteSecret = async ( + client: SecretsManagerClient, + secretKey: string, + attempt = 0 +): Promise => { + try { + return await client.send(new DeleteSecretCommand({ SecretId: secretKey, ForceDeleteWithoutRecovery: true })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return deleteSecret(client, secretKey, attempt + 1); + } + throw error; + } +}; + +export const AwsSecretsManagerSyncFns = { + syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig } = secretSync; + + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + + const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + // skip secrets that don't have a value set + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + if (awsSecretsRecord[key]) { + // skip secrets that haven't changed + if (awsValuesRecord[key]?.SecretString === value) { + // eslint-disable-next-line no-continue + continue; + } + + try { + await updateSecret(client, { + SecretId: key, + SecretString: value + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } else { + try { + await createSecret(client, { + Name: key, + SecretString: value + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (!(secretKey in secretMap) || !secretMap[secretKey].value) { + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + } else { + // Many-To-One Mapping + + const secretValue = JSON.stringify( + Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value])) + ); + + if (awsValuesRecord[destinationConfig.secretName]) { + await updateSecret(client, { + SecretId: destinationConfig.secretName, + SecretString: secretValue + }); + } else { + await createSecret(client, { + Name: destinationConfig.secretName, + SecretString: secretValue + }); + } + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (secretKey === destinationConfig.secretName) { + // eslint-disable-next-line no-continue + continue; + } + + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + }, + getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise => { + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + + const { destinationConfig } = secretSync; + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + return Object.fromEntries( + Object.keys(awsSecretsRecord).map((key) => [key, { value: awsValuesRecord[key].SecretString ?? "" }]) + ); + } + + // Many-To-One Mapping + + const secretValueEntry = awsValuesRecord[destinationConfig.secretName]; + + if (!secretValueEntry) return {}; + + try { + const parsedValue = (secretValueEntry.SecretString ? JSON.parse(secretValueEntry.SecretString) : {}) as Record< + string, + string + >; + + return Object.fromEntries(Object.entries(parsedValue).map(([key, value]) => [key, { value }])); + } catch { + throw new SecretSyncError({ + message: + "Failed to import secrets. Invalid format for Many-To-One mapping behavior: requires key/value configuration.", + shouldRetry: false + }); + } + }, + removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig } = secretSync; + + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (secretKey in secretMap) { + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + } else { + await deleteSecret(client, destinationConfig.secretName); + } + } +}; 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 new file mode 100644 index 000000000..b345b1cd2 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; + +const AwsSecretsManagerSyncDestinationConfigSchema = z + .discriminatedUnion("mappingBehavior", [ + z.object({ + mappingBehavior: z + .literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR) + }), + z.object({ + mappingBehavior: z + .literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR), + secretName: z + .string() + .regex( + /^[a-zA-Z0-9/_+=.@-]+$/, + "Secret name must contain only alphanumeric characters and the characters /_+=.@-" + ) + .min(1, "Secret name is required") + .max(256, "Secret name cannot exceed 256 characters") + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.SECRET_NAME) + }) + ]) + .and( + z.object({ + region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.REGION) + }) + ); + +export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({ + destination: z.literal(SecretSync.AWSSecretsManager), + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema +}); + +export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AWSSecretsManager +).extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema +}); + +export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AWSSecretsManager +).extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() +}); + +export const AwsSecretsManagerSyncListItemSchema = z.object({ + name: z.literal("AWS Secrets Manager"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(SecretSync.AWSSecretsManager), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts new file mode 100644 index 000000000..848dce6bc --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws"; + +import { + AwsSecretsManagerSyncListItemSchema, + AwsSecretsManagerSyncSchema, + CreateAwsSecretsManagerSyncSchema +} from "./aws-secrets-manager-sync-schemas"; + +export type TAwsSecretsManagerSync = z.infer; + +export type TAwsSecretsManagerSyncInput = z.infer; + +export type TAwsSecretsManagerSyncListItem = z.infer; + +export type TAwsSecretsManagerSyncWithCredentials = TAwsSecretsManagerSync & { + connection: TAwsConnection; +}; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/index.ts b/backend/src/services/secret-sync/aws-secrets-manager/index.ts new file mode 100644 index 000000000..ff6ccff98 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-secrets-manager-sync-constants"; +export * from "./aws-secrets-manager-sync-fns"; +export * from "./aws-secrets-manager-sync-schemas"; +export * from "./aws-secrets-manager-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 58d68ebb1..14d5f06c7 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -1,5 +1,6 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", + AWSSecretsManager = "aws-secrets-manager", GitHub = "github", GCPSecretManager = "gcp-secret-manager" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index c02b8599d..dbf916995 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -4,6 +4,10 @@ import { AWS_PARAMETER_STORE_SYNC_LIST_OPTION, AwsParameterStoreSyncFns } from "@app/services/secret-sync/aws-parameter-store"; +import { + AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, + AwsSecretsManagerSyncFns +} from "@app/services/secret-sync/aws-secrets-manager"; import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; @@ -18,6 +22,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns"; const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, + [SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION, [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION }; @@ -73,6 +78,8 @@ export const SecretSyncFns = { switch (secretSync.destination) { case SecretSync.AWSParameterStore: return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.AWSSecretsManager: + return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GitHub: return GithubSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GCPSecretManager: @@ -89,6 +96,9 @@ export const SecretSyncFns = { case SecretSync.AWSParameterStore: secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync); break; + case SecretSync.AWSSecretsManager: + secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync); + break; case SecretSync.GitHub: secretMap = await GithubSyncFns.getSecrets(secretSync); break; @@ -110,6 +120,8 @@ export const SecretSyncFns = { switch (secretSync.destination) { case SecretSync.AWSParameterStore: return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.AWSSecretsManager: + return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GitHub: return GithubSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GCPSecretManager: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 2df7d11e6..8ed4cdb1b 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -3,12 +3,14 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", + [SecretSync.AWSSecretsManager]: "AWS Secrets Manager", [SecretSync.GitHub]: "GitHub", [SecretSync.GCPSecretManager]: "GCP Secret Manager" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, + [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, [SecretSync.GCPSecretManager]: AppConnection.GCP }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index fb622ee9a..9ce331e8e 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -2,6 +2,12 @@ import { Job } from "bullmq"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { QueueJobs } from "@app/queue"; +import { + TAwsSecretsManagerSync, + TAwsSecretsManagerSyncInput, + TAwsSecretsManagerSyncListItem, + TAwsSecretsManagerSyncWithCredentials +} from "@app/services/secret-sync/aws-secrets-manager"; import { TGitHubSync, TGitHubSyncInput, @@ -19,16 +25,25 @@ import { } from "./aws-parameter-store"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; -export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync; +export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials + | TAwsSecretsManagerSyncWithCredentials | TGitHubSyncWithCredentials | TGcpSyncWithCredentials; -export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput; +export type TSecretSyncInput = + | TAwsParameterStoreSyncInput + | TAwsSecretsManagerSyncInput + | TGitHubSyncInput + | TGcpSyncInput; -export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem; +export type TSecretSyncListItem = + | TAwsParameterStoreSyncListItem + | TAwsSecretsManagerSyncListItem + | TGitHubSyncListItem + | TGcpSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx new file mode 100644 index 000000000..c7fbe5a19 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx new file mode 100644 index 000000000..aacd95571 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx new file mode 100644 index 000000000..31805aaf6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx new file mode 100644 index 000000000..4eac37dc2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx new file mode 100644 index 000000000..c3cc65be1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx new file mode 100644 index 000000000..1775ba450 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx new file mode 100644 index 000000000..5398766b9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx new file mode 100644 index 000000000..9119f6147 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx new file mode 100644 index 000000000..6d886630d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png new file mode 100644 index 000000000..d788d27f9 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png new file mode 100644 index 000000000..07bbf6212 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png new file mode 100644 index 000000000..a958af9fd Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png differ 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 new file mode 100644 index 000000000..abd2c0ad1 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png new file mode 100644 index 000000000..ddffa9190 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png new file mode 100644 index 000000000..8da1d5601 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png b/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png new file mode 100644 index 000000000..7d32012c1 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png differ diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 3046c7f47..4b41094b4 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -39,7 +39,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. - **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 Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical 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. - **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 Parameter Store Sync, then click **Next**. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx new file mode 100644 index 000000000..50fa75828 --- /dev/null +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -0,0 +1,142 @@ +--- +title: "AWS Secrets Manager Sync" +description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **AWS Secrets Manager** option. + ![Select AWS Secrets Manager](/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **Region**: The AWS region to deploy secrets to. + - **Mapping Behavior**: Specify how Infisical should map secrets to AWS Secrets Manager: + - **One-To-One**: Each Infisical secret will be mapped to a separate AWS Secrets Manager secret. + - **Many-To-One**: All Infisical secrets will be mapped to a single AWS Secrets Manager secret. + - **Secret Name**: Specifies the name of the AWS Secret to map secrets to if **Many-To-One** mapping behavior is selected. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **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. + - **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**. + ![Configure Details](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Secrets Manager Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png) + + 8. If enabled, your Secrets Manager Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png) + + + + To create an **AWS Secrets Manager Sync**, make an API request to the [Create AWS + Secrets Manager Sync](/api-reference/endpoints/secret-syncs/aws-secrets-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/aws-secrets-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-secrets-manager-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "region": "us-east-1", + "mappingBehavior": "one-to-one" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-secrets-manager-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "aws", + "name": "my-aws-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "aws-secrets-manager", + "destinationConfig": { + "region": "us-east-1", + "mappingBehavior": "one-to-one" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 176a562b1..43a46f74b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -406,6 +406,7 @@ "group": "Syncs", "pages": [ "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/aws-secrets-manager", "integrations/secret-syncs/github", "integrations/secret-syncs/gcp-secret-manager" ] @@ -864,6 +865,20 @@ "api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets" ] }, + { + "group": "AWS Secrets Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-secrets-manager/list", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/create", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/update", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/delete", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets" + ] + }, { "group": "GitHub", "pages": [ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx index a77b0cc04..09076ea9c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx @@ -1,30 +1,11 @@ import { Controller, useFormContext } from "react-hook-form"; -import { components, OptionProps, SingleValue } from "react-select"; -import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; -import { Badge, FilterableSelect, FormControl, Input } from "@app/components/v2"; -import { AWS_REGIONS } from "@app/helpers/appConnections"; +import { FormControl, Input } from "@app/components/v2"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; - -const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => { - return ( - -
-

{children}

- - {props.data.slug} - - {isSelected && ( - - )} -
-
- ); -}; +import { AwsRegionSelect } from "./shared"; export const AwsParameterStoreSyncFields = () => { const { control } = useFormContext< @@ -37,17 +18,7 @@ export const AwsParameterStoreSyncFields = () => { ( - region.slug === value)} - onChange={(option) => - onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug) - } - options={AWS_REGIONS} - placeholder="Select region..." - getOptionLabel={(option) => option.name} - getOptionValue={(option) => option.slug} - components={{ Option }} - /> + )} control={control} diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsSecretsManagerSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsSecretsManagerSyncFields.tsx new file mode 100644 index 000000000..d971f4899 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsSecretsManagerSyncFields.tsx @@ -0,0 +1,96 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input, Select, SelectItem } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +import { TSecretSyncForm } from "../schemas"; +import { AwsRegionSelect } from "./shared"; + +export const AwsSecretsManagerSyncFields = () => { + const { control, watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } + >(); + + const mappingBehavior = watch("destinationConfig.mappingBehavior"); + + return ( + <> + + ( + + + + )} + control={control} + name="destinationConfig.region" + /> + ( + +

Specify how Infisical should map secrets to AWS Secrets Manager:

+
    +
  • +

    + One-To-One: Each + Infisical secret will be mapped to a separate AWS Secrets Manager secret. +

    +
  • +
  • +

    + Many-To-One: All + Infisical secrets will be mapped to a single AWS Secrets Manager secret. +

    +
  • +
+ + } + errorText={error?.message} + isError={Boolean(error?.message)} + label="Mapping Behavior" + > + +
+ )} + /> + {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && ( + ( + + + + )} + control={control} + name="destinationConfig.secretName" + /> + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 19c3d7f7c..262677a6f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields"; +import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; @@ -15,6 +16,8 @@ export const SecretSyncDestinationFields = () => { switch (destination) { case SecretSync.AWSParameterStore: return ; + case SecretSync.AWSSecretsManager: + return ; case SecretSync.GitHub: return ; case SecretSync.GCPSecretManager: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/AwsRegionSelect.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/AwsRegionSelect.tsx new file mode 100644 index 000000000..4125393f5 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/AwsRegionSelect.tsx @@ -0,0 +1,41 @@ +import { components, OptionProps, SingleValue } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Badge, FilterableSelect } from "@app/components/v2"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; + +const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => { + return ( + +
+

{children}

+ + {props.data.slug} + + {isSelected && ( + + )} +
+
+ ); +}; + +type Props = { + value: string; + onChange: (value: string | undefined) => void; +}; + +export const AwsRegionSelect = ({ value, onChange }: Props) => { + return ( + region.slug === value)} + onChange={(option) => onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)} + options={AWS_REGIONS} + placeholder="Select region..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.slug} + components={{ Option }} + /> + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/index.ts b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/index.ts new file mode 100644 index 000000000..5ebd689a0 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/index.ts @@ -0,0 +1 @@ +export * from "./AwsRegionSelect"; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx new file mode 100644 index 000000000..d59fe9500 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx @@ -0,0 +1,39 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { Badge } 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"; + +export const AwsSecretsManagerSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } + >(); + + const [region, mappingBehavior, secretName] = watch([ + "destinationConfig.region", + "destinationConfig.mappingBehavior", + "destinationConfig.secretName" + ]); + + const awsRegion = AWS_REGIONS.find((r) => r.slug === region); + + return ( + <> + + {awsRegion?.name} + + {awsRegion?.slug}{" "} + + + + {mappingBehavior} + + {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && ( + {secretName} + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 6a0ddc280..c343532cb 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -3,6 +3,7 @@ import { useFormContext } from "react-hook-form"; import { SecretSyncLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields"; import { Badge } from "@app/components/v2"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs"; @@ -36,6 +37,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.AWSParameterStore: DestinationFieldsComponent = ; break; + case SecretSync.AWSSecretsManager: + DestinationFieldsComponent = ; + break; case SecretSync.GitHub: DestinationFieldsComponent = ; break; 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 new file mode 100644 index 000000000..b1f43e737 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +export const AwsSecretsManagerSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.AWSSecretsManager), + destinationConfig: z + .discriminatedUnion("mappingBehavior", [ + z.object({ + mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) + }), + z.object({ + mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne), + secretName: z + .string() + .regex( + /^[a-zA-Z0-9/_+=.@-]+$/, + "Secret name must contain only alphanumeric characters and the characters /_+=.@-" + ) + .min(1, "Secret name is required") + .max(256, "Secret name cannot exceed 256 characters") + }) + ]) + .and( + z.object({ + region: z.string().min(1, "Region required") + }) + ) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index c3d517f82..817beafde 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema"; import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; import { slugSchema } from "@app/lib/schemas"; @@ -32,6 +33,7 @@ const BaseSecretSyncSchema = z.object({ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, + AwsSecretsManagerSyncDestinationSchema, GitHubSyncDestinationSchema, GcpSyncDestinationSchema ]); diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 905bae9da..a8f2bcf5c 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -6,13 +6,15 @@ import { } from "@app/hooks/api/secretSyncs"; export const SECRET_SYNC_MAP: Record = { - [SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" }, + [SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" }, + [SecretSync.AWSSecretsManager]: { name: "AWS Secrets Manager", image: "Amazon Web Services.png" }, [SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" }, [SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" } }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, + [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, [SecretSync.GCPSecretManager]: AppConnection.GCP }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 11f902388..0f9c820b5 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -1,5 +1,6 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", + AWSSecretsManager = "aws-secrets-manager", GitHub = "github", GCPSecretManager = "gcp-secret-manager" } 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 new file mode 100644 index 000000000..1bcf7c7df --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts @@ -0,0 +1,26 @@ +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"; + +export type TAwsSecretsManagerSync = TRootSecretSync & { + destination: SecretSync.AWSSecretsManager; + destinationConfig: + | { + mappingBehavior: AwsSecretsManagerSyncMappingBehavior.OneToOne; + region: string; + } + | { + region: string; + mappingBehavior: AwsSecretsManagerSyncMappingBehavior.ManyToOne; + secretName: string; + }; + connection: { + app: AppConnection.AWS; + name: string; + id: string; + }; +}; +export enum AwsSecretsManagerSyncMappingBehavior { + OneToOne = "one-to-one", + ManyToOne = "many-to-one" +} diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 88c2a35da..f7e718a0b 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -3,6 +3,7 @@ import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-par import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync"; import { DiscriminativePick } from "@app/types"; +import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; import { TGcpSync } from "./gcp-sync"; export type TSecretSyncOption = { @@ -11,7 +12,7 @@ export type TSecretSyncOption = { canImportSecrets: boolean; }; -export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync; +export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx index 241e673ca..947850e22 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -52,7 +52,7 @@ export const IntegrationsListPage = () => { />
- + Integrations Update

diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AwsSecretsManagerSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AwsSecretsManagerSyncDestinationCol.tsx new file mode 100644 index 000000000..9cfe9af2b --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AwsSecretsManagerSyncDestinationCol.tsx @@ -0,0 +1,43 @@ +import { + AwsSecretsManagerSyncMappingBehavior, + TAwsSecretsManagerSync +} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell, SecretSyncTableCellProps } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TAwsSecretsManagerSync; +}; + +export const AwsSecretsManagerSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + const { destinationConfig } = secretSync; + + let additionalProps: Pick< + SecretSyncTableCellProps, + "additionalTooltipContent" | "infoBadge" | "secondaryClassName" + > = {}; + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne) { + additionalProps = { + infoBadge: "secondary", + additionalTooltipContent: ( +

+ Secret Name: +

{destinationConfig.secretName}

+
+ ) + }; + } + + return ( + + ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 673e2aced..376258bfb 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -1,6 +1,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol"; +import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -12,6 +13,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { switch (secretSync.destination) { case SecretSync.AWSParameterStore: return ; + case SecretSync.AWSSecretsManager: + return ; case SecretSync.GitHub: return ; case SecretSync.GCPSecretManager: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx index 6f42c1f0e..50d36d606 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx @@ -30,7 +30,11 @@ export const SecretSyncTableCell = ({ content={ <>

{primaryText}

- {secondaryText &&

{secondaryText}

} + {secondaryText && ( +

+ {secondaryText} +

+ )} {additionalTooltipContent} } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 94147aa26..647770ee8 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -17,6 +17,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.path; secondaryText = destinationConfig.region; break; + case SecretSync.AWSSecretsManager: + primaryText = destinationConfig.region; + secondaryText = destinationConfig.mappingBehavior; + break; case SecretSync.GitHub: switch (destinationConfig.scope) { case GitHubSyncScope.Organization: diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection.tsx new file mode 100644 index 000000000..5b73a4492 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection.tsx @@ -0,0 +1,34 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { Badge } from "@app/components/v2"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; +import { + AwsSecretsManagerSyncMappingBehavior, + TAwsSecretsManagerSync +} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +type Props = { + secretSync: TAwsSecretsManagerSync; +}; + +export const AwsSecretsManagerSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + const awsRegion = AWS_REGIONS.find((r) => r.slug === destinationConfig.region); + + return ( + <> + + {awsRegion?.name} + + {awsRegion?.slug}{" "} + + + + {destinationConfig.mappingBehavior} + + {destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && ( + {destinationConfig.secretName} + )} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 4bfbd492a..686f3498c 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -10,6 +10,7 @@ import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissi import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection"; +import { AwsSecretsManagerSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection"; import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; @@ -29,6 +30,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.AWSParameterStore: DestinationComponents = ; break; + case SecretSync.AWSSecretsManager: + DestinationComponents = ; + break; case SecretSync.GitHub: DestinationComponents = ; break;