feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
+117
View File
@@ -0,0 +1,117 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TWebhooks, TWebhooksUpdate } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TWebhookDALFactory = ReturnType<typeof webhookDALFactory>;
export const webhookDALFactory = (db: TDbClient) => {
const webhookOrm = ormify(db, TableName.Webhook);
const webhookFindQuery = (tx: Knex, filter: Partial<TWebhooks>) =>
tx(TableName.Webhook)
.where(filter)
.join(TableName.Environment, `${TableName.Webhook}.envId`, `${TableName.Environment}.id`)
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.Webhook));
const find = async (filter: Partial<TWebhooks>, tx?: Knex) => {
try {
const docs = await webhookFindQuery(tx || db, filter);
return docs.map(({ envId, envSlug, envName, ...el }) => ({
...el,
envId,
environment: {
id: envId,
slug: envSlug,
name: envName
}
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find by id webhook" });
}
};
const findOne = async (filter: Partial<TWebhooks>, tx?: Knex) => {
try {
const doc = await webhookFindQuery(tx || db, filter).first();
if (!doc) return;
const { envName: name, envSlug: slug, envId: id, ...el } = doc;
return { ...el, environment: { id, name, slug } };
} catch (error) {
throw new DatabaseError({ error, name: "Find one webhook" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await webhookFindQuery(tx || db, {
[`${TableName.Webhook}.id` as "id"]: id
}).first();
if (!doc) return;
const { envName: name, envSlug: slug, envId, ...el } = doc;
return { ...el, envId, environment: { id: envId, name, slug } };
} catch (error) {
throw new DatabaseError({ error, name: "Find by id webhook" });
}
};
const findAllWebhooks = async (
projectId: string,
environment?: string,
secretPath?: string,
tx?: Knex
) => {
try {
const webhooks = await (tx || db)(TableName.Webhook)
.where(`${TableName.Environment}.projectId`, projectId)
.where((qb) => {
if (environment) {
qb.where("slug", environment);
}
if (secretPath) {
qb.where("secretPath", secretPath);
}
})
.join(TableName.Environment, `${TableName.Webhook}.envId`, `${TableName.Environment}.id`)
.select(db.ref("name").withSchema(TableName.Environment).as("envName"))
.select(db.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(db.ref("id").withSchema(TableName.Environment).as("envId"))
.select(db.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.Webhook));
return webhooks.map(({ envId, envSlug, envName, ...el }) => ({
...el,
envId,
environment: {
id: envId,
slug: envSlug,
name: envName
}
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all webhooks" });
}
};
const bulkUpdate = async (data: Array<TWebhooksUpdate & { id: string }>, tx?: Knex) => {
try {
const queries = data.map(({ id, ...el }) =>
(tx || db)(TableName.Webhook).where({ id }).update(el)
);
const docs = await Promise.all(queries);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
return { ...webhookOrm, findById, findOne, find, findAllWebhooks, bulkUpdate };
};
+139
View File
@@ -0,0 +1,139 @@
import crypto from "node:crypto";
import picomatch from "picomatch";
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
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({
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({
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}`;
}
}
const req = await request.post(url, payload, {
headers,
timeout: WEBHOOK_TRIGGER_TIMEOUT,
signal: AbortSignal.timeout(WEBHOOK_TRIGGER_TIMEOUT)
});
return req;
};
export const getWebhookPayload = (
eventName: string,
workspaceId: string,
environment: string,
secretPath?: string
) => ({
event: eventName,
project: {
workspaceId,
environment,
secretPath
}
});
export type TFnTriggerWebhookDTO = {
projectId: string;
secretPath: string;
environment: string;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
};
// this is reusable function
// used in secret queue to trigger webhook and update status when secrets changes
export const fnTriggerWebhook = async ({
environment,
secretPath,
projectId,
webhookDAL,
projectEnvDAL
}: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter(
({ secretPath: hookSecretPath, isDisabled }) =>
!isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
);
if (!toBeTriggeredHooks.length) return;
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)
)
)
);
// filter hooks by status
const successWebhooks = webhooksTriggered
.filter(({ status }) => status === "fulfilled")
.map((_, i) => toBeTriggeredHooks[i].id);
const failedWebhooks = webhooksTriggered
.filter(({ status }) => status === "rejected")
.map((data, i) => ({
id: toBeTriggeredHooks[i].id,
error: data.status === "rejected" && data.reason.message
}));
await webhookDAL.transaction(async (tx) => {
const env = await projectEnvDAL.findOne({ projectId, slug: environment }, tx);
if (!env) throw new BadRequestError({ message: "Env not found" });
if (successWebhooks.length) {
await webhookDAL.update(
{ envId: env.id, $in: { id: successWebhooks } },
{ lastStatus: "success", lastRunErrorMessage: null },
tx
);
}
if (failedWebhooks.length) {
await webhookDAL.bulkUpdate(
failedWebhooks.map(({ id, error }) => ({
id,
lastRunErrorMessage: error,
lastStatus: "failed"
})),
tx
);
}
});
logger.info("Secret webhook job ended", { environment, secretPath, projectId });
};
@@ -0,0 +1,178 @@
import { ForbiddenError } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, TWebhooksInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { encryptSymmetric, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
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 {
TCreateWebhookDTO,
TDeleteWebhookDTO,
TListWebhookDTO,
TTestWebhookDTO,
TUpdateWebhookDTO
} from "./webhook-types";
type TWebhookServiceFactoryDep = {
webhookDAL: TWebhookDALFactory;
projectEnvDAL: TProjectEnvDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
export const webhookServiceFactory = ({
webhookDAL,
projectEnvDAL,
permissionService
}: TWebhookServiceFactoryDep) => {
const createWebhook = async ({
actor,
actorId,
projectId,
webhookUrl,
environment,
secretPath,
webhookSecretKey
}: TCreateWebhookDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
);
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Env not found" });
const insertDoc: TWebhooksInsert = {
url: webhookUrl,
envId: env.id,
isDisabled: false,
secretPath: secretPath || "/"
};
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;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM;
insertDoc.keyEncoding = SecretKeyEncoding.BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(
webhookSecretKey,
encryptionKey
);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM;
insertDoc.keyEncoding = SecretKeyEncoding.UTF8;
}
}
const webhook = await webhookDAL.create(insertDoc);
return { ...webhook, projectId, environment: env };
};
const updateWebhook = async ({ actorId, actor, id, isDisabled }: TUpdateWebhookDTO) => {
const webhook = await webhookDAL.findById(id);
if (!webhook) throw new BadRequestError({ message: "Webhook not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
webhook.projectId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
const updatedWebhook = await webhookDAL.updateById(id, { isDisabled });
return { ...webhook, ...updatedWebhook };
};
const deleteWebhook = async ({ id, actor, actorId }: TDeleteWebhookDTO) => {
const webhook = await webhookDAL.findById(id);
if (!webhook) throw new BadRequestError({ message: "Webhook not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
webhook.projectId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
const deletedWebhook = await webhookDAL.deleteById(id);
return { ...webhook, ...deletedWebhook };
};
const testWebhook = async ({ id, actor, actorId }: TTestWebhookDTO) => {
const webhook = await webhookDAL.findById(id);
if (!webhook) throw new BadRequestError({ message: "Webhook not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
webhook.projectId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
let webhookError: string | undefined;
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath)
);
} catch (err) {
webhookError = (err as Error).message;
}
const isSuccess = !webhookError;
const updatedWebhook = await webhookDAL.updateById(webhook.id, {
lastStatus: isSuccess ? "success" : "failed",
lastRunErrorMessage: isSuccess ? null : webhookError
});
return { ...webhook, ...updatedWebhook };
};
const listWebhooks = async ({
actorId,
actor,
projectId,
secretPath,
environment
}: TListWebhookDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
return webhookDAL.findAllWebhooks(projectId, environment, secretPath);
};
return {
createWebhook,
deleteWebhook,
listWebhooks,
updateWebhook,
testWebhook
};
};
@@ -0,0 +1,26 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateWebhookDTO = {
environment: string;
secretPath?: string;
webhookUrl: string;
webhookSecretKey?: string;
} & TProjectPermission;
export type TUpdateWebhookDTO = {
id: string;
isDisabled?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TTestWebhookDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteWebhookDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TListWebhookDTO = {
environment?: string;
secretPath?: string;
} & TProjectPermission;