From cd13733621b860a0547e522d8f7f44e4605f98d9 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 7 Jul 2025 13:50:03 -0700 Subject: [PATCH] improvement: create crud events for secret approvals on merge, improve secret approval audit logs and add missing merge event --- .../v1/secret-approval-request-router.ts | 39 ++++- .../ee/services/audit-log/audit-log-types.ts | 20 +++ .../secret-approval-request-service.ts | 110 ++++++++++++- backend/src/server/routes/v3/secret-router.ts | 147 +++++++++++++++++- 4 files changed, 300 insertions(+), 16 deletions(-) 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 d53124e52..66f6708a0 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -141,14 +141,39 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - approvalId: req.params.id, - bypassReason: req.body.bypassReason + const { approval, projectId, secretMutationEvents } = + await server.services.secretApprovalRequest.mergeSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + approvalId: req.params.id, + bypassReason: req.body.bypassReason + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event: { + type: EventType.SECRET_APPROVAL_MERGED, + metadata: { + mergedBy: req.permission.id, + secretApprovalRequestSlug: approval.slug, + secretApprovalRequestId: approval.id + } + } }); + + for await (const event of secretMutationEvents) { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event + }); + } + return { approval }; } }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 625dd6556..635bae46b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -116,6 +116,15 @@ interface BaseAuthData { userAgentType?: UserAgentType; } +export enum SecretApprovalEvent { + Create = "create", + Update = "update", + Delete = "delete", + CreateMany = "create-many", + UpdateMany = "update-many", + DeleteMany = "delete-many" +} + export enum UserAgentType { WEB = "web", CLI = "cli", @@ -1705,6 +1714,17 @@ interface SecretApprovalRequest { committedBy: string; secretApprovalRequestSlug: string; secretApprovalRequestId: string; + eventType: SecretApprovalEvent; + secretKey?: string; + secretId?: string; + secrets?: { + secretKey?: string; + secretId?: string; + environment?: string; + secretPath?: string; + }[]; + environment: string; + secretPath: string; }; } 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 c0242f8e7..101885fc5 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 @@ -10,6 +10,7 @@ import { TSecretApprovalRequestsSecretsInsert, TSecretApprovalRequestsSecretsV2Insert } from "@app/db/schemas"; +import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -523,7 +524,7 @@ export const secretApprovalRequestServiceFactory = ({ }); } - const { policy, folderId, projectId, bypassers } = secretApprovalRequest; + const { policy, folderId, projectId, bypassers, environment } = secretApprovalRequest; if (policy.deletedAt) { throw new BadRequestError({ message: "The policy associated with this secret approval request has been deleted." @@ -957,7 +958,112 @@ export const secretApprovalRequestServiceFactory = ({ }); } - return mergeStatus; + const { created, updated, deleted } = mergeStatus.secrets; + + const secretMutationEvents: Event[] = []; + + if (created.length) { + if (created.length > 1) { + secretMutationEvents.push({ + type: EventType.CREATE_SECRETS, + metadata: { + environment, + secretPath: folder.path, + secrets: created.map((secret) => ({ + secretId: secret.id, + secretVersion: 1, + // @ts-expect-error not present on v1 secrets + secretKey: secret.key as string, + // @ts-expect-error not present on v1 secrets + secretMetadata: secret.secretMetadata as ResourceMetadataDTO + })) + } + }); + } else { + const [secret] = created; + secretMutationEvents.push({ + type: EventType.CREATE_SECRET, + metadata: { + environment, + secretPath: folder.path, + secretId: secret.id, + secretVersion: 1, + // @ts-expect-error not present on v1 secrets + secretKey: secret.key as string, + // @ts-expect-error not present on v1 secrets + secretMetadata: secret.secretMetadata as ResourceMetadataDTO + } + }); + } + } + + if (updated.length) { + if (updated.length > 1) { + secretMutationEvents.push({ + type: EventType.UPDATE_SECRETS, + metadata: { + environment, + secretPath: folder.path, + secrets: updated.map((secret) => ({ + secretId: secret.id, + secretVersion: secret.version, + // @ts-expect-error not present on v1 secrets + secretKey: secret.key as string, + // @ts-expect-error not present on v1 secrets + secretMetadata: secret.secretMetadata as ResourceMetadataDTO + })) + } + }); + } else { + const [secret] = updated; + secretMutationEvents.push({ + type: EventType.UPDATE_SECRET, + metadata: { + environment, + secretPath: folder.path, + secretId: secret.id, + secretVersion: secret.version, + // @ts-expect-error not present on v1 secrets + secretKey: secret.key as string, + // @ts-expect-error not present on v1 secrets + secretMetadata: secret.secretMetadata as ResourceMetadataDTO + } + }); + } + } + + if (deleted.length) { + if (deleted.length > 1) { + secretMutationEvents.push({ + type: EventType.DELETE_SECRETS, + metadata: { + environment, + secretPath: folder.path, + secrets: deleted.map((secret) => ({ + secretId: secret.id, + secretVersion: secret.version, + // @ts-expect-error not present on v1 secrets + secretKey: secret.key as string + })) + } + }); + } else { + const [secret] = deleted; + secretMutationEvents.push({ + type: EventType.DELETE_SECRET, + metadata: { + environment, + secretPath: folder.path, + secretId: secret.id, + secretVersion: secret.version, + // @ts-expect-error not present on v1 secrets + secretKey: secret.key as string + } + }); + } + } + + return { ...mergeStatus, projectId, secretMutationEvents }; }; // function to save secret change to secret approval diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 8784989bd..55fc094b7 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -2,7 +2,7 @@ import picomatch from "picomatch"; import { z } from "zod"; import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; -import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, SecretApprovalEvent, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -594,6 +594,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretReminderRepeatDays: req.body.secretReminderRepeatDays }); if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Create + } + } + }); + return { approval: secretOperation.approval }; } @@ -730,6 +747,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Update + } + } + }); + return { approval: secretOperation.approval }; } const { secret } = secretOperation; @@ -831,6 +865,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { type: req.body.type }); if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Delete + } + } + }); + return { approval: secretOperation.approval }; } @@ -1165,7 +1216,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, - secretApprovalRequestSlug: approval.slug + secretApprovalRequestSlug: approval.slug, + secretPath, + environment, + eventType: SecretApprovalEvent.Create } } }); @@ -1351,7 +1405,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, - secretApprovalRequestSlug: approval.slug + secretApprovalRequestSlug: approval.slug, + secretPath, + environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Update } } }); @@ -1489,7 +1547,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, - secretApprovalRequestSlug: approval.slug + secretApprovalRequestSlug: approval.slug, + secretPath, + environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Delete } } }); @@ -1673,7 +1735,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, - secretApprovalRequestSlug: approval.slug + secretApprovalRequestSlug: approval.slug, + secretPath, + environment, + eventType: SecretApprovalEvent.CreateMany } } }); @@ -1801,7 +1866,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, - secretApprovalRequestSlug: approval.slug + secretApprovalRequestSlug: approval.slug, + secretPath, + environment, + eventType: SecretApprovalEvent.UpdateMany, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretName + })) } } }); @@ -1920,7 +1991,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, - secretApprovalRequestSlug: approval.slug + secretApprovalRequestSlug: approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretName + })), + eventType: SecretApprovalEvent.DeleteMany } } }); @@ -2038,6 +2115,24 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: inputSecrets }); if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey + })), + eventType: SecretApprovalEvent.CreateMany + } + } + }); return { approval: secretOperation.approval }; } const { secrets } = secretOperation; @@ -2170,6 +2265,25 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { mode: req.body.mode }); if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey, + secretPath: secret.secretPath + })), + eventType: SecretApprovalEvent.UpdateMany + } + } + }); return { approval: secretOperation.approval }; } const { secrets } = secretOperation; @@ -2298,6 +2412,25 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: inputSecrets }); if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey + })), + eventType: SecretApprovalEvent.DeleteMany + } + } + }); + return { approval: secretOperation.approval }; } const { secrets } = secretOperation;