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 65cd3a0e4..d6f0091e5 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 @@ -1,6 +1,7 @@ import path from "node:path"; import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas"; +import { UnauthorizedError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -375,6 +376,7 @@ type TInterpolateSecretArg = { decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined; secretDAL: Pick; folderDAL: Pick; + canExpandValue: (environment: string, secretPath: string) => boolean; }; const MAX_SECRET_REFERENCE_DEPTH = 10; @@ -382,7 +384,8 @@ export const expandSecretReferencesFactory = ({ projectId, decryptSecretValue: decryptSecret, secretDAL, - folderDAL + folderDAL, + canExpandValue }: TInterpolateSecretArg) => { const secretCache: Record> = {}; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; @@ -432,6 +435,11 @@ export const expandSecretReferencesFactory = ({ if (entities.length === 1) { const [secretKey] = entities; + if (!canExpandValue(environment, secretPath)) + throw new UnauthorizedError({ + message: `You don't have access to secret ${secretKey} in environment ${environment} of secret path ${secretPath} ` + }); + // eslint-disable-next-line no-continue,no-await-in-loop const referedValue = await fetchSecret(environment, secretPath, secretKey); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -452,6 +460,11 @@ export const expandSecretReferencesFactory = ({ const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); const secretReferenceKey = entities[entities.length - 1]; + if (!canExpandValue(secretReferenceEnvironment, secretReferencePath)) + throw new UnauthorizedError({ + message: `You don't have access to secret ${secretReferenceKey} in environment ${secretReferenceEnvironment} of secret path ${secretReferencePath} ` + }); + // eslint-disable-next-line no-await-in-loop const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); 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 d7f895084..5dbb32e7d 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 @@ -152,6 +152,15 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); + references.forEach((referredSecret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: referredSecret.environment, + secretPath: referredSecret.secretPath + }) + ); + }); const secret = await secretDAL.transaction((tx) => fnSecretBulkInsert({ @@ -292,6 +301,17 @@ export const secretV2BridgeServiceFactory = ({ references: getAllNestedSecretReferences(secretValue) } : {}; + if (encryptedValue.references) { + encryptedValue.references.forEach((referredSecret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: referredSecret.environment, + secretPath: referredSecret.secretPath + }) + ); + }); + } const updatedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ @@ -675,7 +695,12 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderDAL, secretDAL, - decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) + decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), + canExpandValue: (expandEnvironment, expandSecretPath) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) + ) }); if (shouldExpandSecretReferences) { @@ -799,7 +824,12 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderDAL, secretDAL, - decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) + decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), + canExpandValue: (expandEnvironment, expandSecretPath) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) + ) }); // now if secret is not found @@ -916,21 +946,34 @@ export const secretV2BridgeServiceFactory = ({ const newSecrets = await secretDAL.transaction(async (tx) => fnSecretBulkInsert({ - inputSecrets: inputSecrets.map((el) => ({ - version: 1, - encryptedComment: setKnexStringValue( - el.secretComment, - (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob - ), - encryptedValue: el.secretValue - ? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob - : undefined, - skipMultilineEncoding: el.skipMultilineEncoding, - key: el.secretKey, - tagIds: el.tagIds, - references: getAllNestedSecretReferences(el.secretValue), - type: SecretType.Shared - })), + inputSecrets: inputSecrets.map((el) => { + const references = getAllNestedSecretReferences(el.secretValue); + references.forEach((referredSecret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: referredSecret.environment, + secretPath: referredSecret.secretPath + }) + ); + }); + + return { + version: 1, + encryptedComment: setKnexStringValue( + el.secretComment, + (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob + ), + encryptedValue: el.secretValue + ? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob + : undefined, + skipMultilineEncoding: el.skipMultilineEncoding, + key: el.secretKey, + tagIds: el.tagIds, + references, + type: SecretType.Shared + }; + }), folderId, secretDAL, secretVersionDAL, @@ -1037,6 +1080,19 @@ export const secretV2BridgeServiceFactory = ({ references: getAllNestedSecretReferences(el.secretValue) } : {}; + + if (encryptedValue.references) { + encryptedValue.references.forEach((referredSecret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: referredSecret.environment, + secretPath: referredSecret.secretPath + }) + ); + }); + } + return { filter: { id: originalSecret.id, type: SecretType.Shared }, data: { diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 4005c8f7a..ce9c58391 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -290,7 +290,9 @@ export const secretQueueFactory = ({ decryptSecretValue: dto.decryptor, secretDAL: secretV2BridgeDAL, folderDAL, - projectId: dto.projectId + projectId: dto.projectId, + // on integration expand all secrets + canExpandValue: () => true }); // process secrets in current folder const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);