From 99ca9e04f8494749ad6d587db9a70ee8b19bcf0e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Jan 2025 20:21:29 -0800 Subject: [PATCH] improvements: final adjustments/improvements --- backend/src/lib/api-docs/constants.ts | 3 + .../secret-sync-endpoints.ts | 1 + .../aws-parameter-store-sync-fns.ts | 10 +- .../secret-sync/github/github-sync-fns.ts | 56 ++++++++--- .../secret-sync/secret-sync-errors.ts | 15 ++- .../services/secret-sync/secret-sync-fns.ts | 95 ++++++++++--------- .../services/secret-sync/secret-sync-queue.ts | 46 ++++++--- .../secret-sync/secret-sync-schemas.ts | 27 +++--- .../secret-sync/secret-sync-service.ts | 8 ++ .../services/secret-sync/secret-sync-types.ts | 2 +- .../secret-syncs/aws-parameter-store.mdx | 8 +- docs/integrations/secret-syncs/github.mdx | 12 +-- .../forms/CreateSecretSyncForm.tsx | 32 ++++++- .../secret-syncs/forms/EditSecretSyncForm.tsx | 5 +- .../forms/SecretSyncOptionsFields.tsx | 6 +- .../SecretSyncReviewFields.tsx | 9 +- .../forms/schemas/secret-sync-schema.ts | 23 ++--- .../src/hooks/api/secretSyncs/types/index.ts | 2 +- .../hooks/api/secretSyncs/types/root-sync.ts | 4 +- .../SecretSyncsTab/SecretSyncsTab.tsx | 2 +- .../SecretSyncDetailsByIDPage.tsx | 7 +- .../components/SecretSyncOptionsSection.tsx | 37 ++++---- 22 files changed, 255 insertions(+), 155 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index c9145304e..65c33bda3 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1679,6 +1679,9 @@ export const SecretSyncs = { const destinationName = SECRET_SYNC_NAME_MAP[destination]; return { syncId: `The ID of the ${destinationName} Sync to be updated.`, + connectionId: `The updated ID of the ${ + APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]] + } Connection to use for syncing.`, name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`, environment: `The updated slug of the project environment to sync secrets from.`, secretPath: `The updated folder path to sync secrets from.`, diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts index fabe11aaf..a1ca27921 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -31,6 +31,7 @@ export const registerSyncSecretsEndpoints = ; updateSchema: z.ZodType<{ + connectionId?: string; name?: string; environment?: string; secretPath?: string; 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 a2ae0be18..a495f9e83 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 @@ -129,7 +129,7 @@ const deleteParametersBatch = async ( }; export const AwsParameterStoreSyncFns = { - syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, affixedSecretMap: TSecretMap) => { + syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { const { destinationConfig } = secretSync; const ssm = await getSSM(secretSync); @@ -138,7 +138,7 @@ export const AwsParameterStoreSyncFns = { const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); - for await (const entry of Object.entries(affixedSecretMap)) { + for await (const entry of Object.entries(secretMap)) { const [key, { value }] = entry; // skip empty values (not allowed by AWS) or secrets that haven't changed @@ -167,7 +167,7 @@ export const AwsParameterStoreSyncFns = { for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { const [key, parameter] = entry; - if (!(key in affixedSecretMap) || !affixedSecretMap[key].value) { + if (!(key in secretMap) || !secretMap[key].value) { parametersToDelete.push(parameter); } } @@ -185,7 +185,7 @@ export const AwsParameterStoreSyncFns = { Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }]) ); }, - removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, affixedSecretMap: TSecretMap) => { + removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { const { destinationConfig } = secretSync; const ssm = await getSSM(secretSync); @@ -197,7 +197,7 @@ export const AwsParameterStoreSyncFns = { for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { const [key, param] = entry; - if (key in affixedSecretMap) { + if (key in secretMap) { parametersToDelete.push(param); } } diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 02f408400..a09a41163 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers"; import { getGitHubClient } from "@app/services/app-connection/github"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; @@ -158,7 +159,33 @@ const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials }; export const GithubSyncFns = { - syncSecrets: async (secretSync: TGitHubSyncWithCredentials, affixedSecretMap: TSecretMap) => { + syncSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => { + switch (secretSync.destinationConfig.scope) { + case GitHubSyncScope.Organization: + if (Object.values(secretMap).length > 1000) { + throw new SecretSyncError({ + message: "GitHub does not support storing more than 1,000 secrets at the organization level.", + shouldRetry: false + }); + } + break; + case GitHubSyncScope.Repository: + case GitHubSyncScope.RepositoryEnvironment: + if (Object.values(secretMap).length > 100) { + throw new SecretSyncError({ + message: "GitHub does not support storing more than 100 secrets at the repository level.", + shouldRetry: false + }); + } + break; + default: + throw new Error( + `Unsupported GitHub Sync scope ${ + (secretSync.destinationConfig as TGitHubSyncWithCredentials["destinationConfig"]).scope + }` + ); + } + const client = getGitHubClient(secretSync.connection); const encryptedSecrets = await getEncryptedSecrets(client, secretSync); @@ -166,16 +193,16 @@ export const GithubSyncFns = { const publicKey = await getPublicKey(client, secretSync); for await (const encryptedSecret of encryptedSecrets) { - if (!(encryptedSecret.name in affixedSecretMap)) { + if (!(encryptedSecret.name in secretMap)) { await deleteSecret(client, secretSync, encryptedSecret); } } await sodium.ready.then(async () => { - for await (const key of Object.keys(affixedSecretMap)) { + for await (const key of Object.keys(secretMap)) { // convert secret & base64 key to Uint8Array. const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL); - const binarySecretValue = sodium.from_string(affixedSecretMap[key].value); + const binarySecretValue = sodium.from_string(secretMap[key].value); // encrypt secret using libsodium const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); @@ -183,24 +210,31 @@ export const GithubSyncFns = { // convert encrypted Uint8Array to base64 const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); - await putSecret(client, secretSync, { - secret_name: key, - encrypted_value: encryptedSecretValue, - key_id: publicKey.key_id - }); + try { + await putSecret(client, secretSync, { + secret_name: key, + encrypted_value: encryptedSecretValue, + key_id: publicKey.key_id + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } } }); }, getSecrets: async (secretSync: TGitHubSyncWithCredentials) => { throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); }, - removeSecrets: async (secretSync: TGitHubSyncWithCredentials, affixedSecretMap: TSecretMap) => { + removeSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => { const client = getGitHubClient(secretSync.connection); const encryptedSecrets = await getEncryptedSecrets(client, secretSync); for await (const encryptedSecret of encryptedSecrets) { - if (encryptedSecret.name in affixedSecretMap) { + if (encryptedSecret.name in secretMap) { await deleteSecret(client, secretSync, encryptedSecret); } } diff --git a/backend/src/services/secret-sync/secret-sync-errors.ts b/backend/src/services/secret-sync/secret-sync-errors.ts index 3b8f77d13..859fbb00d 100644 --- a/backend/src/services/secret-sync/secret-sync-errors.ts +++ b/backend/src/services/secret-sync/secret-sync-errors.ts @@ -1,14 +1,23 @@ export class SecretSyncError extends Error { name: string; - error: unknown; + error?: unknown; secretKey?: string; - constructor({ name, error, secretKey }: { name?: string; error?: unknown; secretKey?: string } = {}) { - super(); + shouldRetry?: boolean; + + constructor({ + name, + error, + secretKey, + message, + shouldRetry = true + }: { name?: string; error?: unknown; secretKey?: string; shouldRetry?: boolean; message?: string } = {}) { + super(message); this.name = name || "SecretSyncError"; this.error = error; this.secretKey = secretKey; + this.shouldRetry = shouldRetry; } } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index eec022c7a..90984da17 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -22,55 +22,55 @@ export const listSecretSyncOptions = () => { return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); }; -const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { - let secretMap = { ...unprocessedSecretMap }; - - const { appendSuffix, prependPrefix } = secretSync.syncOptions; - - if (appendSuffix || prependPrefix) { - secretMap = {}; - Object.entries(unprocessedSecretMap).forEach(([key, value]) => { - secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value; - }); - } - - return secretMap; -}; - -const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { - let secretMap = { ...unprocessedSecretMap }; - - const { appendSuffix, prependPrefix } = secretSync.syncOptions; - - if (appendSuffix || prependPrefix) { - secretMap = {}; - Object.entries(unprocessedSecretMap).forEach(([key, value]) => { - let processedKey = key; - - if (prependPrefix && processedKey.startsWith(prependPrefix)) { - processedKey = processedKey.slice(prependPrefix.length); - } - - if (appendSuffix && processedKey.endsWith(appendSuffix)) { - processedKey = processedKey.slice(0, -appendSuffix.length); - } - - secretMap[processedKey] = value; - }); - } - - return secretMap; -}; +// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { +// let secretMap = { ...unprocessedSecretMap }; +// +// const { appendSuffix, prependPrefix } = secretSync.syncOptions; +// +// if (appendSuffix || prependPrefix) { +// secretMap = {}; +// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { +// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value; +// }); +// } +// +// return secretMap; +// }; +// +// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { +// let secretMap = { ...unprocessedSecretMap }; +// +// const { appendSuffix, prependPrefix } = secretSync.syncOptions; +// +// if (appendSuffix || prependPrefix) { +// secretMap = {}; +// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { +// let processedKey = key; +// +// if (prependPrefix && processedKey.startsWith(prependPrefix)) { +// processedKey = processedKey.slice(prependPrefix.length); +// } +// +// if (appendSuffix && processedKey.endsWith(appendSuffix)) { +// processedKey = processedKey.slice(0, -appendSuffix.length); +// } +// +// secretMap[processedKey] = value; +// }); +// } +// +// return secretMap; +// }; export const SecretSyncFns = { syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { - const affixedSecretMap = addAffixes(secretSync, secretMap); + // const affixedSecretMap = addAffixes(secretSync, secretMap); switch (secretSync.destination) { case SecretSync.AWSParameterStore: - return AwsParameterStoreSyncFns.syncSecrets(secretSync, affixedSecretMap); + return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GitHub: - return GithubSyncFns.syncSecrets(secretSync, affixedSecretMap); + return GithubSyncFns.syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for push secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -92,16 +92,17 @@ export const SecretSyncFns = { ); } - return stripAffixes(secretSync, secretMap); + return secretMap; + // return stripAffixes(secretSync, secretMap); }, removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { - const affixedSecretMap = addAffixes(secretSync, secretMap); + // const affixedSecretMap = addAffixes(secretSync, secretMap); switch (secretSync.destination) { case SecretSync.AWSParameterStore: - return AwsParameterStoreSyncFns.removeSecrets(secretSync, affixedSecretMap); + return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GitHub: - return GithubSyncFns.removeSecrets(secretSync, affixedSecretMap); + return GithubSyncFns.removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for removing secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -114,7 +115,7 @@ export const parseSyncErrorMessage = (err: unknown): string => { if (err instanceof SecretSyncError) { return JSON.stringify({ secretKey: err.secretKey, - error: parseSyncErrorMessage(err.error) + error: err.message ?? parseSyncErrorMessage(err.error) }); } diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 31856b671..7b1c5643a 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -31,6 +31,7 @@ import { SecretSyncImportBehavior, SecretSyncInitialSyncBehavior } from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { @@ -94,6 +95,19 @@ type SecretSyncActionJob = Job< TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO >; +const getRequeueDelay = (failureCount?: number) => { + if (!failureCount) return 0; + + const baseDelay = 1000; + const maxDelay = 30000; + + const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay); + + const jitter = delay * (0.5 + Math.random() * 0.5); + + return jitter; +}; + export const secretSyncQueueFactory = ({ queueService, kmsService, @@ -164,13 +178,18 @@ export const secretSyncQueueFactory = ({ resourceMetadataDAL }); - const $getSecrets = async (secretSync: TSecretSyncRaw | TSecretSyncWithCredentials, includeImports = true) => { + const $getInfisicalSecrets = async ( + secretSync: TSecretSyncRaw | TSecretSyncWithCredentials, + includeImports = true + ) => { const { projectId, folderId, environment, folder } = secretSync; if (!folderId || !environment || !folder) - throw new Error( - "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path." - ); + throw new SecretSyncError({ + message: + "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.", + shouldRetry: false + }); const secretMap: TSecretMap = {}; @@ -247,7 +266,7 @@ export const secretSyncQueueFactory = ({ const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) => queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, { - delay: payload.failedToAcquireLockCount ? 1000 : 0, // we don't want to delay initial job + delay: getRequeueDelay(payload.failedToAcquireLockCount), // this is for delaying re-queued jobs if sync is locked attempts: 5, backoff: { type: "exponential", @@ -309,7 +328,7 @@ export const secretSyncQueueFactory = ({ const importedSecretMap: TSecretMap = {}; - const secretMap = await $getSecrets(secretSync, false); + const secretMap = await $getInfisicalSecrets(secretSync, false); const secretsToCreate: Parameters[0]["secrets"] = []; const secretsToUpdate: Parameters[0]["secrets"] = []; @@ -374,7 +393,7 @@ export const secretSyncQueueFactory = ({ let isSynced = false; let syncMessage: string | null = null; - const isFinalAttempt = job.attemptsStarted === job.opts.attempts; + let isFinalAttempt = job.attemptsStarted === job.opts.attempts; try { const { @@ -400,7 +419,7 @@ export const secretSyncQueueFactory = ({ syncOptions: { initialSyncBehavior } } = secretSyncWithCredentials; - const secretMap = await $getSecrets(secretSync); + const secretMap = await $getInfisicalSecrets(secretSync); if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) { const importedSecretMap = await $importSecrets( @@ -438,8 +457,12 @@ export const secretSyncQueueFactory = ({ syncMessage = parseSyncErrorMessage(err); - // re-throw so job fails - throw err; + if (err instanceof SecretSyncError && !err.shouldRetry) { + isFinalAttempt = true; + } else { + // re-throw so job fails + throw err; + } } finally { const ranAt = new Date(); const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed; @@ -639,7 +662,7 @@ export const secretSyncQueueFactory = ({ kmsService }); - const secretMap = await $getSecrets(secretSync); + const secretMap = await $getInfisicalSecrets(secretSync); await SecretSyncFns.removeSecrets( { @@ -816,7 +839,6 @@ export const secretSyncQueueFactory = ({ case QueueJobs.SecretSyncSyncSecrets: { const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO; - // if (failedToAcquireLockCount < 10) { await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 }); return; diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 5b166df0c..92dd17422 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -13,19 +13,19 @@ const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = initialSyncBehavior: (options.canImportSecrets ? z.nativeEnum(SecretSyncInitialSyncBehavior) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) - ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR), - 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(secretSync).INITIAL_SYNC_BEHAVIOR) + // 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) }); export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => @@ -72,6 +72,7 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => z.object({ name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(), + connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(), description: z .string() .trim() diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 9bf4ba494..40af37e79 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -13,6 +13,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncStatus, @@ -262,6 +263,13 @@ export const secretSyncServiceFactory = ({ const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => { let { folderId } = secretSync; + if (params.connectionId) { + const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync]; + + // validates permission to connect and app is valid for sync destination + await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor); + } + if ( (secretPath && secretPath !== secretSync.folder?.path) || (environment && environment !== secretSync.environment?.slug) diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 035d1651e..b46ededc3 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -54,7 +54,7 @@ export type TCreateSecretSyncDTO = Pick> & { +export type TUpdateSecretSyncDTO = Partial> & { syncId: string; destination: SecretSync; }; diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 699578bce..504b386b0 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,8 +40,6 @@ 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 prior to syncing, prioritizing values present in Infisical if secrets conflict. - **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict. - - **Prepend Prefix**: An optional prefix to prepend to secret keys when synced. - - **Append Suffix**: An optional suffix to append to secret keys when synced. - **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled. 6. Configure the **Details** of your Parameter Store Sync, then click **Next**. @@ -76,8 +74,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." "secretPath": "/my-secrets", "isEnabled": true, "syncOptions": { - "initialSyncBehavior": "overwrite-destination", - "prependPrefix": "INF_", + "initialSyncBehavior": "overwrite-destination" }, "destinationConfig": { "region": "us-east-1", @@ -113,8 +110,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." "lastRemoveMessage": null, "lastRemovedAt": null, "syncOptions": { - "initialSyncBehavior": "overwrite-destination", - "prependPrefix": "INF_" + "initialSyncBehavior": "overwrite-destination" }, "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "connection": { diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 62c9a85c3..714d0a2aa 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -62,8 +62,6 @@ description: "Learn how to configure a GitHub Sync for Infisical." GitHub does not support importing secrets. - - **Prepend Prefix**: An optional prefix to prepend to secret keys when synced. - - **Append Suffix**: An optional suffix to append to secret keys when synced. - **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled. 6. Configure the **Details** of your GitHub Sync, then click **Next**. @@ -97,13 +95,12 @@ description: "Learn how to configure a GitHub Sync for Infisical." "secretPath": "/my-secrets", "isEnabled": true, "syncOptions": { - "initialSyncBehavior": "overwrite-destination", - "prependPrefix": "INF_", + "initialSyncBehavior": "overwrite-destination" }, "destinationConfig": { "scope": "repository", "owner": "my-github", - "repo: "my-repository" + "repo": "my-repository" } }' ``` @@ -135,8 +132,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." "lastRemoveMessage": null, "lastRemovedAt": null, "syncOptions": { - "initialSyncBehavior": "overwrite-destination", - "prependPrefix": "INF_" + "initialSyncBehavior": "overwrite-destination" }, "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "connection": { @@ -157,7 +153,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." "destinationConfig": { "scope": "repository", "owner": "my-github", - "repo: "my-repository" + "repo": "my-repository" } } } diff --git a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx index 7ff43613f..ca1d363bd 100644 --- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Switch } from "@app/components/v2"; +import { Button, Checkbox, FormControl, Switch } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { @@ -43,6 +43,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop const { name: destinationName } = SECRET_SYNC_MAP[destination]; const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const [confirmOverwrite, setConfirmOverwrite] = useState(false); const { syncOption } = useSecretSyncOption(destination); @@ -93,7 +94,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop setSelectedTabIndex((prev) => prev - 1); }; - const { handleSubmit, trigger } = formMethods; + const { handleSubmit, trigger, watch, control } = formMethods; const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields); @@ -122,7 +123,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop return isEnabled; }; - const { control } = formMethods; + const initialSyncBehavior = watch("syncOptions.initialSyncBehavior"); return (
@@ -195,10 +196,33 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop + {isFinalStep && + initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination && ( + setConfirmOverwrite(Boolean(isChecked))} + > +

+ I understand all secrets present in the configured {destinationName} destination will + be removed that are not present within Infisical. +

+
+ )}
- + {} {selectedTabIndex > 0 && (
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 c06bc710e..11a3f9045 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 @@ -13,17 +13,18 @@ const BaseSecretSyncSchema = z.object({ 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), - prependPrefix: z - .string() - .trim() - .transform((str) => str.toUpperCase()) - .optional(), - appendSuffix: z - .string() - .trim() - .transform((str) => str.toUpperCase()) - .optional() + 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() }), isEnabled: z.boolean() }); diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 033c00840..7f2a26357 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -28,7 +28,7 @@ export type TCreateSecretSyncDTO = DiscriminativePick< > & { environment: string; secretPath: string; projectId: string }; export type TUpdateSecretSyncDTO = Partial< - Omit + Omit > & { destination: SecretSync; syncId: string; diff --git a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts index 716173dd7..4a9fcbd6c 100644 --- a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts @@ -26,8 +26,8 @@ export type TRootSecretSync = { lastRemoveMessage: string | null; syncOptions: { initialSyncBehavior: SecretSyncInitialSyncBehavior; - prependPrefix?: string; - appendSuffix?: string; + // prependPrefix?: string; + // appendSuffix?: string; }; connection: { app: AppConnection; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx index 08eba28c4..f44ea439b 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx @@ -19,7 +19,7 @@ export const SecretSyncsTab = () => { const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs( currentWorkspace.id, { - refetchInterval: 2000 + refetchInterval: 4000 } ); diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx index 03b5d3e4d..1a67e98a0 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); @@ -109,7 +109,10 @@ const PageContent = () => {
- +
{ +export const SecretSyncOptionsSection = ({ + secretSync + // onEditOptions +}: Props) => { const { destination, - syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior }, - lastSyncedAt + syncOptions: { + // appendSuffix, + // prependPrefix, + initialSyncBehavior + } } = secretSync; return ( @@ -26,7 +25,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =

Sync Options

- @@ -41,17 +40,15 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = )} - + */}
- {!lastSyncedAt && ( - - {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} - - )} - {prependPrefix} - {appendSuffix} + + {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} + + {/* {prependPrefix} + {appendSuffix} */}