From 59c0f1ff08928402a1b020ecb3a91e77c143da8a Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 16 Jul 2025 09:14:06 -0300 Subject: [PATCH 1/3] List secrets, throw when user does not have permission to read the referenced secret value --- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 71180772f..bb24c9d78 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1105,9 +1105,9 @@ export const secretV2BridgeServiceFactory = ({ if (shouldExpandSecretReferences) { const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath); - await Promise.allSettled( + await Promise.all( Object.keys(secretsGroupByPath).map((groupedPath) => - Promise.allSettled( + Promise.all( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, From 0174d361363ec6947eb26b94d8c49458a439a24e Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 16 Jul 2025 22:41:08 -0300 Subject: [PATCH 2/3] Throw a meaningful error message when secret reference process fails --- .../secret-v2-bridge-service.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index bb24c9d78..4d8365276 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1105,9 +1105,9 @@ export const secretV2BridgeServiceFactory = ({ if (shouldExpandSecretReferences) { const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath); - await Promise.all( + const settledPromises = await Promise.allSettled( Object.keys(secretsGroupByPath).map((groupedPath) => - Promise.all( + Promise.allSettled( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, @@ -1121,6 +1121,34 @@ export const secretV2BridgeServiceFactory = ({ ) ) ); + const errors: { path: string; error: string }[] = []; + + settledPromises.forEach((outerResult: PromiseSettledResult[]>, outerIndex) => { + const groupedPath = Object.keys(secretsGroupByPath)[outerIndex]; + + if (outerResult.status === "rejected") { + errors.push({ + path: groupedPath, + error: `Failed to process secret group: ${outerResult.reason}` + }); + } else { + // Check inner promise results + outerResult.value.forEach((innerResult: PromiseSettledResult) => { + if (innerResult.status === "rejected") { + errors.push({ + path: groupedPath, + error: `Failed to expand secret reference: ${innerResult.reason}` + }); + } + }); + } + }); + if (errors.length > 0) { + throw new ForbiddenRequestError({ + message: "Failed to expand one or more secret references", + details: errors.map((err) => err.error) + }); + } } if (!includeImports) { From b560cdb0f8e88ae77008c52232be20d2e8e8cfe0 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 17 Jul 2025 11:56:27 -0300 Subject: [PATCH 3/3] Improve reference error message --- .../src/services/secret-import/secret-import-fns.ts | 4 +++- .../src/services/secret-sync/secret-sync-queue.ts | 3 ++- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 8 ++++++-- .../secret-v2-bridge/secret-v2-bridge-service.ts | 12 ++++++++---- backend/src/services/secret/secret-queue.ts | 3 ++- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index c68033911..6aa73465d 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -174,6 +174,7 @@ export const fnSecretsV2FromImports = async ({ skipMultilineEncoding?: boolean | null; secretPath: string; environment: string; + secretKey: string; }) => Promise; hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { @@ -293,7 +294,8 @@ export const fnSecretsV2FromImports = async ({ value: decryptedSecret.secretValue, secretPath: processedImport.secretPath, environment: processedImport.environment, - skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding, + secretKey: decryptedSecret.secretKey }); // eslint-disable-next-line no-param-reassign processedImport.secrets[index].secretValue = expandedSecretValue || ""; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 2acba91e5..8f5a2e806 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -231,7 +231,8 @@ export const secretSyncQueueFactory = ({ environment: environment.slug, secretPath: folder.path, skipMultilineEncoding: secret.skipMultilineEncoding, - value: secretValue + value: secretValue, + secretKey }); secretMap[secretKey] = { value: expandedSecretValue || "" }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 01718d570..9f62b6d57 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -614,6 +614,7 @@ export const expandSecretReferencesFactory = ({ secretPath: string; environment: string; shouldStackTrace?: boolean; + secretKey: string; }) => { const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode; @@ -656,7 +657,7 @@ export const expandSecretReferencesFactory = ({ const referredValue = await fetchSecret(environment, secretPath, secretKey); if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` + message: `You do not have permission to read secret '${secretKey}' in environment '${environment}' at path '${secretPath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.` }); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -675,7 +676,7 @@ export const expandSecretReferencesFactory = ({ const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` + message: `You do not have permission to read secret '${secretReferenceKey}' in environment '${secretReferenceEnvironment}' at path '${secretReferencePath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.` }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); @@ -692,6 +693,7 @@ export const expandSecretReferencesFactory = ({ secretPath: referencedSecretPath, environment: referencedSecretEnvironmentSlug, depth: depth + 1, + secretKey: referencedSecretKey, trace }; @@ -726,6 +728,7 @@ export const expandSecretReferencesFactory = ({ skipMultilineEncoding?: boolean | null; secretPath: string; environment: string; + secretKey: string; }) => { if (!inputSecret.value) return inputSecret.value; @@ -741,6 +744,7 @@ export const expandSecretReferencesFactory = ({ value?: string; secretPath: string; environment: string; + secretKey: string; }) => { const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true }); return { stackTrace, expandedValue }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 4d8365276..cab41933f 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1113,7 +1113,8 @@ export const secretV2BridgeServiceFactory = ({ value: decryptedSecret.secretValue, secretPath: groupedPath, environment, - skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding, + secretKey: decryptedSecret.secretKey }); // eslint-disable-next-line no-param-reassign secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || ""; @@ -1135,9 +1136,10 @@ export const secretV2BridgeServiceFactory = ({ // Check inner promise results outerResult.value.forEach((innerResult: PromiseSettledResult) => { if (innerResult.status === "rejected") { + const reason = innerResult.reason as ForbiddenRequestError; errors.push({ path: groupedPath, - error: `Failed to expand secret reference: ${innerResult.reason}` + error: reason.message }); } }); @@ -1452,7 +1454,8 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath: path, value: secretValue, - skipMultilineEncoding: secret.skipMultilineEncoding + skipMultilineEncoding: secret.skipMultilineEncoding, + secretKey: secret.key }); secretValue = expandedSecretValue || ""; @@ -2750,7 +2753,8 @@ export const secretV2BridgeServiceFactory = ({ const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ environment, secretPath, - value: decryptedSecretValue + value: decryptedSecretValue, + secretKey: secretName }); return { tree: stackTrace, value: expandedValue }; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 8fcaa6e34..b178aa8e1 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -426,7 +426,8 @@ export const secretQueueFactory = ({ environment: dto.environment, secretPath: dto.secretPath, skipMultilineEncoding: secret.skipMultilineEncoding, - value: secretValue + value: secretValue, + secretKey }); content[secretKey] = { value: expandedSecretValue || "" };