From 30d7e63a67fcedf3399bb2ef346371052bf0ff49 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 4 Jun 2025 21:20:16 -0400 Subject: [PATCH] Add {{environment}} support for key schemas --- .../oci-vault/oci-vault-sync-fns.ts | 3 +- .../1password/1password-sync-fns.ts | 3 +- .../aws-parameter-store-sync-fns.ts | 4 +- .../aws-secrets-manager-sync-fns.ts | 24 ++++++--- .../azure-app-configuration-sync-fns.ts | 2 +- .../azure-key-vault-sync-fns.ts | 2 +- .../secret-sync/camunda/camunda-sync-fns.ts | 2 +- .../databricks/databricks-sync-fns.ts | 2 +- .../services/secret-sync/gcp/gcp-sync-fns.ts | 2 +- .../secret-sync/github/github-sync-fns.ts | 3 +- .../secret-sync/hc-vault/hc-vault-sync-fns.ts | 3 +- .../humanitec/humanitec-sync-fns.ts | 3 +- .../services/secret-sync/secret-sync-fns.ts | 50 ++++++++++++++----- .../secret-sync/secret-sync-schemas.ts | 27 ++++++++-- .../secret-sync/teamcity/teamcity-sync-fns.ts | 2 +- .../terraform-cloud-sync-fns.ts | 7 ++- .../secret-sync/vercel/vercel-sync-fns.ts | 5 +- .../secret-sync/windmill/windmill-sync-fns.ts | 3 +- docs/integrations/secret-syncs/1password.mdx | 2 +- .../secret-syncs/aws-parameter-store.mdx | 2 +- .../secret-syncs/aws-secrets-manager.mdx | 2 +- .../secret-syncs/azure-app-configuration.mdx | 2 +- .../secret-syncs/azure-key-vault.mdx | 2 +- docs/integrations/secret-syncs/camunda.mdx | 2 +- docs/integrations/secret-syncs/databricks.mdx | 2 +- .../secret-syncs/gcp-secret-manager.mdx | 2 +- docs/integrations/secret-syncs/github.mdx | 2 +- .../secret-syncs/hashicorp-vault.mdx | 2 +- docs/integrations/secret-syncs/humanitec.mdx | 2 +- docs/integrations/secret-syncs/oci-vault.mdx | 2 +- docs/integrations/secret-syncs/teamcity.mdx | 2 +- .../secret-syncs/terraform-cloud.mdx | 2 +- docs/integrations/secret-syncs/vercel.mdx | 2 +- docs/integrations/secret-syncs/windmill.mdx | 2 +- .../forms/schemas/base-secret-sync-schema.ts | 22 ++++++-- 35 files changed, 140 insertions(+), 61 deletions(-) diff --git a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts index 5b05b2301..5fb39a0ec 100644 --- a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -117,6 +117,7 @@ export const OCIVaultSyncFns = { syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { compartmentOcid, vaultOcid, keyOcid } } = secretSync; @@ -213,7 +214,7 @@ export const OCIVaultSyncFns = { // Update and delete secrets for await (const [key, variable] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; // Only update / delete active secrets if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { diff --git a/backend/src/services/secret-sync/1password/1password-sync-fns.ts b/backend/src/services/secret-sync/1password/1password-sync-fns.ts index c832fbbdb..9305f2e3c 100644 --- a/backend/src/services/secret-sync/1password/1password-sync-fns.ts +++ b/backend/src/services/secret-sync/1password/1password-sync-fns.ts @@ -127,6 +127,7 @@ export const OnePassSyncFns = { syncSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { vaultId } } = secretSync; @@ -164,7 +165,7 @@ export const OnePassSyncFns = { for await (const [key, variable] of Object.entries(items)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(key in secretMap)) { try { 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 a73bc81c9..b687d81dd 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 @@ -294,7 +294,7 @@ const deleteParametersBatch = async ( export const AwsParameterStoreSyncFns = { syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, syncOptions } = secretSync; + const { destinationConfig, syncOptions, environment } = secretSync; const ssm = await getSSM(secretSync); @@ -391,7 +391,7 @@ export const AwsParameterStoreSyncFns = { const [key, parameter] = entry; // eslint-disable-next-line no-continue - if (!matchesSchema(key, syncOptions.keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", syncOptions.keySchema)) continue; if (!(key in secretMap) || !secretMap[key].value) { parametersToDelete.push(parameter); diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts index 1b1daf2ac..df73512e5 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -57,7 +57,11 @@ const sleep = async () => setTimeout(resolve, 1000); }); -const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string): Promise => { +const getSecretsRecord = async ( + client: SecretsManagerClient, + environment: string, + keySchema?: string +): Promise => { const awsSecretsRecord: TAwsSecretsRecord = {}; let hasNext = true; let nextToken: string | undefined; @@ -72,7 +76,7 @@ const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string if (output.SecretList) { output.SecretList.forEach((secretEntry) => { - if (secretEntry.Name && matchesSchema(secretEntry.Name, keySchema)) { + if (secretEntry.Name && matchesSchema(secretEntry.Name, environment, keySchema)) { awsSecretsRecord[secretEntry.Name] = secretEntry; } }); @@ -307,11 +311,11 @@ const processTags = ({ export const AwsSecretsManagerSyncFns = { syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, syncOptions } = secretSync; + const { destinationConfig, syncOptions, environment } = secretSync; const client = await getSecretsManagerClient(secretSync); - const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema); + const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema); const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); @@ -401,7 +405,7 @@ export const AwsSecretsManagerSyncFns = { for await (const secretKey of Object.keys(awsSecretsRecord)) { // eslint-disable-next-line no-continue - if (!matchesSchema(secretKey, syncOptions.keySchema)) continue; + if (!matchesSchema(secretKey, environment?.slug || "", syncOptions.keySchema)) continue; if (!(secretKey in secretMap) || !secretMap[secretKey].value) { try { @@ -468,7 +472,11 @@ export const AwsSecretsManagerSyncFns = { getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise => { const client = await getSecretsManagerClient(secretSync); - const awsSecretsRecord = await getSecretsRecord(client, secretSync.syncOptions.keySchema); + const awsSecretsRecord = await getSecretsRecord( + client, + secretSync.environment?.slug || "", + secretSync.syncOptions.keySchema + ); const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); const { destinationConfig } = secretSync; @@ -503,11 +511,11 @@ export const AwsSecretsManagerSyncFns = { } }, removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, syncOptions } = secretSync; + const { destinationConfig, syncOptions, environment } = secretSync; const client = await getSecretsManagerClient(secretSync); - const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema); + const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema); if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { for await (const secretKey of Object.keys(awsSecretsRecord)) { diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts index dce509fac..7aa1c16ce 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -141,7 +141,7 @@ export const azureAppConfigurationSyncFactory = ({ for await (const key of Object.keys(azureAppConfigSecrets)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; const azureSecret = azureAppConfigSecrets[key]; if ( diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts index fd1e2bd78..edc8af709 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -194,7 +194,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur for await (const deleteSecretKey of deleteSecrets.filter( (secret) => - matchesSchema(secret, secretSync.syncOptions.keySchema) && + matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) && !setSecrets.find((setSecret) => setSecret.key === secret) )) { await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts index 256ae4644..516efae10 100644 --- a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts +++ b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts @@ -118,7 +118,7 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec for await (const secret of Object.keys(camundaSecrets)) { // eslint-disable-next-line no-continue - if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(secret in secretMap) || !secretMap[secret].value) { try { diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts index 11143e24d..175901323 100644 --- a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -117,7 +117,7 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri for await (const secret of databricksSecretKeys) { // eslint-disable-next-line no-continue - if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(secret.key in secretMap)) { await deleteDatabricksSecrets({ diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index 97da66a48..6a45aab31 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -155,7 +155,7 @@ export const GcpSyncFns = { for await (const key of Object.keys(gcpSecrets)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; try { if (!(key in secretMap) || !secretMap[key].value) { 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 952f4b512..dd950692a 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -224,7 +224,8 @@ export const GithubSyncFns = { for await (const encryptedSecret of encryptedSecrets) { // eslint-disable-next-line no-continue - if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + continue; if (!(encryptedSecret.name in secretMap)) { await deleteSecret(client, secretSync, encryptedSecret); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts index 6331cd91f..724eec7be 100644 --- a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -68,6 +68,7 @@ export const HCVaultSyncFns = { syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { mount, path }, syncOptions: { disableSecretDeletion, keySchema } } = secretSync; @@ -97,7 +98,7 @@ export const HCVaultSyncFns = { for await (const [key] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; if (!(key in secretMap)) { delete variables[key]; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts index 2fcf488aa..aeef95047 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -201,7 +201,8 @@ export const HumanitecSyncFns = { for await (const humanitecSecret of humanitecSecrets) { // eslint-disable-next-line no-continue - if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(humanitecSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + continue; if (!secretMap[humanitecSecret.key]) { await deleteSecret(secretSync, humanitecSecret); diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index dbf3a3699..8e99aabb8 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -1,5 +1,5 @@ import { AxiosError } from "axios"; -import RE2 from "re2"; +import handlebars from "handlebars"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault"; @@ -68,13 +68,17 @@ type TSyncSecretDeps = { }; // Add schema to secret keys -const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => { +const addSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => { if (!schema) return unprocessedSecretMap; const processedSecretMap: TSecretMap = {}; for (const [key, value] of Object.entries(unprocessedSecretMap)) { - const newKey = new RE2("{{secretKey}}").replace(schema, key); + const newKey = handlebars.compile(schema)({ + secretKey: key, + environment + }); + processedSecretMap[newKey] = value; } @@ -103,21 +107,40 @@ const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecret }; // Checks if a key matches a schema -export const matchesSchema = (key: string, schema?: string): boolean => { +export const matchesSchema = (key: string, environment: string, schema?: string): boolean => { if (!schema) return true; - const [prefix, suffix] = schema.split("{{secretKey}}"); - if (prefix === undefined || suffix === undefined) return true; + const compiledSchemaPattern = handlebars.compile(schema)({ + secretKey: "{{secretKey}}", // Keep secretKey + environment + }); - return key.startsWith(prefix) && key.endsWith(suffix); + // This edge-case shouldn't be possible + if (!compiledSchemaPattern.includes("{{secretKey}}")) { + return key === compiledSchemaPattern; + } + + const parts = compiledSchemaPattern.split("{{secretKey}}"); + const prefix = parts[0]; + const suffix = parts[parts.length - 1]; + + if (prefix === "" && suffix === "") return true; + + // If prefix is empty, key must end with suffix + if (prefix === "") return key.endsWith(suffix); + + // If suffix is empty, key must start with prefix + if (suffix === "") return key.startsWith(prefix); + + return key.startsWith(prefix) && key.endsWith(suffix) && key.length >= prefix.length + suffix.length; }; // Filter only for secrets with keys that match the schema -const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => { +const filterForSchema = (secretMap: TSecretMap, environment: string, schema?: string): TSecretMap => { const filteredMap: TSecretMap = {}; for (const [key, value] of Object.entries(secretMap)) { - if (matchesSchema(key, schema)) { + if (matchesSchema(key, environment, schema)) { filteredMap[key] = value; } } @@ -131,7 +154,7 @@ export const SecretSyncFns = { secretMap: TSecretMap, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema); + const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); switch (secretSync.destination) { case SecretSync.AWSParameterStore: @@ -255,14 +278,17 @@ export const SecretSyncFns = { ); } - return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema); + return stripSchema( + filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema), + secretSync.syncOptions.keySchema + ); }, removeSecrets: ( secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema); + const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); switch (secretSync.destination) { case SecretSync.AWSParameterStore: diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 80e96bf8b..5b599215f 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -28,10 +28,29 @@ const BaseSyncOptionsSchema = ({ keySchema: z .string() .optional() - .refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), { - message: - "Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder." - }) + .refine( + (val) => { + if (!val) return true; + + const allowedOptionalPlaceholders = ["{{environment}}"]; + + const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders] + .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters + .join("|"); + + const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`); + const contentIsValid = allowedContentRegex.test(val); + + // Check if {{secretKey}} appears exactly once + const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length; + + return contentIsValid && secretKeyCount === 1; + }, + { + message: + "Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." + } + ) .describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema), disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) }); diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts index 0afe29beb..28ef2d0d3 100644 --- a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts @@ -127,7 +127,7 @@ export const TeamCitySyncFns = { for await (const [key, variable] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(key in secretMap)) { try { diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts index a58ec213c..cb546ba63 100644 --- a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts @@ -232,8 +232,11 @@ export const TerraformCloudSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for (const terraformCloudVariable of terraformCloudVariables) { - // eslint-disable-next-line no-continue - if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue; + if ( + !matchesSchema(terraformCloudVariable.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) + ) + // eslint-disable-next-line no-continue + continue; if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) { await deleteVariable(secretSync, terraformCloudVariable); diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts index 90e9327e5..b5ea98265 100644 --- a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts +++ b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts @@ -291,8 +291,9 @@ export const VercelSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const vercelSecret of vercelSecrets) { - // eslint-disable-next-line no-continue - if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(vercelSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + // eslint-disable-next-line no-continue + continue; if (!secretMap[vercelSecret.key]) { await deleteSecret(secretSync, vercelSecret); diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts index a09706581..b5e11c957 100644 --- a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts +++ b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts @@ -128,6 +128,7 @@ export const WindmillSyncFns = { syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { path }, syncOptions: { disableSecretDeletion, keySchema } } = secretSync; @@ -171,7 +172,7 @@ export const WindmillSyncFns = { for await (const [key, variable] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; if (!(key in secretMap)) { try { diff --git a/docs/integrations/secret-syncs/1password.mdx b/docs/integrations/secret-syncs/1password.mdx index a33f54c8d..c259e57bf 100644 --- a/docs/integrations/secret-syncs/1password.mdx +++ b/docs/integrations/secret-syncs/1password.mdx @@ -46,7 +46,7 @@ description: "Learn how to configure a 1Password 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 1Password when keys conflict. - **Import Secrets (Prioritize 1Password)**: Imports secrets from the destination endpoint before syncing, prioritizing values from 1Password over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 11f0c94ad..b1ceade0d 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index f7654eeae..e4c85e21b 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -43,7 +43,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index ee47504bc..cc1da4249 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -48,7 +48,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index 609ba8b8d..b676bc513 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -51,7 +51,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/camunda.mdx b/docs/integrations/secret-syncs/camunda.mdx index df57a5b7d..ee97ed6ac 100644 --- a/docs/integrations/secret-syncs/camunda.mdx +++ b/docs/integrations/secret-syncs/camunda.mdx @@ -39,7 +39,7 @@ description: "Learn how to configure a Camunda 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 Camunda when keys conflict. - **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx index 225bad5b1..5a303d9b0 100644 --- a/docs/integrations/secret-syncs/databricks.mdx +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -46,7 +46,7 @@ description: "Learn how to configure a Databricks Sync for Infisical." Databricks does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index ace63787d..5f1fcc326 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -42,7 +42,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict. - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 7786567cc..349680779 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -62,7 +62,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." GitHub does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/hashicorp-vault.mdx b/docs/integrations/secret-syncs/hashicorp-vault.mdx index 48e4d8dfd..143303a00 100644 --- a/docs/integrations/secret-syncs/hashicorp-vault.mdx +++ b/docs/integrations/secret-syncs/hashicorp-vault.mdx @@ -54,7 +54,7 @@ description: "Learn how to configure a Hashicorp Vault 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 Hashicorp Vault when keys conflict. - **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx index ec36bd4da..208c75df9 100644 --- a/docs/integrations/secret-syncs/humanitec.mdx +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -55,7 +55,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical." Humanitec does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 00b7120e7..db5abfb91 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -57,7 +57,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f - **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 OCI Vault when keys conflict. - **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/teamcity.mdx b/docs/integrations/secret-syncs/teamcity.mdx index 3482101ca..f5b2bf933 100644 --- a/docs/integrations/secret-syncs/teamcity.mdx +++ b/docs/integrations/secret-syncs/teamcity.mdx @@ -48,7 +48,7 @@ description: "Learn how to configure a TeamCity Sync for Infisical." Infisical only syncs secrets from within the target scope; inherited secrets will not be imported. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx index d2f762ef1..d8399db10 100644 --- a/docs/integrations/secret-syncs/terraform-cloud.mdx +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -56,7 +56,7 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical." Terraform Cloud does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/vercel.mdx b/docs/integrations/secret-syncs/vercel.mdx index c903d3faa..4553b46c6 100644 --- a/docs/integrations/secret-syncs/vercel.mdx +++ b/docs/integrations/secret-syncs/vercel.mdx @@ -43,7 +43,7 @@ description: "Learn how to configure a Vercel 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 Vercel when keys conflict. - **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/windmill.mdx b/docs/integrations/secret-syncs/windmill.mdx index e98a2c7b6..56dda245d 100644 --- a/docs/integrations/secret-syncs/windmill.mdx +++ b/docs/integrations/secret-syncs/windmill.mdx @@ -44,7 +44,7 @@ description: "Learn how to configure a Windmill 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 Windmill when keys conflict. - **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. 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 index 75a5b68c1..1d925eb07 100644 --- 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 @@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = - !val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val), + (val) => { + if (!val) return true; + + const allowedOptionalPlaceholders = ["{{environment}}"]; + + const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders] + .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters + .join("|"); + + const allowedContentRegex = new RegExp( + `^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$` + ); + const contentIsValid = allowedContentRegex.test(val); + + const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length; + + return contentIsValid && secretKeyCount === 1; + }, { message: - "Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder." + "Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." } ) });