feat: added native slack webhook type

This commit is contained in:
Sheen Capadngan
2024-07-02 19:57:58 +08:00
parent b8f65fc91a
commit ed5a7d72ab
7 changed files with 290 additions and 69 deletions
+98 -29
View File
@@ -12,47 +12,82 @@ import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const triggerWebhookRequest = async (
{ url, encryptedSecretKey, iv, tag, keyEncoding }: TWebhooks,
data: Record<string, unknown>
) => {
const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() };
const appCfg = getConfig();
if (encryptedSecretKey) {
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
let secretKey;
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
// case: encoding scheme is base64
secretKey = decryptSymmetric({
export const decryptWebhookDetails = (webhook: TWebhooks) => {
const appCfg = getConfig();
const { keyEncoding, iv, encryptedSecretKey, tag, encryptedUrl, urlIV, urlTag, url } = webhook;
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
let decryptedSecretKey = "";
let decryptedUrl = url;
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
// case: encoding scheme is base64
if (encryptedSecretKey) {
decryptedSecretKey = decryptSymmetric({
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string,
key: rootEncryptionKey
});
} else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
// case: encoding scheme is utf8
secretKey = decryptSymmetric128BitHexKeyUTF8({
}
if (encryptedUrl) {
decryptedUrl = decryptSymmetric({
ciphertext: encryptedUrl,
iv: urlIV as string,
tag: urlTag as string,
key: rootEncryptionKey
});
}
} else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
// case: encoding scheme is utf8
if (encryptedSecretKey) {
decryptedSecretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string,
key: encryptionKey
});
}
if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`;
if (encryptedUrl) {
decryptedUrl = decryptSymmetric128BitHexKeyUTF8({
ciphertext: encryptedUrl,
iv: urlIV as string,
tag: urlTag as string,
key: encryptionKey
});
}
}
return {
secretKey: decryptedSecretKey,
url: decryptedUrl
};
};
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => {
const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook);
if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`;
}
const req = await request.post(url, payload, {
headers,
timeout: WEBHOOK_TRIGGER_TIMEOUT,
signal: AbortSignal.timeout(WEBHOOK_TRIGGER_TIMEOUT)
});
return req;
};
@@ -60,15 +95,48 @@ export const getWebhookPayload = (
eventName: string,
workspaceId: string,
environment: string,
secretPath?: string
) => ({
event: eventName,
project: {
workspaceId,
environment,
secretPath
secretPath?: string,
type?: string | null
) => {
switch (type) {
case WebhookType.SLACK:
return {
text: "A secret value has been updated",
attachments: [
{
color: "#E7F256",
fields: [
{
title: "Workspace ID",
value: workspaceId,
short: true
},
{
title: "Environment",
value: environment,
short: true
},
{
title: "Secret Path",
value: secretPath,
short: true
}
]
}
]
};
case WebhookType.GENERAL:
default:
return {
event: eventName,
project: {
workspaceId,
environment,
secretPath
}
};
}
});
};
export type TFnTriggerWebhookDTO = {
projectId: string;
@@ -95,9 +163,10 @@ export const fnTriggerWebhook = async ({
logger.info("Secret webhook job started", { environment, secretPath, projectId });
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath))
triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath, hook.type))
)
);
// filter hooks by status
const successWebhooks = webhooksTriggered
.filter(({ status }) => status === "fulfilled")
@@ -9,7 +9,7 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import {
TCreateWebhookDTO,
TDeleteWebhookDTO,
@@ -36,8 +36,13 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
webhookUrl,
environment,
secretPath,
webhookSecretKey
webhookSecretKey,
type
}: TCreateWebhookDTO) => {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
@@ -50,15 +55,14 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
if (!env) throw new BadRequestError({ message: "Env not found" });
const insertDoc: TWebhooksInsert = {
url: webhookUrl,
url: "", // deprecated - we are moving away from plaintext URLs
envId: env.id,
isDisabled: false,
secretPath: secretPath || "/"
secretPath: secretPath || "/",
type
};
if (webhookSecretKey) {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric(webhookSecretKey, rootEncryptionKey);
insertDoc.encryptedSecretKey = ciphertext;
@@ -76,6 +80,22 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
}
}
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric(webhookUrl, rootEncryptionKey);
insertDoc.encryptedUrl = ciphertext;
insertDoc.urlIV = iv;
insertDoc.urlTag = tag;
insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM;
insertDoc.keyEncoding = SecretKeyEncoding.BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(webhookUrl, encryptionKey);
insertDoc.encryptedUrl = ciphertext;
insertDoc.urlIV = iv;
insertDoc.urlTag = tag;
insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM;
insertDoc.keyEncoding = SecretKeyEncoding.UTF8;
}
const webhook = await webhookDAL.create(insertDoc);
return { ...webhook, projectId, environment: env };
};
@@ -131,7 +151,7 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath)
getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath, webhook.type)
);
} catch (err) {
webhookError = (err as Error).message;
@@ -162,7 +182,14 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
return webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
return webhooks.map((w) => {
const { url } = decryptWebhookDetails(w);
return {
...w,
url
};
});
};
return {
@@ -5,6 +5,7 @@ export type TCreateWebhookDTO = {
secretPath?: string;
webhookUrl: string;
webhookSecretKey?: string;
type: string;
} & TProjectPermission;
export type TUpdateWebhookDTO = {
@@ -24,3 +25,8 @@ export type TListWebhookDTO = {
environment?: string;
secretPath?: string;
} & TProjectPermission;
export enum WebhookType {
GENERAL = "general",
SLACK = "slack"
}