diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 0ba7099a3..0f5f3f3db 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -3,16 +3,14 @@ import { z } from "zod"; import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, - SecretApprovalRequestsSecretsSchema, - SecretsSchema, SecretTagsSchema, - SecretVersionsSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; const approvalRequestUser = z.object({ userId: z.string() }).merge( @@ -261,43 +259,30 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv committerUser: approvalRequestUser, reviewers: approvalRequestUser.extend({ status: z.string() }).array(), secretPath: z.string(), - commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true }) + commits: secretRawSchema + .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .merge( z.object({ tags: tagSchema, - secret: SecretsSchema.pick({ - id: true, - version: true, - secretKeyIV: true, - secretKeyTag: true, - secretKeyCiphertext: true, - secretValueIV: true, - secretValueTag: true, - secretValueCiphertext: true, - secretCommentIV: true, - secretCommentTag: true, - secretCommentCiphertext: true - }) + secret: z + .object({ + id: z.string(), + version: z.number(), + secretKey: z.string(), + secretValue: z.string().optional(), + secretComment: z.string().optional() + }) .optional() .nullable(), - secretVersion: SecretVersionsSchema.pick({ - id: true, - version: true, - secretKeyIV: true, - secretKeyTag: true, - secretKeyCiphertext: true, - secretValueIV: true, - secretValueTag: true, - secretValueCiphertext: true, - secretCommentIV: true, - secretCommentTag: true, - secretCommentCiphertext: true - }) - .merge( - z.object({ - tags: tagSchema - }) - ) + secretVersion: z + .object({ + id: z.string(), + version: z.number(), + secretKey: z.string(), + secretValue: z.string().optional(), + secretComment: z.string().optional(), + tags: tagSchema + }) .optional() }) ) diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 0604135ba..11443ebfe 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -1,8 +1,8 @@ import { z } from "zod"; -import { SecretVersionsSchema } from "@app/db/schemas"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretVersionRouter = async (server: FastifyZodProvider) => { @@ -22,7 +22,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }).array() + secretVersions: secretRawSchema.array() }) } }, diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 6767f8383..aa1e66be2 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,9 +1,10 @@ import { z } from "zod"; -import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas"; +import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSnapshotRouter = async (server: FastifyZodProvider) => { @@ -27,7 +28,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { slug: z.string(), name: z.string() }), - secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }) + secretVersions: secretRawSchema + .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .merge( z.object({ tags: SecretTagsSchema.pick({ diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 7d2197a58..6bd9bbd40 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -221,6 +221,15 @@ export const secretApprovalRequestServiceFactory = ({ ); secrets = encrypedSecrets.map((el) => ({ ...el, + secretKey: el.key, + id: el.id, + version: el.version, + secretValue: el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : undefined, + secretComment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : undefined, secret: { secretKey: el.secret.key, id: el.secret.id, @@ -249,6 +258,7 @@ export const secretApprovalRequestServiceFactory = ({ const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); secrets = encrypedSecrets.map((el) => ({ ...el, + ...decryptSecretWithBot(el, botKey), secret: { id: el.secret.id, version: el.secret.version, diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 39d3a0997..c5d8bd4fa 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,9 +1,13 @@ import { ForbiddenError, subject } from "@casl/ability"; import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; @@ -45,6 +49,8 @@ type TSecretSnapshotServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; licenseService: Pick; + kmsService: Pick; + projectBotService: Pick; }; export type TSecretSnapshotServiceFactory = ReturnType; @@ -64,7 +70,9 @@ export const secretSnapshotServiceFactory = ({ secretVersionV2BridgeDAL, secretV2BridgeDAL, snapshotSecretV2BridgeDAL, - secretVersionV2TagBridgeDAL + secretVersionV2TagBridgeDAL, + kmsService, + projectBotService }: TSecretSnapshotServiceFactoryDep) => { const projectSecretSnapshotCount = async ({ environment, @@ -144,9 +152,55 @@ export const secretSnapshotServiceFactory = ({ const shouldUseBridge = snapshot.projectVersion === 3; let snapshotDetails; if (shouldUseBridge) { - snapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: snapshot.projectId + }); + const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); + snapshotDetails = { + ...encryptedSnapshotDetails, + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ + ...el, + secretKey: el.key, + secretValue: el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : undefined, + secretComment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : undefined + })) + }; } else { - snapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); + const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); + const { botKey } = await projectBotService.getBotKey(snapshot.projectId); + if (!botKey) throw new BadRequestError({ message: "bot not found" }); + snapshotDetails = { + ...encryptedSnapshotDetails, + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ + ...el, + secretKey: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretKeyCiphertext, + iv: el.secretKeyIV, + tag: el.secretKeyTag, + key: botKey + }), + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretValueCiphertext, + iv: el.secretValueIV, + tag: el.secretValueTag, + key: botKey + }), + secretComment: + el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretCommentCiphertext, + iv: el.secretCommentIV, + tag: el.secretCommentTag, + key: botKey + }) + : "" + })) + }; } const fullFolderPath = await getFullFolderPath({ diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3e4f940f7..0627cc932 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -66,6 +66,7 @@ import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/s import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal"; import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal"; +import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; @@ -235,7 +236,7 @@ export const registerRoutes = async ( const secretV2BridgeDAL = secretV2BridgeDALFactory(db); const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db); - const secretVersionV2TagBridgeDAL = secretVersionV2TagBridgeDALFactory(db); + const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db); const integrationDAL = integrationDALFactory(db); const integrationAuthDAL = integrationAuthDALFactory(db); @@ -285,6 +286,7 @@ export const registerRoutes = async ( const secretRotationDAL = secretRotationDALFactory(db); const snapshotDAL = snapshotDALFactory(db); const snapshotSecretDAL = snapshotSecretDALFactory(db); + const snapshotSecretV2BridgeDAL = snapshotSecretV2DALFactory(db); const snapshotFolderDAL = snapshotFolderDALFactory(db); const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db); @@ -665,7 +667,13 @@ export const registerRoutes = async ( secretVersionDAL, folderVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + projectBotService, + kmsService, + secretV2BridgeDAL, + secretVersionV2BridgeDAL, + snapshotSecretV2BridgeDAL, + secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL }); const webhookService = webhookServiceFactory({ permissionService, @@ -689,7 +697,8 @@ export const registerRoutes = async ( integrationDAL, permissionService, projectBotDAL, - projectBotService + projectBotService, + kmsService }); const secretQueueService = secretQueueFactory({ queueService, @@ -709,7 +718,11 @@ export const registerRoutes = async ( secretVersionDAL, secretBlindIndexDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + kmsService, + secretVersionV2BridgeDAL, + secretV2BridgeDAL, + secretVersionTagV2BridgeDAL }); const secretImportService = secretImportServiceFactory({ licenseService, @@ -720,7 +733,9 @@ export const registerRoutes = async ( secretImportDAL, projectDAL, secretDAL, - secretQueueService + secretQueueService, + secretV2BridgeDAL, + kmsService }); const secretBlindIndexService = secretBlindIndexServiceFactory({ permissionService, @@ -734,13 +749,39 @@ export const registerRoutes = async ( secretQueueService, secretDAL: secretV2BridgeDAL, permissionService, - secretVersionTagDAL: secretVersionV2TagBridgeDAL, + secretVersionTagDAL: secretVersionTagV2BridgeDAL, secretTagDAL, projectEnvDAL, secretImportDAL, secretApprovalRequestDAL, secretApprovalPolicyService, - secretApprovalRequestSecretDAL + secretApprovalRequestSecretDAL, + kmsService, + snapshotService + }); + + const secretApprovalRequestService = secretApprovalRequestServiceFactory({ + permissionService, + projectBotService, + folderDAL, + secretDAL, + secretTagDAL, + secretApprovalRequestSecretDAL, + secretApprovalRequestReviewerDAL, + projectDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretApprovalRequestDAL, + snapshotService, + secretVersionTagDAL, + secretQueueService, + kmsService, + secretV2BridgeDAL, + secretVersionV2BridgeDAL, + secretVersionTagV2BridgeDAL, + smtpService, + projectEnvDAL, + userDAL }); const secretService = secretServiceFactory({ @@ -760,7 +801,8 @@ export const registerRoutes = async ( secretApprovalPolicyService, secretApprovalRequestDAL, secretApprovalRequestSecretDAL, - secretV2BridgeService + secretV2BridgeService, + secretApprovalRequestService }); const secretSharingService = secretSharingServiceFactory({ @@ -769,26 +811,6 @@ export const registerRoutes = async ( orgDAL }); - const secretApprovalRequestService = secretApprovalRequestServiceFactory({ - permissionService, - projectBotService, - folderDAL, - secretDAL, - secretTagDAL, - secretApprovalRequestSecretDAL, - secretApprovalRequestReviewerDAL, - projectDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretApprovalRequestDAL, - snapshotService, - secretVersionTagDAL, - secretQueueService, - smtpService, - userDAL, - projectEnvDAL - }); - const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, @@ -822,11 +844,14 @@ export const registerRoutes = async ( queueService, folderDAL, secretApprovalPolicyService, - secretBlindIndexDAL, secretApprovalRequestDAL, secretApprovalRequestSecretDAL, secretQueueService, - projectBotService + projectBotService, + kmsService, + secretV2BridgeDAL, + secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL, + secretVersionV2BridgeDAL }); const secretRotationQueue = secretRotationQueueFactory({ telemetryService, @@ -834,7 +859,10 @@ export const registerRoutes = async ( queue: queueService, secretDAL, secretVersionDAL, - projectBotService + projectBotService, + secretVersionV2BridgeDAL, + secretV2BridgeDAL, + kmsService }); const secretRotationService = secretRotationServiceFactory({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index b24bef1a0..b0776d150 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -18,7 +18,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ProjectFilterType } from "@app/services/project/project-types"; -import { SecretOperations } from "@app/services/secret/secret-types"; +import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { secretRawSchema } from "../sanitizedSchemas"; @@ -442,14 +442,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote) }), response: { - 200: z.object({ - secret: secretRawSchema - }) + 200: z.union([ + z.object({ + secret: secretRawSchema + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const secret = await server.services.secret.createSecretRaw({ + const secretOperation = await server.services.secret.createSecretRaw({ actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -466,7 +469,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretReminderNote: req.body.secretReminderNote, secretReminderRepeatDays: req.body.secretReminderRepeatDays }); + if (secretOperation.type === SecretProtectionType.Approval) { + return { approval: secretOperation.approval }; + } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ projectId: req.body.workspaceId, ...req.auditLogInfo, @@ -542,14 +549,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment) }), response: { - 200: z.object({ - secret: secretRawSchema - }) + 200: z.union([ + z.object({ + secret: secretRawSchema + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const secret = await server.services.secret.updateSecretRaw({ + const secretOperation = await server.services.secret.updateSecretRaw({ actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -568,6 +578,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { newSecretName: req.body.newSecretName, secretComment: req.body.secretComment }); + if (secretOperation.type === SecretProtectionType.Approval) { + return { approval: secretOperation.approval }; + } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ projectId: req.body.workspaceId, @@ -628,14 +642,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type) }), response: { - 200: z.object({ - secret: secretRawSchema - }) + 200: z.union([ + z.object({ + secret: secretRawSchema + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const secret = await server.services.secret.deleteSecretRaw({ + const secretOperation = await server.services.secret.deleteSecretRaw({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, @@ -646,6 +663,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: req.params.secretName, type: req.body.type }); + if (secretOperation.type === SecretProtectionType.Approval) { + return { approval: secretOperation.approval }; + } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ projectId: req.body.workspaceId, @@ -1815,16 +1836,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .min(1) }), response: { - 200: z.object({ - secrets: secretRawSchema.array() - }) + 200: z.union([ + z.object({ + secrets: secretRawSchema.array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; - const secrets = await server.services.secret.createManySecretsRaw({ + const secretOperation = await server.services.secret.createManySecretsRaw({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, @@ -1835,6 +1859,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId: req.body.workspaceId, secrets: inputSecrets }); + if (secretOperation.type === SecretProtectionType.Approval) { + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, @@ -1914,15 +1942,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .min(1) }), response: { - 200: z.object({ - secrets: secretRawSchema.array() - }) + 200: z.union([ + z.object({ + secrets: secretRawSchema.array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; - const secrets = await server.services.secret.updateManySecretsRaw({ + const secretOperation = await server.services.secret.updateManySecretsRaw({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, @@ -1933,6 +1964,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId: req.body.workspaceId, secrets: inputSecrets }); + if (secretOperation.type === SecretProtectionType.Approval) { + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, @@ -1999,15 +2034,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .min(1) }), response: { - 200: z.object({ - secrets: secretRawSchema.array() - }) + 200: z.union([ + z.object({ + secrets: secretRawSchema.array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; - const secrets = await server.services.secret.deleteManySecretsRaw({ + const secretOperation = await server.services.secret.deleteManySecretsRaw({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, @@ -2018,6 +2056,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId: req.body.workspaceId, secrets: inputSecrets }); + if (secretOperation.type === SecretProtectionType.Approval) { + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, 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 9656221d2..726314a45 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 @@ -995,9 +995,20 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions; + return secretVersions.map((el) => + reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, + comment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : undefined + }) + ); }; // this is a backfilling API for secret references diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 11222ec2b..ea29f7c8b 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -51,6 +51,7 @@ import { import { TSecretQueueFactory } from "./secret-queue"; import { SecretOperations, + SecretProtectionType, TAttachSecretTagsDTO, TBackFillSecretReferencesDTO, TCreateBulkSecretDTO, @@ -1228,7 +1229,7 @@ export const secretServiceFactory = ({ : undefined; if (shouldUseSecretV2Bridge) { if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, secretPath, environment, @@ -1251,6 +1252,7 @@ export const secretServiceFactory = ({ ] } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secret = await secretV2BridgeService.createSecret({ @@ -1270,7 +1272,7 @@ export const secretServiceFactory = ({ skipMultilineEncoding, secretReminderRepeatDays }); - return secret; + return { secret, type: SecretProtectionType.Direct as const }; } if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -1278,7 +1280,7 @@ export const secretServiceFactory = ({ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequest({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, secretPath, environment, @@ -1306,6 +1308,7 @@ export const secretServiceFactory = ({ ] } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secret = await createSecret({ @@ -1333,7 +1336,7 @@ export const secretServiceFactory = ({ tags: tagIds }); - return decryptSecretRaw(secret, botKey); + return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; }; const updateSecretRaw = async ({ @@ -1362,7 +1365,7 @@ export const secretServiceFactory = ({ : undefined; if (shouldUseSecretV2Bridge) { if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, secretPath, environment, @@ -1386,6 +1389,7 @@ export const secretServiceFactory = ({ ] } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secret = await secretV2BridgeService.updateSecret({ secretReminderRepeatDays, @@ -1406,7 +1410,7 @@ export const secretServiceFactory = ({ metadata, secretValue }); - return secret; + return { type: SecretProtectionType.Direct as const, secret }; } if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -1416,7 +1420,7 @@ export const secretServiceFactory = ({ const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(newSecretName || secretName, botKey); if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequest({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, secretPath, environment, @@ -1447,6 +1451,7 @@ export const secretServiceFactory = ({ ] } }); + return { approval, type: SecretProtectionType.Approval as const }; } const secret = await updateSecret({ @@ -1477,7 +1482,7 @@ export const secretServiceFactory = ({ }); await snapshotService.performSnapshot(secret.folderId); - return decryptSecretRaw(secret, botKey); + return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; }; const deleteSecretRaw = async ({ @@ -1498,7 +1503,7 @@ export const secretServiceFactory = ({ : undefined; if (shouldUseSecretV2Bridge) { if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, actorAuthMethod, actorOrgId, @@ -1515,6 +1520,7 @@ export const secretServiceFactory = ({ ] } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secret = await secretV2BridgeService.deleteSecret({ secretName, @@ -1527,11 +1533,11 @@ export const secretServiceFactory = ({ environment, secretPath }); - return secret; + return { type: SecretProtectionType.Direct as const, secret }; } if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequest({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, actorAuthMethod, actorOrgId, @@ -1548,6 +1554,7 @@ export const secretServiceFactory = ({ ] } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secret = await deleteSecret({ secretName, @@ -1561,7 +1568,7 @@ export const secretServiceFactory = ({ actorAuthMethod }); - return decryptSecretRaw(secret, botKey); + return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; }; const createManySecretsRaw = async ({ @@ -1593,7 +1600,7 @@ export const secretServiceFactory = ({ : undefined; if (shouldUseSecretV2Bridge) { if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, secretPath, environment, @@ -1613,6 +1620,7 @@ export const secretServiceFactory = ({ })) } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secrets = await secretV2BridgeService.createManySecret({ secretPath, @@ -1624,8 +1632,9 @@ export const secretServiceFactory = ({ actorId, secrets: inputSecrets }); - return secrets; + return { secrets, type: SecretProtectionType.Direct as const }; } + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); const sanitizedSecrets = inputSecrets.map( ({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => { @@ -1651,7 +1660,7 @@ export const secretServiceFactory = ({ } ); if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequest({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, secretPath, environment, @@ -1664,6 +1673,7 @@ export const secretServiceFactory = ({ [SecretOperations.Create]: sanitizedSecrets } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secrets = await createManySecret({ projectId, @@ -1676,9 +1686,12 @@ export const secretServiceFactory = ({ secrets: sanitizedSecrets }); - return secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) - ); + return { + type: SecretProtectionType.Direct as const, + secrets: secrets.map((secret) => + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + ) + }; }; const updateManySecretsRaw = async ({ @@ -1709,7 +1722,7 @@ export const secretServiceFactory = ({ : undefined; if (shouldUseSecretV2Bridge) { if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, secretPath, environment, @@ -1728,6 +1741,7 @@ export const secretServiceFactory = ({ })) } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secrets = await secretV2BridgeService.updateManySecret({ secretPath, @@ -1739,7 +1753,7 @@ export const secretServiceFactory = ({ actorId, secrets: inputSecrets }); - return secrets; + return { type: SecretProtectionType.Direct as const, secrets }; } if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -1779,7 +1793,7 @@ export const secretServiceFactory = ({ } ); if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequest({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, secretPath, environment, @@ -1792,6 +1806,8 @@ export const secretServiceFactory = ({ [SecretOperations.Update]: sanitizedSecrets } }); + + return { type: SecretProtectionType.Approval as const, approval }; } const secrets = await updateManySecret({ projectId, @@ -1804,9 +1820,12 @@ export const secretServiceFactory = ({ secrets: sanitizedSecrets }); - return secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) - ); + return { + type: SecretProtectionType.Direct as const, + secrets: secrets.map((secret) => + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + ) + }; }; const deleteManySecretsRaw = async ({ @@ -1837,7 +1856,7 @@ export const secretServiceFactory = ({ : undefined; if (shouldUseSecretV2Bridge) { if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, actorAuthMethod, actorOrgId, @@ -1850,6 +1869,7 @@ export const secretServiceFactory = ({ [SecretOperations.Delete]: inputSecrets } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secrets = await secretV2BridgeService.deleteManySecret({ secretPath, @@ -1861,13 +1881,13 @@ export const secretServiceFactory = ({ actorId, secrets: inputSecrets }); - return secrets; + return { type: SecretProtectionType.Direct as const, secrets }; } if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); if (policy) { - return secretApprovalRequestService.generateSecretApprovalRequest({ + const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, actorAuthMethod, actorOrgId, @@ -1880,6 +1900,7 @@ export const secretServiceFactory = ({ [SecretOperations.Delete]: inputSecrets.map((el) => ({ secretName: el.secretKey })) } }); + return { type: SecretProtectionType.Approval as const, approval }; } const secrets = await deleteManySecret({ projectId, @@ -1892,9 +1913,12 @@ export const secretServiceFactory = ({ secrets: inputSecrets.map(({ secretKey, type = SecretType.Shared }) => ({ secretName: secretKey, type })) }); - return secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) - ); + return { + type: SecretProtectionType.Direct as const, + secrets: secrets.map((secret) => + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + ) + }; }; const getSecretVersions = async ({ @@ -1906,12 +1930,25 @@ export const secretServiceFactory = ({ offset = 0, secretId }: TGetSecretVersionsDTO) => { + const secretVersionV2 = await secretV2BridgeService.getSecretVersions({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + limit, + offset, + secretId + }); + if (secretVersionV2) return secretVersionV2; + const secret = await secretDAL.findById(secretId); if (!secret) throw new BadRequestError({ message: "Failed to find secret" }); - const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new BadRequestError({ message: "Failed to find secret" }); + const { botKey } = await projectBotService.getBotKey(folder.projectId); + if (!botKey) throw new BadRequestError({ message: "bot not found" }); + const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -1920,9 +1957,18 @@ export const secretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions; + return secretVersions.map((el) => + decryptSecretRaw( + { + ...el, + workspace: folder.projectId, + environment: folder.environment.envSlug, + secretPath: "/" + }, + botKey + ) + ); }; const attachTags = async ({ diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 8628e12f3..e6dcf19e2 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -444,3 +444,8 @@ export type TMoveSecretsDTO = { secretIds: string[]; shouldOverwrite: boolean; } & Omit; + +export enum SecretProtectionType { + Approval = "approval", + Direct = "direct" +}