feat: webhook on secret reminder trigger

This commit is contained in:
=
2025-03-07 15:01:14 +05:30
parent 706feafbf2
commit 6cd448b8a5
6 changed files with 158 additions and 46 deletions
@@ -503,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!hasMinApproval && !isSoftEnforcement) if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); 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; let mergeStatus;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
// this cycle if for bridged secrets // this cycle if for bridged secrets
@@ -861,7 +861,6 @@ export const secretApprovalRequestServiceFactory = ({
if (isSoftEnforcement) { if (isSoftEnforcement) {
const cfg = getConfig(); const cfg = getConfig();
const project = await projectDAL.findProjectById(projectId);
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
const requestedByUser = await userDAL.findOne({ id: actorId }); const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
+2 -1
View File
@@ -21,6 +21,7 @@ import {
TQueueSecretSyncSyncSecretsByIdDTO, TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types"; } from "@app/services/secret-sync/secret-sync-types";
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
export enum QueueName { export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@@ -107,7 +108,7 @@ export type TQueueJobTypes = {
}; };
[QueueName.SecretWebhook]: { [QueueName.SecretWebhook]: {
name: QueueJobs.SecWebhook; name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string; depth?: number }; payload: TWebhookPayloads;
}; };
[QueueName.AccessTokenStatusUpdate]: [QueueName.AccessTokenStatusUpdate]:
+30 -4
View File
@@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { WebhookEvents } from "../webhook/webhook-types";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { interpolateSecrets } from "./secret-fns"; import { interpolateSecrets } from "./secret-fns";
import { import {
@@ -623,7 +624,14 @@ export const secretQueueFactory = ({
await queueService.queue( await queueService.queue(
QueueName.SecretWebhook, QueueName.SecretWebhook,
QueueJobs.SecWebhook, QueueJobs.SecWebhook,
{ environment, projectId, secretPath }, {
type: WebhookEvents.SecretModified,
payload: {
environment,
projectId,
secretPath
}
},
{ {
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`, jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
removeOnFail: { count: 5 }, removeOnFail: { count: 5 },
@@ -1055,6 +1063,8 @@ export const secretQueueFactory = ({
const organization = await orgDAL.findOrgByProjectId(projectId); const organization = await orgDAL.findOrgByProjectId(projectId);
const project = await projectDAL.findById(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) { if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`); logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
@@ -1083,6 +1093,19 @@ export const secretQueueFactory = ({
organizationName: organization.name 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) => { const startSecretV2Migration = async (projectId: string) => {
@@ -1490,14 +1513,17 @@ export const secretQueueFactory = ({
queueService.start(QueueName.SecretWebhook, async (job) => { queueService.start(QueueName.SecretWebhook, async (job) => {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: job.data.projectId projectId: job.data.payload.projectId
}); });
await fnTriggerWebhook({ await fnTriggerWebhook({
...job.data, projectId: job.data.payload.projectId,
environment: job.data.payload.environment,
secretPath: job.data.payload.secretPath || "/",
projectEnvDAL, projectEnvDAL,
webhookDAL,
projectDAL, projectDAL,
webhookDAL,
event: job.data,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
}); });
}); });
+81 -32
View File
@@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal"; import { TWebhookDALFactory } from "./webhook-dal";
import { WebhookType } from "./webhook-types"; import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
@@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
return req; return req;
}; };
export const getWebhookPayload = ( export const getWebhookPayload = (event: TWebhookPayloads) => {
eventName: string, if (event.type === WebhookEvents.SecretModified) {
details: { const { projectName, projectId, environment, secretPath, type } = event.payload;
workspaceName: string;
workspaceId: string; switch (type) {
environment: string; case WebhookType.SLACK:
secretPath?: string; return {
type?: string | null; 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) { switch (type) {
case WebhookType.SLACK: case WebhookType.SLACK:
return { return {
text: "A secret value has been added or modified.", text: "You have a secret reminder",
attachments: [ attachments: [
{ {
color: "#E7F256", color: "#E7F256",
fields: [ fields: [
{ {
title: "Project", title: "Project",
value: workspaceName, value: projectName,
short: false short: false
}, },
{ {
@@ -88,6 +123,16 @@ export const getWebhookPayload = (
title: "Secret Path", title: "Secret Path",
value: secretPath, value: secretPath,
short: false 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: case WebhookType.GENERAL:
default: default:
return { return {
event: eventName, event: event.type,
project: { project: {
workspaceId, workspaceId: projectId,
projectName,
environment, environment,
secretPath secretPath,
secretName,
reminderNote
} }
}; };
} }
@@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
projectId: string; projectId: string;
secretPath: string; secretPath: string;
environment: string; environment: string;
event: TWebhookPayloads;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">; webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
@@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
projectId, projectId,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL, event,
secretManagerDecryptor secretManagerDecryptor,
projectDAL
}: TFnTriggerWebhookDTO) => { }: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter( const toBeTriggeredHooks = webhooks.filter(
@@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
); );
if (!toBeTriggeredHooks.length) return; if (!toBeTriggeredHooks.length) return;
logger.info({ environment, secretPath, projectId }, "Secret webhook job started"); 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( const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) => toBeTriggeredHooks.map((hook) => {
triggerWebhookRequest( const formattedEvent = {
hook, type: event.type,
secretManagerDecryptor, payload: { ...event.payload, type: hook.type, projectName }
getWebhookPayload("secrets.modified", { } as TWebhookPayloads;
workspaceName: project.name, return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
workspaceId: projectId, })
environment,
secretPath,
type: hook.type
})
)
)
); );
// filter hooks by status // filter hooks by status
@@ -16,7 +16,8 @@ import {
TDeleteWebhookDTO, TDeleteWebhookDTO,
TListWebhookDTO, TListWebhookDTO,
TTestWebhookDTO, TTestWebhookDTO,
TUpdateWebhookDTO TUpdateWebhookDTO,
WebhookEvents
} from "./webhook-types"; } from "./webhook-types";
type TWebhookServiceFactoryDep = { type TWebhookServiceFactoryDep = {
@@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
await triggerWebhookRequest( await triggerWebhookRequest(
webhook, webhook,
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
getWebhookPayload("test", { getWebhookPayload({
workspaceName: project.name, type: "test" as WebhookEvents.SecretModified,
workspaceId: webhook.projectId, payload: {
environment: webhook.environment.slug, projectName: project.name,
secretPath: webhook.secretPath, projectId: webhook.projectId,
type: webhook.type environment: webhook.environment.slug,
secretPath: webhook.secretPath,
type: webhook.type
}
}) })
); );
} catch (err) { } catch (err) {
@@ -30,3 +30,36 @@ export enum WebhookType {
GENERAL = "general", GENERAL = "general",
SLACK = "slack" 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;