From 6cd448b8a5d7a1d5558ff0cc88bad3d80496b65b Mon Sep 17 00:00:00 2001 From: = Date: Fri, 7 Mar 2025 15:01:14 +0530 Subject: [PATCH 1/2] feat: webhook on secret reminder trigger --- .../secret-approval-request-service.ts | 3 +- backend/src/queue/queue-service.ts | 3 +- backend/src/services/secret/secret-queue.ts | 34 +++++- backend/src/services/webhook/webhook-fns.ts | 113 +++++++++++++----- .../src/services/webhook/webhook-service.ts | 18 +-- backend/src/services/webhook/webhook-types.ts | 33 +++++ 6 files changed, 158 insertions(+), 46 deletions(-) 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 8569ef2a9..491579fc2 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 @@ -503,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({ if (!hasMinApproval && !isSoftEnforcement) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); - const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId); let mergeStatus; if (shouldUseSecretV2Bridge) { // this cycle if for bridged secrets @@ -861,7 +861,6 @@ export const secretApprovalRequestServiceFactory = ({ if (isSoftEnforcement) { const cfg = getConfig(); - const project = await projectDAL.findProjectById(projectId); const env = await projectEnvDAL.findOne({ id: policy.envId }); const requestedByUser = await userDAL.findOne({ id: actorId }); const approverUsers = await userDAL.find({ diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index f9aec5881..5d6b8b60b 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -21,6 +21,7 @@ import { TQueueSecretSyncSyncSecretsByIdDTO, TQueueSendSecretSyncActionFailedNotificationsDTO } from "@app/services/secret-sync/secret-sync-types"; +import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; export enum QueueName { SecretRotation = "secret-rotation", @@ -107,7 +108,7 @@ export type TQueueJobTypes = { }; [QueueName.SecretWebhook]: { name: QueueJobs.SecWebhook; - payload: { projectId: string; environment: string; secretPath: string; depth?: number }; + payload: TWebhookPayloads; }; [QueueName.AccessTokenStatusUpdate]: diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 00b0e7da8..c84fe5ae0 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { fnTriggerWebhook } from "../webhook/webhook-fns"; +import { WebhookEvents } from "../webhook/webhook-types"; import { TSecretDALFactory } from "./secret-dal"; import { interpolateSecrets } from "./secret-fns"; import { @@ -623,7 +624,14 @@ export const secretQueueFactory = ({ await queueService.queue( QueueName.SecretWebhook, QueueJobs.SecWebhook, - { environment, projectId, secretPath }, + { + type: WebhookEvents.SecretModified, + payload: { + environment, + projectId, + secretPath + } + }, { jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`, removeOnFail: { count: 5 }, @@ -1055,6 +1063,8 @@ export const secretQueueFactory = ({ const organization = await orgDAL.findOrgByProjectId(projectId); const project = await projectDAL.findById(projectId); + const secret = await secretV2BridgeDAL.findById(data.secretId); + const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]); if (!organization) { logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`); @@ -1083,6 +1093,19 @@ export const secretQueueFactory = ({ organizationName: organization.name } }); + + await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, { + type: WebhookEvents.SecretReminderExpired, + payload: { + projectName: project.name, + projectId: project.id, + secretPath: folder?.path, + environment: folder?.environmentSlug || "", + reminderNote: data.note, + secretName: secret?.key, + secretId: data.secretId + } + }); }); const startSecretV2Migration = async (projectId: string) => { @@ -1490,14 +1513,17 @@ export const secretQueueFactory = ({ queueService.start(QueueName.SecretWebhook, async (job) => { const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, - projectId: job.data.projectId + projectId: job.data.payload.projectId }); await fnTriggerWebhook({ - ...job.data, + projectId: job.data.payload.projectId, + environment: job.data.payload.environment, + secretPath: job.data.payload.secretPath || "/", projectEnvDAL, - webhookDAL, projectDAL, + webhookDAL, + event: job.data, secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() }); }); diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index e46f9db2a..a16158e14 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; -import { WebhookType } from "./webhook-types"; +import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; @@ -54,29 +54,64 @@ export const triggerWebhookRequest = async ( return req; }; -export const getWebhookPayload = ( - eventName: string, - details: { - workspaceName: string; - workspaceId: string; - environment: string; - secretPath?: string; - type?: string | null; +export const getWebhookPayload = (event: TWebhookPayloads) => { + if (event.type === WebhookEvents.SecretModified) { + const { projectName, projectId, environment, secretPath, type } = event.payload; + + switch (type) { + case WebhookType.SLACK: + return { + text: "A secret value has been added or modified.", + attachments: [ + { + color: "#E7F256", + fields: [ + { + title: "Project", + value: projectName, + short: false + }, + { + title: "Environment", + value: environment, + short: false + }, + { + title: "Secret Path", + value: secretPath, + short: false + } + ] + } + ] + }; + case WebhookType.GENERAL: + default: + return { + event: event.type, + project: { + workspaceId: projectId, + projectName, + environment, + secretPath + } + }; + } } -) => { - const { workspaceName, workspaceId, environment, secretPath, type } = details; + + const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload; switch (type) { case WebhookType.SLACK: return { - text: "A secret value has been added or modified.", + text: "You have a secret reminder", attachments: [ { color: "#E7F256", fields: [ { title: "Project", - value: workspaceName, + value: projectName, short: false }, { @@ -88,6 +123,16 @@ export const getWebhookPayload = ( title: "Secret Path", value: secretPath, short: false + }, + { + title: "Secret Name", + value: secretName, + short: false + }, + { + title: "Reminder Note", + value: reminderNote, + short: false } ] } @@ -96,11 +141,14 @@ export const getWebhookPayload = ( case WebhookType.GENERAL: default: return { - event: eventName, + event: event.type, project: { - workspaceId, + workspaceId: projectId, + projectName, environment, - secretPath + secretPath, + secretName, + reminderNote } }; } @@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = { projectId: string; secretPath: string; environment: string; + event: TWebhookPayloads; webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; @@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL, - secretManagerDecryptor + event, + secretManagerDecryptor, + projectDAL }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({ ); if (!toBeTriggeredHooks.length) return; logger.info({ environment, secretPath, projectId }, "Secret webhook job started"); - const project = await projectDAL.findById(projectId); + let { projectName } = event.payload; + if (!projectName) { + const project = await projectDAL.findById(event.payload.projectId); + projectName = project.name; + } + const webhooksTriggered = await Promise.allSettled( - toBeTriggeredHooks.map((hook) => - triggerWebhookRequest( - hook, - secretManagerDecryptor, - getWebhookPayload("secrets.modified", { - workspaceName: project.name, - workspaceId: projectId, - environment, - secretPath, - type: hook.type - }) - ) - ) + toBeTriggeredHooks.map((hook) => { + const formattedEvent = { + type: event.type, + payload: { ...event.payload, type: hook.type, projectName } + } as TWebhookPayloads; + return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent)); + }) ); // filter hooks by status diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index bb078e0f1..c555dc8d1 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -16,7 +16,8 @@ import { TDeleteWebhookDTO, TListWebhookDTO, TTestWebhookDTO, - TUpdateWebhookDTO + TUpdateWebhookDTO, + WebhookEvents } from "./webhook-types"; type TWebhookServiceFactoryDep = { @@ -144,12 +145,15 @@ export const webhookServiceFactory = ({ await triggerWebhookRequest( webhook, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), - getWebhookPayload("test", { - workspaceName: project.name, - workspaceId: webhook.projectId, - environment: webhook.environment.slug, - secretPath: webhook.secretPath, - type: webhook.type + getWebhookPayload({ + type: "test" as WebhookEvents.SecretModified, + payload: { + projectName: project.name, + projectId: webhook.projectId, + environment: webhook.environment.slug, + secretPath: webhook.secretPath, + type: webhook.type + } }) ); } catch (err) { diff --git a/backend/src/services/webhook/webhook-types.ts b/backend/src/services/webhook/webhook-types.ts index 40dacb42a..8ce2c8d8e 100644 --- a/backend/src/services/webhook/webhook-types.ts +++ b/backend/src/services/webhook/webhook-types.ts @@ -30,3 +30,36 @@ export enum WebhookType { GENERAL = "general", SLACK = "slack" } + +export enum WebhookEvents { + SecretModified = "secrets.modified", + SecretReminderExpired = "secrets.reminder-expired", + TestEvent = "test" +} + +type TWebhookSecretModifiedEventPayload = { + type: WebhookEvents.SecretModified; + payload: { + projectName?: string; + projectId: string; + environment: string; + secretPath?: string; + type?: string | null; + }; +}; + +type TWebhookSecretReminderEventPayload = { + type: WebhookEvents.SecretReminderExpired; + payload: { + projectName?: string; + projectId: string; + environment: string; + secretPath?: string; + type?: string | null; + secretName: string; + secretId: string; + reminderNote?: string | null; + }; +}; + +export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload; From a55b26164a6d9179ed747aac9937fe01d7d8e527 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 7 Mar 2025 15:14:09 +0530 Subject: [PATCH 2/2] feat: updated doc --- docs/documentation/platform/webhooks.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx index dc3a71b27..92d3ff8b8 100644 --- a/docs/documentation/platform/webhooks.mdx +++ b/docs/documentation/platform/webhooks.mdx @@ -36,3 +36,18 @@ If the signature in the header matches the signature that you generated, then yo "timestamp": "" } ``` + +```json +{ + "event": "secrets.reminder-expired", + "project": { + "workspaceId": "the workspace id", + "environment": "project environment", + "secretPath": "project folder path", + "secretName": "name of the secret", + "secretId": "id of the secret", + "reminderNote": "reminder note of the secret" + }, + "timestamp": "" +} +```