diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7e85c2031..5cac80c19 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1722,6 +1722,13 @@ export const SecretSyncs = { initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.` }; }, + ADDITIONAL_SYNC_OPTIONS: { + AWS_PARAMETER_STORE: { + 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.` + } + }, DESTINATION_CONFIG: { AWS_PARAMETER_STORE: { region: "The AWS region to sync secrets to.", diff --git a/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts index 87ed022a2..674e6e417 100644 --- a/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts @@ -1,13 +1,19 @@ -import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; import { CreateAwsConnectionSchema, SanitizedAwsConnectionSchema, UpdateAwsConnectionSchema } from "@app/services/app-connection/aws"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; -export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => +export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ app: AppConnection.AWS, server, @@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => createSchema: CreateAwsConnectionSchema, updateSchema: UpdateAwsConnectionSchema }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/kms-keys`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + region: z.nativeEnum(AWSRegion), + destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager]) + }), + response: { + 200: z.object({ + kmsKeys: z.object({ alias: z.string(), id: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const kmsKeys = await server.services.appConnection.aws.listKmsKeys( + { + connectionId, + ...req.query + }, + req.permission + ); + + return { kmsKeys }; + } + }); +}; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 7ae8b6377..8c4dd6a7c 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -22,18 +22,19 @@ import { TUpdateAppConnectionDTO, TValidateAppConnectionCredentials } from "@app/services/app-connection/app-connection-types"; -import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws"; -import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks"; -import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service"; -import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github"; -import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TAppConnectionDALFactory } from "./app-connection-dal"; +import { ValidateAwsConnectionCredentialsSchema } from "./aws"; +import { awsConnectionService } from "./aws/aws-connection-service"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; +import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; +import { databricksConnectionService } from "./databricks/databricks-connection-service"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; +import { ValidateGitHubConnectionCredentialsSchema } from "./github"; +import { githubConnectionService } from "./github/github-connection-service"; export type TAppConnectionServiceFactoryDep = { appConnectionDAL: TAppConnectionDALFactory; @@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({ listAvailableAppConnectionsForUser, github: githubConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById), - databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) + databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), + aws: awsConnectionService(connectAppConnectionById) }; }; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index f0640eea4..b9dceda2e 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -1,3 +1,4 @@ +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; import { TAwsConnection, TAwsConnectionConfig, @@ -16,6 +17,7 @@ import { TGitHubConnectionInput, TValidateGitHubConnectionCredentials } from "@app/services/app-connection/github"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { TAzureAppConfigurationConnection, @@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials = | TValidateAzureKeyVaultConnectionCredentials | TValidateAzureAppConfigurationConnectionCredentials | TValidateDatabricksConnectionCredentials; + +export type TListAwsConnectionKmsKeys = { + connectionId: string; + region: AWSRegion; + destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager; +}; diff --git a/backend/src/services/app-connection/aws/aws-connection-service.ts b/backend/src/services/app-connection/aws/aws-connection-service.ts new file mode 100644 index 000000000..d722d727e --- /dev/null +++ b/backend/src/services/app-connection/aws/aws-connection-service.ts @@ -0,0 +1,67 @@ +import AWS from "aws-sdk"; + +import { OrgServiceActor } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listAwsKmsKeys = async ( + appConnection: TAwsConnection, + { region, destination }: Pick +) => { + const { credentials } = await getAwsConnectionConfig(appConnection, region); + + const awsKms = new AWS.KMS({ + credentials, + region + }); + + const { Aliases = [] } = await awsKms.listAliases({ Limit: 100 }).promise(); + + const aliasEntries = Aliases.filter((aliasEntry) => { + if (!aliasEntry.TargetKeyId) return false; + + if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true; + + if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager") + return true; + + if (aliasEntry.AliasName?.includes("alias/aws/")) return false; + + return true; + }); + + const kmsKeys = aliasEntries.map((alias) => { + return { + id: alias.TargetKeyId!, + alias: alias.AliasName! + }; + }); + + return kmsKeys; +}; + +export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listKmsKeys = async ( + { connectionId, region, destination }: TListAwsConnectionKmsKeys, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor); + + const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination }); + + return kmsKeys; + }; + + return { + listKmsKeys + }; +}; diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index 8745d286e..31826b79d 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -1,8 +1,9 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; -import { Knex } from "knex"; export type TKmsRootConfigDALFactory = ReturnType; 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 6d7739afa..e74f6e451 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 @@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types"; type TAWSParameterStoreRecord = Record; +type TAWSParameterStoreMetadataRecord = Record; +type TAWSParameterStoreTagsRecord = Record>; const MAX_RETRIES = 5; const BATCH_SIZE = 10; @@ -80,6 +82,122 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise => { + const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const parameters = await ssm + .describeParameters({ + MaxResults: 10, + NextToken: nextToken, + ParameterFilters: [ + { + Key: "Path", + Option: "OneLevel", + Values: [path] + } + ] + }) + .promise(); + + attempt = 0; + + if (parameters.Parameters) { + parameters.Parameters.forEach((parameter) => { + if (parameter.Name) { + // no leading slash if path is '/' + const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name; + awsParameterStoreMetadataRecord[secKey] = parameter; + } + }); + } + + hasNext = Boolean(parameters.NextToken); + nextToken = parameters.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 awsParameterStoreMetadataRecord; +}; + +const getParameterStoreTagsRecord = async ( + ssm: AWS.SSM, + path: string, + awsParameterStoreSecretsRecord: TAWSParameterStoreRecord, + areTagsPresent: boolean +): Promise => { + const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {}; + + for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) { + const [key, parameter] = entry; + + if (!parameter.Name) { + // eslint-disable-next-line no-continue + continue; + } + + try { + const tags = await ssm + .listTagsForResource({ + ResourceType: "Parameter", + ResourceId: parameter.Name + }) + .promise(); + + awsParameterStoreTagsRecord[key] = Object.fromEntries(tags.TagList?.map((tag) => [tag.Key, tag.Value]) ?? []); + } catch (e) { + // users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized + // and they aren't trying to configure tags + if ((e as AWSError).code === "AccessDeniedException" && !areTagsPresent) { + return {}; + } + + throw e; + } + } + + return awsParameterStoreTagsRecord; +}; + +const processParameterTags = ({ + syncTagsRecord, + awsTagsRecord +}: { + syncTagsRecord: Record; + awsTagsRecord: Record; +}) => { + const tagsToAdd: AWS.SSM.TagList = []; + 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 }; +}; + const putParameter = async ( ssm: AWS.SSM, params: AWS.SSM.PutParameterRequest, @@ -98,6 +216,42 @@ const putParameter = async ( } }; +const addTagsToParameter = async ( + ssm: AWS.SSM, + params: Omit, + attempt = 0 +): Promise => { + try { + return await ssm.addTagsToResource({ ...params, ResourceType: "Parameter" }).promise(); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return addTagsToParameter(ssm, params, attempt + 1); + } + throw error; + } +}; + +const removeTagsFromParameter = async ( + ssm: AWS.SSM, + params: Omit, + attempt = 0 +): Promise => { + try { + return await ssm.removeTagsFromResource({ ...params, ResourceType: "Parameter" }).promise(); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return removeTagsFromParameter(ssm, params, attempt + 1); + } + throw error; + } +}; + const deleteParametersBatch = async ( ssm: AWS.SSM, parameters: AWS.SSM.Parameter[], @@ -132,34 +286,75 @@ const deleteParametersBatch = async ( export const AwsParameterStoreSyncFns = { syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig } = secretSync; + const { destinationConfig, syncOptions } = secretSync; const ssm = await getSSM(secretSync); - // TODO(scott): KMS Key ID, Tags - const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); - for await (const entry of Object.entries(secretMap)) { - const [key, { value }] = entry; + const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path); - // skip empty values (not allowed by AWS) or secrets that haven't changed - if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) { + const awsParameterStoreTagsRecord = await getParameterStoreTagsRecord( + ssm, + destinationConfig.path, + awsParameterStoreSecretsRecord, + Boolean(syncOptions.tags || syncOptions.syncSecretMetadataAsTags) + ); + + const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value, secretMetadata }] = entry; + + // skip empty values (not allowed by AWS) + if (!value) { // eslint-disable-next-line no-continue continue; } - try { - await putParameter(ssm, { - Name: `${destinationConfig.path}${key}`, - Type: "SecureString", - Value: value, - Overwrite: true + // create parameter or update if changed + if ( + !(key in awsParameterStoreSecretsRecord) || + value !== awsParameterStoreSecretsRecord[key].Value || + syncOptions.keyId !== awsParameterStoreMetadataRecord[key]?.KeyId + ) { + try { + await putParameter(ssm, { + Name: `${destinationConfig.path}${key}`, + Type: "SecureString", + Value: value, + Overwrite: true, + KeyId: syncOptions.keyId + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + const { tagsToAdd, tagKeysToRemove } = processParameterTags({ + syncTagsRecord: { + // configured sync tags take preference over secret metadata + ...(syncOptions.syncSecretMetadataAsTags && + Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), + ...syncTagsRecord + }, + awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {} + }); + + if (tagsToAdd.length) { + await addTagsToParameter(ssm, { + ResourceId: `${destinationConfig.path}${key}`, + Tags: tagsToAdd }); - } catch (error) { - throw new SecretSyncError({ - error, - secretKey: key + } + + if (tagKeysToRemove.length) { + await removeTagsFromParameter(ssm, { + ResourceId: `${destinationConfig.path}${key}`, + TagKeys: tagKeysToRemove }); } } 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 8b0765388..3ed07a678 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 @@ -8,6 +8,7 @@ import { GenericCreateSecretSyncFieldsSchema, GenericUpdateSecretSyncFieldsSchema } from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; const AwsParameterStoreSyncDestinationConfigSchema = z.object({ region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region), @@ -20,19 +21,65 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({ .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path) }); -export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({ +const AwsParameterStoreSyncOptionsSchema = 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_PARAMETER_STORE.keyId), + tags: z + .object({ + key: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Tag keys can only contain Unicode letters, digits, white space and any of the following: _." + ) + .min(1, "AWS tag key required") + .max(128, "AWS tag name cannot exceed 128 characters"), + value: z + .string() + .regex(/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, "Invalid AWS tag value") + .max(256, "Tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-") + }) + .array() + .max(50) + .refine((items) => new Set(items.map((item) => item.key)).size === items.length, { + message: "AWS tag keys must be unique" + }) + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags), + syncSecretMetadataAsTags: z + .boolean() + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags) +}); + +const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema( + SecretSync.AWSParameterStore, + AwsParameterStoreSyncOptionsConfig, + AwsParameterStoreSyncOptionsSchema +).extend({ destination: z.literal(SecretSync.AWSParameterStore), destinationConfig: AwsParameterStoreSyncDestinationConfigSchema }); export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema( - SecretSync.AWSParameterStore + SecretSync.AWSParameterStore, + AwsParameterStoreSyncOptionsConfig, + AwsParameterStoreSyncOptionsSchema ).extend({ destinationConfig: AwsParameterStoreSyncDestinationConfigSchema }); export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema( - SecretSync.AWSParameterStore + SecretSync.AWSParameterStore, + AwsParameterStoreSyncOptionsConfig, + AwsParameterStoreSyncOptionsSchema ).extend({ destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional() }); 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 6de014502..adcef458d 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 @@ -9,6 +9,7 @@ import { GenericCreateSecretSyncFieldsSchema, GenericUpdateSecretSyncFieldsSchema } from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; const AwsSecretsManagerSyncDestinationConfigSchema = z .discriminatedUnion("mappingBehavior", [ @@ -38,19 +39,26 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z }) ); -export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({ +const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema( + SecretSync.AWSSecretsManager, + AwsSecretsManagerSyncOptionsConfig +).extend({ destination: z.literal(SecretSync.AWSSecretsManager), destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema }); export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema( - SecretSync.AWSSecretsManager + SecretSync.AWSSecretsManager, + AwsSecretsManagerSyncOptionsConfig ).extend({ destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema }); export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema( - SecretSync.AWSSecretsManager + SecretSync.AWSSecretsManager, + AwsSecretsManagerSyncOptionsConfig ).extend({ destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() }); diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 808930b15..cdc9540da 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({ } secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + secretMap[secretKey].secretMetadata = secret.secretMetadata; }) ); @@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({ secretMap[importedSecret.key] = { skipMultilineEncoding: importedSecret.skipMultilineEncoding, comment: importedSecret.secretComment, - value: importedSecret.secretValue || "" + value: importedSecret.secretValue || "", + secretMetadata: importedSecret.secretMetadata }; } } diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 89bdc4375..8ea3a4f0b 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { AnyZodObject, z } from "zod"; import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs"; import { SecretSyncs } from "@app/lib/api-docs"; @@ -8,34 +8,45 @@ import { SecretSync, SecretSyncInitialSyncBehavior } from "@app/services/secret- import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; -const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) => - z.object({ - initialSyncBehavior: (options.canImportSecrets +const BaseSyncOptionsSchema = ({ + destination, + syncOptionsConfig: { canImportSecrets }, + merge, + isUpdateSchema +}: { + destination: SecretSync; + syncOptionsConfig: TSyncOptionsConfig; + merge?: T; + isUpdateSchema?: boolean; +}) => { + const baseSchema = z.object({ + initialSyncBehavior: (canImportSecrets ? z.nativeEnum(SecretSyncInitialSyncBehavior) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) - ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior) - // prependPrefix: z - // .string() - // .trim() - // .transform((str) => str.toUpperCase()) - // .optional() - // .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX), - // appendSuffix: z - // .string() - // .trim() - // .transform((str) => str.toUpperCase()) - // .optional() - // .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX) + ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior) }); -export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => + const schema = merge ? baseSchema.merge(merge) : baseSchema; + + return ( + isUpdateSchema + ? schema.describe(SecretSyncs.UPDATE(destination).syncOptions).optional() + : schema.describe(SecretSyncs.CREATE(destination).syncOptions) + ) as T extends AnyZodObject ? z.ZodObject> : typeof schema; +}; + +export const BaseSecretSyncSchema = ( + destination: SecretSync, + syncOptionsConfig: TSyncOptionsConfig, + merge?: T +) => SecretSyncsSchema.omit({ destination: true, destinationConfig: true, syncOptions: true }).extend({ // destination needs to be on the extended object for type differentiation - syncOptions: SyncOptionsSchema(destination, syncOptionsConfig), + syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }), // join properties projectId: z.string(), connection: z.object({ @@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig? folder: z.object({ id: z.string(), path: z.string() }).nullable() }); -export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => +export const GenericCreateSecretSyncFieldsSchema = ( + destination: SecretSync, + syncOptionsConfig: TSyncOptionsConfig, + merge?: T +) => z.object({ name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name), projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId), @@ -66,10 +81,14 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn .transform(removeTrailingSlash) .describe(SecretSyncs.CREATE(destination).secretPath), isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled), - syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions) + syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }) }); -export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => +export const GenericUpdateSecretSyncFieldsSchema = ( + destination: SecretSync, + syncOptionsConfig: TSyncOptionsConfig, + merge?: T +) => z.object({ name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(), connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(), @@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn .optional() .describe(SecretSyncs.UPDATE(destination).secretPath), isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled), - syncOptions: SyncOptionsSchema(destination, syncOptionsConfig) - .optional() - .describe(SecretSyncs.UPDATE(destination).syncOptions) + syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true }) }); diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 2c6d3a830..60742e797 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -2,6 +2,7 @@ import { Job } from "bullmq"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { QueueJobs } from "@app/queue"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { TAwsSecretsManagerSync, TAwsSecretsManagerSyncInput, @@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job< export type TSecretMap = Record< string, - { value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined } + { + value: string; + comment?: string; + skipMultilineEncoding?: boolean | null | undefined; + secretMetadata?: ResourceMetadataDTO; + } >; diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png index 11923e5a2..6a4a68f2c 100644 Binary files a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png differ diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index 27b2ff623..ece01374f 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -118,7 +118,9 @@ Infisical supports two methods for connecting to AWS. "ssm:GetParametersByPath", "ssm:DescribeParameters", "ssm:DeleteParameters", + "ssm:ListTagsForResource", // if you need to add tags to secrets "ssm:AddTagsToResource", // if you need to add tags to secrets + "ssm:RemoveTagsFromResource", // if you need to add tags to secrets "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 @@ -259,7 +261,9 @@ Infisical supports two methods for connecting to AWS. "ssm:GetParametersByPath", "ssm:DescribeParameters", "ssm:DeleteParameters", + "ssm:ListTagsForResource", // if you need to add tags to secrets "ssm:AddTagsToResource", // if you need to add tags to secrets + "ssm:RemoveTagsFromResource", // if you need to add tags to secrets "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 diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 4b41094b4..8a7d6394f 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,6 +40,10 @@ 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. + - **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. - **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/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx index e7721f113..efd7f412c 100644 --- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx @@ -16,10 +16,10 @@ import { useSecretSyncOption } from "@app/hooks/api/secretSyncs"; +import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields"; import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas"; import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields"; import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields"; -import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields"; import { SecretSyncReviewFields } from "./SecretSyncReviewFields"; import { SecretSyncSourceFields } from "./SecretSyncSourceFields"; @@ -32,7 +32,7 @@ type Props = { const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [ { name: "Source", key: "source", fields: ["secretPath", "environment"] }, { name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] }, - { name: "Options", key: "options", fields: ["syncOptions"] }, + { name: "Sync Options", key: "options", fields: ["syncOptions"] }, { name: "Details", key: "details", fields: ["name", "description"] }, { name: "Review", key: "review", fields: [] } ]; diff --git a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx index 6d15e8007..25d992735 100644 --- a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx @@ -8,10 +8,10 @@ import { Button, ModalClose } from "@app/components/v2"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs"; +import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields"; import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas"; import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields"; import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields"; -import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields"; import { SecretSyncSourceFields } from "./SecretSyncSourceFields"; type Props = { diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx index 09076ea9c..47975ca37 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx @@ -8,13 +8,17 @@ import { TSecretSyncForm } from "../schemas"; import { AwsRegionSelect } from "./shared"; export const AwsParameterStoreSyncFields = () => { - const { control } = useFormContext< + const { control, setValue } = useFormContext< TSecretSyncForm & { destination: SecretSync.AWSParameterStore } >(); return ( <> - + { + setValue("syncOptions.keyId", undefined); + }} + /> ( diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/AwsParameterStoreSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/AwsParameterStoreSyncOptionsFields.tsx new file mode 100644 index 000000000..4c440a60f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/AwsParameterStoreSyncOptionsFields.tsx @@ -0,0 +1,184 @@ +import { Fragment } from "react"; +import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FilterableSelect, + FormControl, + FormLabel, + IconButton, + Input, + Switch, + Tooltip +} from "@app/components/v2"; +import { + TAwsConnectionKmsKey, + useListAwsConnectionKmsKeys +} from "@app/hooks/api/appConnections/aws"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const AwsParameterStoreSyncOptionsFields = () => { + const { control, watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSParameterStore } + >(); + + const region = watch("destinationConfig.region"); + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys( + { + connectionId, + region, + destination: SecretSync.AWSParameterStore + }, + { enabled: Boolean(connectionId && region) } + ); + + const tagFields = useFieldArray({ + control, + name: "syncOptions.tags" + }); + + return ( + <> + ( + + org.alias === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.alias ?? null) + } + options={kmsKeys} + placeholder="Leave blank to use default KMS key" + getOptionLabel={(option) => + option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias + } + getOptionValue={(option) => option.alias} + /> + + )} + /> + +
+ {tagFields.fields.map(({ id: tagFieldId }, i) => ( + +
+ {i === 0 && Key} + ( + + + + )} + /> +
+
+ {i === 0 && ( + + )} + ( + + + + )} + /> +
+ + tagFields.remove(i)} + > + + + +
+ ))} +
+
+ +
+ ( + + +

+ 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 field above will take precedence over + secret metadata when tag keys conflict. +

+ + } + > + + +

+
+
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx similarity index 83% rename from frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields.tsx rename to frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 6bfef2f71..73a5256de 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -1,12 +1,14 @@ +import { ReactNode } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FormControl, Select, SelectItem } from "@app/components/v2"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; -import { useSecretSyncOption } from "@app/hooks/api/secretSyncs"; +import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; -import { TSecretSyncForm } from "./schemas"; +import { TSecretSyncForm } from "../schemas"; +import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields"; type Props = { hideInitialSync?: boolean; @@ -21,6 +23,24 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { const { syncOption } = useSecretSyncOption(destination); + let AdditionalSyncOptionsFieldsComponent: ReactNode; + + switch (destination) { + case SecretSync.AWSParameterStore: + AdditionalSyncOptionsFieldsComponent = ; + break; + case SecretSync.AWSSecretsManager: + case SecretSync.GitHub: + case SecretSync.GCPSecretManager: + case SecretSync.AzureKeyVault: + case SecretSync.AzureAppConfiguration: + case SecretSync.Databricks: + AdditionalSyncOptionsFieldsComponent = null; + break; + default: + throw new Error(`Unhandled Additional Sync Options Fields: ${destination}`); + } + return ( <>

Configure how secrets should be synced.

@@ -91,6 +111,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { )} )} + {AdditionalSyncOptionsFieldsComponent} {/* ( { +export const AwsParameterStoreSyncOptionsReviewFields = () => { const { watch } = useFormContext< TSecretSyncForm & { destination: SecretSync.AWSParameterStore } >(); - const [region, path] = watch(["destinationConfig.region", "destinationConfig.path"]); + const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]); + + return ( + <> + {keyId && {keyId}} + {tags?.length && ( + + + + Key + Value + + + {tags.map((tag) => ( + + {tag.key} + {tag.value} + + ))} + + + } + > +
+ + + + {tags.length} Tag{tags.length > 1 ? "s" : ""} + + +
+
+
+ )} + {syncSecretMetadataAsTags && ( + + Enabled + + )} + + ); +}; + +export const AwsParameterStoreDestinationReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSParameterStore } + >(); + + const [{ region, path }] = watch(["destinationConfig"]); const awsRegion = AWS_REGIONS.find((r) => r.slug === region); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 61408cf93..167fc830a 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -3,15 +3,18 @@ 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 { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields"; 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"; -import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields"; +import { + AwsParameterStoreDestinationReviewFields, + AwsParameterStoreSyncOptionsReviewFields +} from "./AwsParameterStoreSyncReviewFields"; +import { AwsSecretsManagerSyncReviewFields } from "./AwsSecretsManagerSyncReviewFields"; import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; +import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; @@ -19,6 +22,7 @@ export const SecretSyncReviewFields = () => { const { watch } = useFormContext(); let DestinationFieldsComponent: ReactNode; + let AdditionalSyncOptionsFieldsComponent: ReactNode; const { name, @@ -38,7 +42,8 @@ export const SecretSyncReviewFields = () => { switch (destination) { case SecretSync.AWSParameterStore: - DestinationFieldsComponent = ; + DestinationFieldsComponent = ; + AdditionalSyncOptionsFieldsComponent = ; break; case SecretSync.AWSSecretsManager: DestinationFieldsComponent = ; @@ -84,7 +89,7 @@ export const SecretSyncReviewFields = () => {
- Options + Sync Options
@@ -97,6 +102,7 @@ export const SecretSyncReviewFields = () => { {/* {prependPrefix} {appendSuffix} */} + {AdditionalSyncOptionsFieldsComponent}
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 68abb7793..8c89baddd 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 @@ -1,16 +1,45 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -export const AwsParameterStoreSyncDestinationSchema = z.object({ - destination: z.literal(SecretSync.AWSParameterStore), - destinationConfig: z.object({ - path: z - .string() - .trim() - .min(1, "Parameter Store Path required") - .max(2048, "Cannot exceed 2048 characters") - .regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'), - region: z.string().min(1, "Region required") +export const AwsParameterStoreSyncDestinationSchema = 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, "AWS tag name 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.AWSParameterStore), + destinationConfig: z.object({ + path: z + .string() + .trim() + .min(1, "Parameter Store Path required") + .max(2048, "Cannot exceed 2048 characters") + .regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'), + region: z.string().min(1, "Region required") + }) + }) +); 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 b1f43e737..e44dfd93a 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 @@ -1,30 +1,33 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; 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") - }) - ) -}); +export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema().merge( + 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/azure-app-configuration-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts index d472ddb04..5fb9d2895 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts @@ -1,19 +1,22 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -export const AzureAppConfigurationSyncDestinationSchema = z.object({ - destination: z.literal(SecretSync.AzureAppConfiguration), - destinationConfig: z.object({ - configurationUrl: z - .string() - .trim() - .min(1, { message: "Azure App Configuration URL is required" }) - .url() - .refine( - (val) => val.endsWith(".azconfig.io"), - "URL should have the following format: https://resource-name-here.azconfig.io" - ), - label: z.string().optional() +export const AzureAppConfigurationSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.AzureAppConfiguration), + destinationConfig: z.object({ + configurationUrl: z + .string() + .trim() + .min(1, { message: "Azure App Configuration URL is required" }) + .url() + .refine( + (val) => val.endsWith(".azconfig.io"), + "URL should have the following format: https://resource-name-here.azconfig.io" + ), + label: z.string().optional() + }) }) -}); +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts index 11fa6279b..30ea3b300 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts @@ -1,10 +1,16 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -export const AzureKeyVaultSyncDestinationSchema = z.object({ - destination: z.literal(SecretSync.AzureKeyVault), - destinationConfig: z.object({ - vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required") +export const AzureKeyVaultSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.AzureKeyVault), + destinationConfig: z.object({ + vaultBaseUrl: z + .string() + .url("Invalid vault base URL format") + .min(1, "Vault base URL required") + }) }) -}); +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts new file mode 100644 index 000000000..b898fe91f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts @@ -0,0 +1,39 @@ +import { AnyZodObject, z } from "zod"; + +import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; +import { slugSchema } from "@app/lib/schemas"; + +export const BaseSecretSyncSchema = ( + additionalSyncOptions?: T +) => { + const baseSyncOptionsSchema = z.object({ + initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior) + // scott: removed temporarily for evaluation of template formatting + // prependPrefix: z + // .string() + // .trim() + // .transform((str) => str.toUpperCase()) + // .optional(), + // appendSuffix: z + // .string() + // .trim() + // .transform((str) => str.toUpperCase()) + // .optional() + }); + + const syncOptionsSchema = additionalSyncOptions + ? baseSyncOptionsSchema.merge(additionalSyncOptions) + : (baseSyncOptionsSchema as T extends AnyZodObject + ? z.ZodObject> + : typeof baseSyncOptionsSchema); + + return z.object({ + name: slugSchema({ field: "Name" }), + description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(), + connection: z.object({ name: z.string(), id: z.string().uuid() }), + environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }), + secretPath: z.string().min(1, "Secret path required"), + syncOptions: syncOptionsSchema, + isAutoSyncEnabled: z.boolean() + }); +}; diff --git a/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts index 0a8f5e723..4319f616d 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts @@ -1,10 +1,13 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -export const DatabricksSyncDestinationSchema = z.object({ - destination: z.literal(SecretSync.Databricks), - destinationConfig: z.object({ - scope: z.string().trim().min(1, "Databricks scope required") +export const DatabricksSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Databricks), + destinationConfig: z.object({ + scope: z.string().trim().min(1, "Databricks scope required") + }) }) -}); +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts index 6ffa3ca86..4225c6619 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts @@ -1,12 +1,15 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; -export const GcpSyncDestinationSchema = z.object({ - destination: z.literal(SecretSync.GCPSecretManager), - destinationConfig: z.object({ - scope: z.literal(GcpSyncScope.Global), - projectId: z.string().min(1, "Project ID required") +export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.GCPSecretManager), + destinationConfig: z.object({ + scope: z.literal(GcpSyncScope.Global), + projectId: z.string().min(1, "Project ID required") + }) }) -}); +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/github-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/github-sync-destination-schema.ts index ccebc946c..54a204528 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/github-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/github-sync-destination-schema.ts @@ -1,45 +1,48 @@ import { z } from "zod"; +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/hooks/api/secretSyncs/types/github-sync"; -export const GitHubSyncDestinationSchema = z.object({ - destination: z.literal(SecretSync.GitHub), - destinationConfig: z - .discriminatedUnion("scope", [ - z.object({ - scope: z.literal(GitHubSyncScope.Organization), - org: z.string().min(1, "Organization name required"), - visibility: z.nativeEnum(GitHubSyncVisibility), - selectedRepositoryIds: z.number().array().optional() - }), - z.object({ - scope: z.literal(GitHubSyncScope.Repository), - owner: z.string().min(1, "Repository owner name required"), - repo: z.string().min(1, "Repository name required") - }), - z.object({ - scope: z.literal(GitHubSyncScope.RepositoryEnvironment), - owner: z.string().min(1, "Repository owner name required"), - repo: z.string().min(1, "Repository name required"), - env: z.string().min(1, "Environment name required") - }) - ]) - .superRefine((options, ctx) => { - if (options.scope === GitHubSyncScope.Organization) { - if ( - options.visibility === GitHubSyncVisibility.Selected && - !options.selectedRepositoryIds?.length - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Select at least 1 repository", - path: ["selectedRepositoryIds"] - }); +export const GitHubSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.GitHub), + destinationConfig: z + .discriminatedUnion("scope", [ + z.object({ + scope: z.literal(GitHubSyncScope.Organization), + org: z.string().min(1, "Organization name required"), + visibility: z.nativeEnum(GitHubSyncVisibility), + selectedRepositoryIds: z.number().array().optional() + }), + z.object({ + scope: z.literal(GitHubSyncScope.Repository), + owner: z.string().min(1, "Repository owner name required"), + repo: z.string().min(1, "Repository name required") + }), + z.object({ + scope: z.literal(GitHubSyncScope.RepositoryEnvironment), + owner: z.string().min(1, "Repository owner name required"), + repo: z.string().min(1, "Repository name required"), + env: z.string().min(1, "Environment name required") + }) + ]) + .superRefine((options, ctx) => { + if (options.scope === GitHubSyncScope.Organization) { + if ( + options.visibility === GitHubSyncVisibility.Selected && + !options.selectedRepositoryIds?.length + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Select at least 1 repository", + path: ["selectedRepositoryIds"] + }); + } } - } - }) -}); + }) + }) +); 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 f7d11dba4..55ee6cb3d 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 @@ -3,37 +3,12 @@ import { z } from "zod"; import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-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"; import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema"; import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; -const BaseSecretSyncSchema = z.object({ - name: slugSchema({ field: "Name" }), - description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(), - connection: z.object({ name: z.string(), id: z.string().uuid() }), - environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }), - secretPath: z.string().min(1, "Secret path required"), - syncOptions: z.object({ - initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior) - // scott: removed temporarily for evaluation of template formatting - // prependPrefix: z - // .string() - // .trim() - // .transform((str) => str.toUpperCase()) - // .optional(), - // appendSuffix: z - // .string() - // .trim() - // .transform((str) => str.toUpperCase()) - // .optional() - }), - isAutoSyncEnabled: z.boolean() -}); - const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, AwsSecretsManagerSyncDestinationSchema, @@ -44,8 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ DatabricksSyncDestinationSchema ]); -export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema); +export const SecretSyncFormSchema = SecretSyncUnionSchema; -export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema.partial()); +export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema; export type TSecretSyncForm = z.infer; diff --git a/frontend/src/hooks/api/appConnections/aws/index.ts b/frontend/src/hooks/api/appConnections/aws/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/aws/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/aws/queries.tsx b/frontend/src/hooks/api/appConnections/aws/queries.tsx new file mode 100644 index 000000000..87965507b --- /dev/null +++ b/frontend/src/hooks/api/appConnections/aws/queries.tsx @@ -0,0 +1,42 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { + TAwsConnectionKmsKey, + TAwsConnectionListKmsKeysResponse, + TListAwsConnectionKmsKeys +} from "./types"; + +const awsConnectionKeys = { + all: [...appConnectionKeys.all, "aws"] as const, + listKmsKeys: (params: TListAwsConnectionKmsKeys) => + [...awsConnectionKeys.all, "kms-keys", params] as const +}; + +export const useListAwsConnectionKmsKeys = ( + { connectionId, ...params }: TListAwsConnectionKmsKeys, + options?: Omit< + UseQueryOptions< + TAwsConnectionKmsKey[], + unknown, + TAwsConnectionKmsKey[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: awsConnectionKeys.listKmsKeys({ connectionId, ...params }), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/aws/${connectionId}/kms-keys`, + { params } + ); + + return data.kmsKeys; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/aws/types.ts b/frontend/src/hooks/api/appConnections/aws/types.ts new file mode 100644 index 000000000..b49111455 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/aws/types.ts @@ -0,0 +1,24 @@ +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export type TDatabricksSecretScope = { + name: string; +}; + +export type TDatabricksConnectionListSecretScopesResponse = { + secretScopes: TDatabricksSecretScope[]; +}; + +export type TListAwsConnectionKmsKeys = { + connectionId: string; + region: string; + destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager; +}; + +export type TAwsConnectionKmsKey = { + alias: string; + id: string; +}; + +export type TAwsConnectionListKmsKeysResponse = { + kmsKeys: TAwsConnectionKmsKey[]; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/aws-parameter-store-sync.ts b/frontend/src/hooks/api/secretSyncs/types/aws-parameter-store-sync.ts index 26c7aed68..32217cf47 100644 --- a/frontend/src/hooks/api/secretSyncs/types/aws-parameter-store-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/aws-parameter-store-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 TAwsParameterStoreSync = TRootSecretSync & { destination: SecretSync.AWSParameterStore; @@ -13,4 +13,9 @@ export type TAwsParameterStoreSync = TRootSecretSync & { name: string; id: string; }; + syncOptions: RootSyncOptions & { + keyId?: string; + tags?: { key: string; value?: string }[]; + syncSecretMetadataAsTags?: boolean; + }; }; diff --git a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts index 947b58e4a..49cf9c978 100644 --- a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts @@ -1,6 +1,12 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs"; +export type RootSyncOptions = { + initialSyncBehavior: SecretSyncInitialSyncBehavior; + // prependPrefix?: string; + // appendSuffix?: string; +}; + export type TRootSecretSync = { id: string; name: string; @@ -24,11 +30,7 @@ export type TRootSecretSync = { lastRemoveJobId: string | null; lastRemovedAt: Date | null; lastRemoveMessage: string | null; - syncOptions: { - initialSyncBehavior: SecretSyncInitialSyncBehavior; - // prependPrefix?: string; - // appendSuffix?: string; - }; + syncOptions: RootSyncOptions; connection: { app: AppConnection; id: string; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx index db3787b49..8d65871cf 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx @@ -66,7 +66,7 @@ const PageContent = () => { const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source); - // const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options); + const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options); const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination); @@ -108,10 +108,7 @@ const PageContent = () => {
- +
{ - const { - destination, - syncOptions: { - // appendSuffix, - // prependPrefix, - initialSyncBehavior - } - } = secretSync; - - return ( -
-
-
-

Sync Options

- {/* - {(isAllowed) => ( - - - - )} - */} -
-
-
- - {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} - - {/* {prependPrefix} - {appendSuffix} */} -
-
-
-
- ); -}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/AwsParameterStoreSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/AwsParameterStoreSyncOptionsSection.tsx new file mode 100644 index 000000000..12d91d3dd --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/AwsParameterStoreSyncOptionsSection.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 { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync"; + +type Props = { + secretSync: TAwsParameterStoreSync; +}; + +export const AwsParameterStoreSyncOptionsSection = ({ secretSync }: Props) => { + const { + syncOptions: { keyId, tags, syncSecretMetadataAsTags } + } = secretSync; + + return ( + <> + {keyId && {keyId}} + {tags?.length && ( + + + + 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 new file mode 100644 index 000000000..c0a4bdc3f --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -0,0 +1,87 @@ +import { ReactNode } from "react"; +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { IconButton } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; +import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs"; +import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; + +import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection"; + +type Props = { + secretSync: TSecretSync; + onEditOptions: VoidFunction; +}; + +export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => { + const { + destination, + syncOptions: { + // appendSuffix, + // prependPrefix, + initialSyncBehavior + } + } = secretSync; + + let AdditionalSyncOptionsComponent: ReactNode; + + switch (destination) { + case SecretSync.AWSParameterStore: + AdditionalSyncOptionsComponent = ( + + ); + break; + case SecretSync.AWSSecretsManager: + case SecretSync.GitHub: + case SecretSync.GCPSecretManager: + case SecretSync.AzureKeyVault: + case SecretSync.AzureAppConfiguration: + case SecretSync.Databricks: + AdditionalSyncOptionsComponent = null; + break; + default: + throw new Error(`Unhandled Destination Review Fields: ${destination}`); + } + + return ( +
+
+
+

Sync Options

+ {AdditionalSyncOptionsComponent && ( + + {(isAllowed) => ( + + + + )} + + )} +
+
+
+ + {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} + + {/* {prependPrefix} + {appendSuffix} */} + {AdditionalSyncOptionsComponent} +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/index.ts b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/index.ts new file mode 100644 index 000000000..51e35c596 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/index.ts @@ -0,0 +1 @@ +export * from "./SecretSyncOptionsSection";