Skip to content

Commit 2e02f8b

Browse files
authored
Merge pull request #3199 from akhilmhdh/feat/webhook-reminder
Added webhook trigger for secret reminder
2 parents 8203158 + a55b261 commit 2e02f8b

File tree

7 files changed

+173
-46
lines changed

7 files changed

+173
-46
lines changed

backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
503503
if (!hasMinApproval && !isSoftEnforcement)
504504
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
505505

506-
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
506+
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
507507
let mergeStatus;
508508
if (shouldUseSecretV2Bridge) {
509509
// this cycle if for bridged secrets
@@ -861,7 +861,6 @@ export const secretApprovalRequestServiceFactory = ({
861861

862862
if (isSoftEnforcement) {
863863
const cfg = getConfig();
864-
const project = await projectDAL.findProjectById(projectId);
865864
const env = await projectEnvDAL.findOne({ id: policy.envId });
866865
const requestedByUser = await userDAL.findOne({ id: actorId });
867866
const approverUsers = await userDAL.find({

backend/src/queue/queue-service.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
TQueueSecretSyncSyncSecretsByIdDTO,
2222
TQueueSendSecretSyncActionFailedNotificationsDTO
2323
} from "@app/services/secret-sync/secret-sync-types";
24+
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
2425

2526
export enum QueueName {
2627
SecretRotation = "secret-rotation",
@@ -107,7 +108,7 @@ export type TQueueJobTypes = {
107108
};
108109
[QueueName.SecretWebhook]: {
109110
name: QueueJobs.SecWebhook;
110-
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
111+
payload: TWebhookPayloads;
111112
};
112113

113114
[QueueName.AccessTokenStatusUpdate]:

backend/src/services/secret/secret-queue.ts

+30-4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
6161
import { TUserDALFactory } from "../user/user-dal";
6262
import { TWebhookDALFactory } from "../webhook/webhook-dal";
6363
import { fnTriggerWebhook } from "../webhook/webhook-fns";
64+
import { WebhookEvents } from "../webhook/webhook-types";
6465
import { TSecretDALFactory } from "./secret-dal";
6566
import { interpolateSecrets } from "./secret-fns";
6667
import {
@@ -623,7 +624,14 @@ export const secretQueueFactory = ({
623624
await queueService.queue(
624625
QueueName.SecretWebhook,
625626
QueueJobs.SecWebhook,
626-
{ environment, projectId, secretPath },
627+
{
628+
type: WebhookEvents.SecretModified,
629+
payload: {
630+
environment,
631+
projectId,
632+
secretPath
633+
}
634+
},
627635
{
628636
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
629637
removeOnFail: { count: 5 },
@@ -1055,6 +1063,8 @@ export const secretQueueFactory = ({
10551063

10561064
const organization = await orgDAL.findOrgByProjectId(projectId);
10571065
const project = await projectDAL.findById(projectId);
1066+
const secret = await secretV2BridgeDAL.findById(data.secretId);
1067+
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
10581068

10591069
if (!organization) {
10601070
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
@@ -1083,6 +1093,19 @@ export const secretQueueFactory = ({
10831093
organizationName: organization.name
10841094
}
10851095
});
1096+
1097+
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
1098+
type: WebhookEvents.SecretReminderExpired,
1099+
payload: {
1100+
projectName: project.name,
1101+
projectId: project.id,
1102+
secretPath: folder?.path,
1103+
environment: folder?.environmentSlug || "",
1104+
reminderNote: data.note,
1105+
secretName: secret?.key,
1106+
secretId: data.secretId
1107+
}
1108+
});
10861109
});
10871110

10881111
const startSecretV2Migration = async (projectId: string) => {
@@ -1490,14 +1513,17 @@ export const secretQueueFactory = ({
14901513
queueService.start(QueueName.SecretWebhook, async (job) => {
14911514
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
14921515
type: KmsDataKey.SecretManager,
1493-
projectId: job.data.projectId
1516+
projectId: job.data.payload.projectId
14941517
});
14951518

14961519
await fnTriggerWebhook({
1497-
...job.data,
1520+
projectId: job.data.payload.projectId,
1521+
environment: job.data.payload.environment,
1522+
secretPath: job.data.payload.secretPath || "/",
14981523
projectEnvDAL,
1499-
webhookDAL,
15001524
projectDAL,
1525+
webhookDAL,
1526+
event: job.data,
15011527
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
15021528
});
15031529
});

backend/src/services/webhook/webhook-fns.ts

+81-32
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
1111
import { TProjectDALFactory } from "../project/project-dal";
1212
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
1313
import { TWebhookDALFactory } from "./webhook-dal";
14-
import { WebhookType } from "./webhook-types";
14+
import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
1515

1616
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
1717

@@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
5454
return req;
5555
};
5656

57-
export const getWebhookPayload = (
58-
eventName: string,
59-
details: {
60-
workspaceName: string;
61-
workspaceId: string;
62-
environment: string;
63-
secretPath?: string;
64-
type?: string | null;
57+
export const getWebhookPayload = (event: TWebhookPayloads) => {
58+
if (event.type === WebhookEvents.SecretModified) {
59+
const { projectName, projectId, environment, secretPath, type } = event.payload;
60+
61+
switch (type) {
62+
case WebhookType.SLACK:
63+
return {
64+
text: "A secret value has been added or modified.",
65+
attachments: [
66+
{
67+
color: "#E7F256",
68+
fields: [
69+
{
70+
title: "Project",
71+
value: projectName,
72+
short: false
73+
},
74+
{
75+
title: "Environment",
76+
value: environment,
77+
short: false
78+
},
79+
{
80+
title: "Secret Path",
81+
value: secretPath,
82+
short: false
83+
}
84+
]
85+
}
86+
]
87+
};
88+
case WebhookType.GENERAL:
89+
default:
90+
return {
91+
event: event.type,
92+
project: {
93+
workspaceId: projectId,
94+
projectName,
95+
environment,
96+
secretPath
97+
}
98+
};
99+
}
65100
}
66-
) => {
67-
const { workspaceName, workspaceId, environment, secretPath, type } = details;
101+
102+
const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
68103

69104
switch (type) {
70105
case WebhookType.SLACK:
71106
return {
72-
text: "A secret value has been added or modified.",
107+
text: "You have a secret reminder",
73108
attachments: [
74109
{
75110
color: "#E7F256",
76111
fields: [
77112
{
78113
title: "Project",
79-
value: workspaceName,
114+
value: projectName,
80115
short: false
81116
},
82117
{
@@ -88,6 +123,16 @@ export const getWebhookPayload = (
88123
title: "Secret Path",
89124
value: secretPath,
90125
short: false
126+
},
127+
{
128+
title: "Secret Name",
129+
value: secretName,
130+
short: false
131+
},
132+
{
133+
title: "Reminder Note",
134+
value: reminderNote,
135+
short: false
91136
}
92137
]
93138
}
@@ -96,11 +141,14 @@ export const getWebhookPayload = (
96141
case WebhookType.GENERAL:
97142
default:
98143
return {
99-
event: eventName,
144+
event: event.type,
100145
project: {
101-
workspaceId,
146+
workspaceId: projectId,
147+
projectName,
102148
environment,
103-
secretPath
149+
secretPath,
150+
secretName,
151+
reminderNote
104152
}
105153
};
106154
}
@@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
110158
projectId: string;
111159
secretPath: string;
112160
environment: string;
161+
event: TWebhookPayloads;
113162
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
114163
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
115164
projectDAL: Pick<TProjectDALFactory, "findById">;
@@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
124173
projectId,
125174
webhookDAL,
126175
projectEnvDAL,
127-
projectDAL,
128-
secretManagerDecryptor
176+
event,
177+
secretManagerDecryptor,
178+
projectDAL
129179
}: TFnTriggerWebhookDTO) => {
130180
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
131181
const toBeTriggeredHooks = webhooks.filter(
@@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
134184
);
135185
if (!toBeTriggeredHooks.length) return;
136186
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
137-
const project = await projectDAL.findById(projectId);
187+
let { projectName } = event.payload;
188+
if (!projectName) {
189+
const project = await projectDAL.findById(event.payload.projectId);
190+
projectName = project.name;
191+
}
192+
138193
const webhooksTriggered = await Promise.allSettled(
139-
toBeTriggeredHooks.map((hook) =>
140-
triggerWebhookRequest(
141-
hook,
142-
secretManagerDecryptor,
143-
getWebhookPayload("secrets.modified", {
144-
workspaceName: project.name,
145-
workspaceId: projectId,
146-
environment,
147-
secretPath,
148-
type: hook.type
149-
})
150-
)
151-
)
194+
toBeTriggeredHooks.map((hook) => {
195+
const formattedEvent = {
196+
type: event.type,
197+
payload: { ...event.payload, type: hook.type, projectName }
198+
} as TWebhookPayloads;
199+
return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
200+
})
152201
);
153202

154203
// filter hooks by status

backend/src/services/webhook/webhook-service.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
TDeleteWebhookDTO,
1717
TListWebhookDTO,
1818
TTestWebhookDTO,
19-
TUpdateWebhookDTO
19+
TUpdateWebhookDTO,
20+
WebhookEvents
2021
} from "./webhook-types";
2122

2223
type TWebhookServiceFactoryDep = {
@@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
144145
await triggerWebhookRequest(
145146
webhook,
146147
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
147-
getWebhookPayload("test", {
148-
workspaceName: project.name,
149-
workspaceId: webhook.projectId,
150-
environment: webhook.environment.slug,
151-
secretPath: webhook.secretPath,
152-
type: webhook.type
148+
getWebhookPayload({
149+
type: "test" as WebhookEvents.SecretModified,
150+
payload: {
151+
projectName: project.name,
152+
projectId: webhook.projectId,
153+
environment: webhook.environment.slug,
154+
secretPath: webhook.secretPath,
155+
type: webhook.type
156+
}
153157
})
154158
);
155159
} catch (err) {

backend/src/services/webhook/webhook-types.ts

+33
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,36 @@ export enum WebhookType {
3030
GENERAL = "general",
3131
SLACK = "slack"
3232
}
33+
34+
export enum WebhookEvents {
35+
SecretModified = "secrets.modified",
36+
SecretReminderExpired = "secrets.reminder-expired",
37+
TestEvent = "test"
38+
}
39+
40+
type TWebhookSecretModifiedEventPayload = {
41+
type: WebhookEvents.SecretModified;
42+
payload: {
43+
projectName?: string;
44+
projectId: string;
45+
environment: string;
46+
secretPath?: string;
47+
type?: string | null;
48+
};
49+
};
50+
51+
type TWebhookSecretReminderEventPayload = {
52+
type: WebhookEvents.SecretReminderExpired;
53+
payload: {
54+
projectName?: string;
55+
projectId: string;
56+
environment: string;
57+
secretPath?: string;
58+
type?: string | null;
59+
secretName: string;
60+
secretId: string;
61+
reminderNote?: string | null;
62+
};
63+
};
64+
65+
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;

docs/documentation/platform/webhooks.mdx

+15
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,18 @@ If the signature in the header matches the signature that you generated, then yo
3636
"timestamp": ""
3737
}
3838
```
39+
40+
```json
41+
{
42+
"event": "secrets.reminder-expired",
43+
"project": {
44+
"workspaceId": "the workspace id",
45+
"environment": "project environment",
46+
"secretPath": "project folder path",
47+
"secretName": "name of the secret",
48+
"secretId": "id of the secret",
49+
"reminderNote": "reminder note of the secret"
50+
},
51+
"timestamp": ""
52+
}
53+
```

0 commit comments

Comments
 (0)