mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 01:25:47 +00:00
feat: webhook on secret reminder trigger
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user