From f5159583aee6213008003e1b24a2c425b0874ce7 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 28 Dec 2023 12:38:36 +0530 Subject: [PATCH] feat(infisical-pg: completed webhook and service token --- backend-pg/src/@types/knex.d.ts | 14 +- .../migrations/20231218092517_secret-tag.ts | 6 +- .../migrations/20231222172455_integration.ts | 2 +- .../20231225072545_service-token.ts | 32 ++++ .../db/migrations/20231225072552_webhook.ts | 32 ++++ backend-pg/src/db/schemas/index.ts | 2 + backend-pg/src/db/schemas/models.ts | 9 + backend-pg/src/db/schemas/secret-tags.ts | 4 +- backend-pg/src/db/schemas/service-tokens.ts | 29 +++ backend-pg/src/db/schemas/webhooks.ts | 29 +++ backend-pg/src/server/routes/index.ts | 5 +- .../services/integration/integration-dal.ts | 3 +- .../src/services/secret-tag/secret-tag-dal.ts | 48 +++++ .../services/secret-tag/secret-tag-service.ts | 74 ++++++++ .../services/secret-tag/secret-tag-types.ts | 13 ++ backend-pg/src/services/secret/secret-dal.ts | 38 +++- .../src/services/secret/secret-service.ts | 73 ++++++- .../src/services/secret/secret-types.ts | 4 + .../service-token/service-token-dal.ts | 10 + .../service-token/service-token-service.ts | 139 ++++++++++++++ .../service-token/service-token-types.ts | 19 ++ .../src/services/webhook/webhook-dal.ts | 94 +++++++++ .../src/services/webhook/webhook-fns.ts | 61 ++++++ .../src/services/webhook/webhook-service.ts | 179 ++++++++++++++++++ .../src/services/webhook/webhook-types.ts | 26 +++ 25 files changed, 927 insertions(+), 18 deletions(-) create mode 100644 backend-pg/src/db/migrations/20231225072545_service-token.ts create mode 100644 backend-pg/src/db/migrations/20231225072552_webhook.ts create mode 100644 backend-pg/src/db/schemas/service-tokens.ts create mode 100644 backend-pg/src/db/schemas/webhooks.ts create mode 100644 backend-pg/src/services/secret-tag/secret-tag-dal.ts create mode 100644 backend-pg/src/services/secret-tag/secret-tag-service.ts create mode 100644 backend-pg/src/services/secret-tag/secret-tag-types.ts create mode 100644 backend-pg/src/services/service-token/service-token-dal.ts create mode 100644 backend-pg/src/services/service-token/service-token-service.ts create mode 100644 backend-pg/src/services/service-token/service-token-types.ts create mode 100644 backend-pg/src/services/webhook/webhook-dal.ts create mode 100644 backend-pg/src/services/webhook/webhook-fns.ts create mode 100644 backend-pg/src/services/webhook/webhook-service.ts create mode 100644 backend-pg/src/services/webhook/webhook-types.ts diff --git a/backend-pg/src/@types/knex.d.ts b/backend-pg/src/@types/knex.d.ts index 1fff361a0..b380ebbed 100644 --- a/backend-pg/src/@types/knex.d.ts +++ b/backend-pg/src/@types/knex.d.ts @@ -70,6 +70,9 @@ import { TSecretVersions, TSecretVersionsInsert, TSecretVersionsUpdate, + TServiceTokens, + TServiceTokensInsert, + TServiceTokensUpdate, TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate, @@ -81,7 +84,10 @@ import { TUserEncryptionKeysUpdate, TUsers, TUsersInsert, - TUsersUpdate + TUsersUpdate, + TWebhooks, + TWebhooksInsert, + TWebhooksUpdate } from "@app/db/schemas"; declare module "knex/types/tables" { @@ -191,6 +197,12 @@ declare module "knex/types/tables" { TIntegrationsInsert, TIntegrationsUpdate >; + [TableName.Webhook]: Knex.CompositeTableType; + [TableName.ServiceToken]: Knex.CompositeTableType< + TServiceTokens, + TServiceTokensInsert, + TServiceTokensUpdate + >; [TableName.IntegrationAuth]: Knex.CompositeTableType< TIntegrationAuths, TIntegrationAuthsInsert, diff --git a/backend-pg/src/db/migrations/20231218092517_secret-tag.ts b/backend-pg/src/db/migrations/20231218092517_secret-tag.ts index 63cbc0daa..a1e000e6a 100644 --- a/backend-pg/src/db/migrations/20231218092517_secret-tag.ts +++ b/backend-pg/src/db/migrations/20231218092517_secret-tag.ts @@ -9,10 +9,10 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("name").notNullable(); t.string("slug").notNullable(); - t.string("tagColor").notNullable(); + t.string("color"); t.timestamps(true, true, true); - t.uuid("createdBy").notNullable(); - t.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("NO ACTION"); + t.uuid("createdBy"); + t.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("SET NULL"); t.uuid("projectId").notNullable(); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); }); diff --git a/backend-pg/src/db/migrations/20231222172455_integration.ts b/backend-pg/src/db/migrations/20231222172455_integration.ts index 346a97ff0..26e7c4a2d 100644 --- a/backend-pg/src/db/migrations/20231222172455_integration.ts +++ b/backend-pg/src/db/migrations/20231222172455_integration.ts @@ -64,8 +64,8 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists(TableName.IntegrationAuth); await knex.schema.dropTableIfExists(TableName.Integration); + await knex.schema.dropTableIfExists(TableName.IntegrationAuth); await dropOnUpdateTrigger(knex, TableName.IntegrationAuth); await dropOnUpdateTrigger(knex, TableName.Integration); } diff --git a/backend-pg/src/db/migrations/20231225072545_service-token.ts b/backend-pg/src/db/migrations/20231225072545_service-token.ts new file mode 100644 index 000000000..7e416800f --- /dev/null +++ b/backend-pg/src/db/migrations/20231225072545_service-token.ts @@ -0,0 +1,32 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ServiceToken))) { + await knex.schema.createTable(TableName.ServiceToken, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.jsonb("scopes").notNullable(); + t.specificType("permissions", "text[]").notNullable(); + t.datetime("lastUsed"); + t.datetime("expiresAt"); + t.text("secretHash").notNullable(); + t.text("encryptedKey"); + t.text("iv"); + t.text("tag"); + t.timestamps(true, true, true); + // user is old one + t.string("createdBy").notNullable(); + t.uuid("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } + await createOnUpdateTrigger(knex, TableName.ServiceToken); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ServiceToken); + await dropOnUpdateTrigger(knex, TableName.ServiceToken); +} diff --git a/backend-pg/src/db/migrations/20231225072552_webhook.ts b/backend-pg/src/db/migrations/20231225072552_webhook.ts new file mode 100644 index 000000000..f12f489e8 --- /dev/null +++ b/backend-pg/src/db/migrations/20231225072552_webhook.ts @@ -0,0 +1,32 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.Webhook))) { + await knex.schema.createTable(TableName.Webhook, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("secretPath").notNullable().defaultTo("/"); + t.string("url").notNullable(); + t.string("lastStatus"); + t.text("lastRunErrorMessage"); + t.boolean("isDisabled").defaultTo(false).notNullable(); + // webhook signature + t.text("encryptedSecretKey"); + t.text("iv"); + t.text("tag"); + t.string("algorithm"); + t.string("keyEncoding"); + t.timestamps(true, true, true); + t.uuid("envId").notNullable(); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + }); + } + await createOnUpdateTrigger(knex, TableName.Webhook); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.Webhook); + await dropOnUpdateTrigger(knex, TableName.Webhook); +} diff --git a/backend-pg/src/db/schemas/index.ts b/backend-pg/src/db/schemas/index.ts index 06f8741b3..94cca7613 100644 --- a/backend-pg/src/db/schemas/index.ts +++ b/backend-pg/src/db/schemas/index.ts @@ -22,7 +22,9 @@ export * from "./secret-tag-junction"; export * from "./secret-tags"; export * from "./secret-versions"; export * from "./secrets"; +export * from "./service-tokens"; export * from "./super-admin"; export * from "./user-actions"; export * from "./user-encryption-keys"; export * from "./users"; +export * from "./webhooks"; diff --git a/backend-pg/src/db/schemas/models.ts b/backend-pg/src/db/schemas/models.ts index d7c2a234e..fac7f6058 100644 --- a/backend-pg/src/db/schemas/models.ts +++ b/backend-pg/src/db/schemas/models.ts @@ -27,6 +27,8 @@ export enum TableName { SecretTag = "secret_tags", Integration = "integrations", IntegrationAuth = "integration_auths", + ServiceToken = "service_tokens", + Webhook = "webhooks", JnSecretTag = "secret_tag_junction", JnSecretVersionTag = "secret_version_tag_junction" } @@ -41,6 +43,13 @@ export const UserDeviceSchema = z .array() .default([]); +export const ServiceTokenScopes = z + .object({ + environment: z.string(), + secretPath: z.string().default("/") + }) + .array(); + export enum OrgMembershipRole { Admin = "admin", Member = "member", diff --git a/backend-pg/src/db/schemas/secret-tags.ts b/backend-pg/src/db/schemas/secret-tags.ts index 1da8a715a..73a72ba42 100644 --- a/backend-pg/src/db/schemas/secret-tags.ts +++ b/backend-pg/src/db/schemas/secret-tags.ts @@ -11,10 +11,10 @@ export const SecretTagsSchema = z.object({ id: z.string().uuid(), name: z.string(), slug: z.string(), - tagColor: z.string(), + color: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - createdBy: z.string().uuid(), + createdBy: z.string().uuid().nullable().optional(), projectId: z.string().uuid(), }); diff --git a/backend-pg/src/db/schemas/service-tokens.ts b/backend-pg/src/db/schemas/service-tokens.ts new file mode 100644 index 000000000..d559939c9 --- /dev/null +++ b/backend-pg/src/db/schemas/service-tokens.ts @@ -0,0 +1,29 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ServiceTokensSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + scopes: z.unknown(), + permissions: z.string().array(), + lastUsed: z.date().nullable().optional(), + expiresAt: z.date().nullable().optional(), + secretHash: z.string(), + encryptedKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + createdBy: z.string(), + projectId: z.string().uuid(), +}); + +export type TServiceTokens = z.infer; +export type TServiceTokensInsert = Omit; +export type TServiceTokensUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/webhooks.ts b/backend-pg/src/db/schemas/webhooks.ts new file mode 100644 index 000000000..2b7e36cab --- /dev/null +++ b/backend-pg/src/db/schemas/webhooks.ts @@ -0,0 +1,29 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const WebhooksSchema = z.object({ + id: z.string().uuid(), + secretPath: z.string().default('/'), + url: z.string(), + lastStatus: z.string().nullable().optional(), + lastRunErrorMessage: z.string().nullable().optional(), + isDisabled: z.boolean().default(false), + encryptedSecretKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + algorithm: z.string().nullable().optional(), + keyEncoding: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + envId: z.string().uuid(), +}); + +export type TWebhooks = z.infer; +export type TWebhooksInsert = Omit; +export type TWebhooksUpdate = Partial>; diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index 2462fe56d..f8915841c 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -42,6 +42,7 @@ import { secretFolderDalFactory } from "@app/services/secret-folder/secret-folde import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; import { secretImportDalFactory } from "@app/services/secret-import/secret-import-dal"; import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service"; +import { secretTagDalFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal"; import { superAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; @@ -76,6 +77,7 @@ export const registerRoutes = async ( const projectBotDal = projectBotDalFactory(db); const secretDal = secretDalFactory(db); + const secretTagDal = secretTagDalFactory(db); const folderDal = secretFolderDalFactory(db); const secretImportDal = secretImportDalFactory(db); const secretVersionDal = secretVersionDalFactory(db); @@ -156,7 +158,8 @@ export const registerRoutes = async ( secretVersionDal, secretBlindIndexDal, permissionService, - secretDal + secretDal, + secretTagDal }); const folderService = secretFolderServiceFactory({ permissionService, diff --git a/backend-pg/src/services/integration/integration-dal.ts b/backend-pg/src/services/integration/integration-dal.ts index 87faff272..1319068ef 100644 --- a/backend-pg/src/services/integration/integration-dal.ts +++ b/backend-pg/src/services/integration/integration-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName,TIntegrations } from "@app/db/schemas"; +import { TableName, TIntegrations } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -13,6 +13,7 @@ export const integrationDalFactory = (db: TDbClient) => { const integrationFindQuery = (tx: Knex, filter: Partial) => tx(TableName.Integration) .where(filter) + .join(TableName.Environment, `${TableName.Integration}.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")) diff --git a/backend-pg/src/services/secret-tag/secret-tag-dal.ts b/backend-pg/src/services/secret-tag/secret-tag-dal.ts new file mode 100644 index 000000000..12d8528d0 --- /dev/null +++ b/backend-pg/src/services/secret-tag/secret-tag-dal.ts @@ -0,0 +1,48 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TSecretTagDalFactory = ReturnType; + +export const secretTagDalFactory = (db: TDbClient) => { + const secretTagOrm = ormify(db, TableName.SecretTag); + const secretJnTagOrm = ormify(db, TableName.JnSecretTag); + + const findManyTagsById = async (projectId: string, ids: string[], tx?: Knex) => { + try { + const tags = await (tx || db)(TableName.SecretTag).where({ projectId }).whereIn("id", ids); + return tags; + } catch (error) { + throw new DatabaseError({ error, name: "Find all by ids" }); + } + }; + + const deleteTagsManySecret = async (projectId: string, secretIds: string[], tx?: Knex) => { + try { + const tags = await (tx || db)(TableName.JnSecretTag) + .join( + TableName.SecretTag, + `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .where("projectId", projectId) + .whereIn("secretsId", secretIds) + .delete() + .returning("*"); + return tags; + } catch (error) { + throw new DatabaseError({ error, name: "Find all by ids" }); + } + }; + + return { + ...secretTagOrm, + saveTagsToSecret: secretJnTagOrm.insertMany, + deleteTagsToSecret: secretJnTagOrm.delete, + deleteTagsManySecret, + findManyTagsById + }; +}; diff --git a/backend-pg/src/services/secret-tag/secret-tag-service.ts b/backend-pg/src/services/secret-tag/secret-tag-service.ts new file mode 100644 index 000000000..8b845e79f --- /dev/null +++ b/backend-pg/src/services/secret-tag/secret-tag-service.ts @@ -0,0 +1,74 @@ +import { ForbiddenError } from "@casl/ability"; + +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { + ProjectPermissionActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError } from "@app/lib/errors"; + +import { TSecretTagDalFactory } from "./secret-tag-dal"; +import { TCreateTagDTO, TDeleteTagDTO, TListProjectTagsDTO } from "./secret-tag-types"; + +type TSecretTagServiceFactoryDep = { + secretTagDal: TSecretTagDalFactory; + permissionService: Pick; +}; + +export type TSecretTagServiceFactory = ReturnType; + +export const secretTagServiceFactory = ({ + secretTagDal, + permissionService +}: TSecretTagServiceFactoryDep) => { + const createTag = async ({ name, slug, actor, color, actorId, projectId }: TCreateTagDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Tags + ); + + const existingTag = await secretTagDal.findOne({ slug }); + if (existingTag) throw new BadRequestError({ message: "Tag already exist" }); + + const newTag = await secretTagDal.create({ + projectId, + name, + slug, + color, + createdBy: actorId + }); + return newTag; + }; + + const deleteTag = async ({ actorId, actor, id }: TDeleteTagDTO) => { + const tag = await secretTagDal.findById(id); + if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + tag.projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Tags + ); + + const deletedTag = await secretTagDal.deleteById(tag.id); + return deletedTag; + }; + + const getProjectTags = async ({ actor, actorId, projectId }: TListProjectTagsDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Tags + ); + + const tags = await secretTagDal.find({ projectId }); + return tags; + }; + + return { createTag, deleteTag, getProjectTags }; +}; diff --git a/backend-pg/src/services/secret-tag/secret-tag-types.ts b/backend-pg/src/services/secret-tag/secret-tag-types.ts new file mode 100644 index 000000000..d2f027153 --- /dev/null +++ b/backend-pg/src/services/secret-tag/secret-tag-types.ts @@ -0,0 +1,13 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreateTagDTO = { + name: string; + color: string; + slug: string; +} & TProjectPermission; + +export type TDeleteTagDTO = { + id: string; +} & Omit; + +export type TListProjectTagsDTO = TProjectPermission; diff --git a/backend-pg/src/services/secret/secret-dal.ts b/backend-pg/src/services/secret/secret-dal.ts index 2e84bcc04..abf6a7728 100644 --- a/backend-pg/src/services/secret/secret-dal.ts +++ b/backend-pg/src/services/secret/secret-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { SecretType, TableName, TSecrets, TSecretsInsert,TSecretsUpdate } from "@app/db/schemas"; +import { SecretType, TableName, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { mergeOneToManyRelation, ormify, selectAllTableCols } from "@app/lib/knex"; export type TSecretDalFactory = ReturnType; @@ -33,6 +33,7 @@ export const secretDalFactory = (db: TDbClient) => { try { const secs = await (tx || db)(TableName.Secret) .insert(data as TSecretsInsert[]) + .increment("version", 1) .onConflict("id") .merge() .returning("*"); @@ -70,12 +71,39 @@ export const secretDalFactory = (db: TDbClient) => { const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => { try { - const sec = await (tx || db)(TableName.Secret) + const secs = await (tx || db)(TableName.Secret) .where({ folderId }) .where((bd) => { bd.whereNull("userId").orWhere({ userId: userId || null }); - }); - return sec; + }) + .join( + TableName.JnSecretTag, + `${TableName.Secret}.id`, + `${TableName.JnSecretTag}.${TableName.Secret}Id` + ) + .join( + TableName.SecretTag, + `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")); + const formatedSecs = mergeOneToManyRelation( + secs, + "id", + ({ tagColor, tagId, tagName, tagSlug, ...data }) => data, + ({ tagSlug: slug, tagName: name, tagId: id, tagColor: color }) => ({ + id, + slug, + name, + color + }), + "tags" + ); + return formatedSecs; } catch (error) { throw new DatabaseError({ error, name: "get all secret" }); } diff --git a/backend-pg/src/services/secret/secret-service.ts b/backend-pg/src/services/secret/secret-service.ts index 110cbf056..2fe2d3e0d 100644 --- a/backend-pg/src/services/secret/secret-service.ts +++ b/backend-pg/src/services/secret/secret-service.ts @@ -4,9 +4,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, + TableName, TSecretBlindIndexes, - TSecrets -} from "@app/db/schemas"; + TSecrets} from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -17,6 +17,7 @@ import { buildSecretBlindIndexFromName } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretTagDalFactory } from "../secret-tag/secret-tag-dal"; import { TSecretBlindIndexDalFactory } from "./secret-blind-index-dal"; import { TSecretDalFactory } from "./secret-dal"; import { @@ -33,6 +34,7 @@ import { TSecretVersionDalFactory } from "./secret-version-dal"; type TSecretServiceFactoryDep = { secretDal: TSecretDalFactory; + secretTagDal: TSecretTagDalFactory; secretVersionDal: TSecretVersionDalFactory; folderDal: TSecretFolderDalFactory; secretBlindIndexDal: TSecretBlindIndexDalFactory; @@ -43,6 +45,7 @@ export type TSecretServiceFactory = ReturnType; export const secretServiceFactory = ({ secretDal, + secretTagDal, secretVersionDal, folderDal, secretBlindIndexDal, @@ -108,6 +111,7 @@ export const secretServiceFactory = ({ if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; + // check if secret exist by finding the secret blindIndex const existingSecret = await secretDal.findOne({ secretBlindIndex, folderId, @@ -115,6 +119,7 @@ export const secretServiceFactory = ({ userId: inputSecret.type === SecretType.Personal ? actorId : null }); if (existingSecret) throw new BadRequestError({ message: "Secret already exist" }); + // if user creating personal check its shared also exist if (inputSecret.type === SecretType.Personal) { const sharedExist = await secretDal.findOne({ @@ -128,6 +133,14 @@ export const secretServiceFactory = ({ }); } + // validate tags + // fetch all tags and if not same count throw error meaning one was invalid tags + const tags = inputSecret.tags + ? await secretTagDal.findManyTagsById(projectId, inputSecret.tags) + : []; + if ((inputSecret.tags || []).length !== tags.length) + throw new BadRequestError({ message: "Tag not found" }); + const secret = await secretDal.transaction(async (tx) => { const { secretName, type, ...el } = inputSecret; const doc = await secretDal.create( @@ -143,6 +156,10 @@ export const secretServiceFactory = ({ }, tx ); + await secretTagDal.saveTagsToSecret( + tags.map(({ id }) => ({ secretsId: doc.id, secret_tagsId: id })), + tx + ); await secretVersionDal.create( { secretBlindIndex, @@ -161,7 +178,7 @@ export const secretServiceFactory = ({ }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot - return secret; + return { ...secret, tags }; }; const updateSecret = async ({ @@ -207,6 +224,12 @@ export const secretServiceFactory = ({ } } + const tags = inputSecret.tags + ? await secretTagDal.findManyTagsById(projectId, inputSecret.tags) + : []; + if ((inputSecret.tags || []).length !== tags.length) + throw new BadRequestError({ message: "Tag not found" }); + const updatedSecret = await secretDal.transaction(async (tx) => { const { secretName, ...el } = inputSecret; const [doc] = await secretDal.update( @@ -222,6 +245,12 @@ export const secretServiceFactory = ({ }, tx ); + // replace tags + await secretTagDal.deleteTagsToSecret({ secretsId: doc.id }, tx); + await secretTagDal.saveTagsToSecret( + tags.map(({ id }) => ({ secretsId: doc.id, secret_tagsId: id })), + tx + ); const { id, createdAt, updatedAt, ...newVersion } = doc; await secretVersionDal.create( { @@ -290,7 +319,6 @@ export const secretServiceFactory = ({ const folderId = folder.id; const secrets = await secretDal.findByFolderId(folderId, actorId); - return secrets; }; @@ -370,6 +398,10 @@ export const secretServiceFactory = ({ ); if (exists.length) throw new BadRequestError({ message: "Secret already exist" }); + // get all tags + const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags); + const tags = tagIds.length ? await secretTagDal.findManyTagsById(projectId, tagIds) : []; + const secrets = await secretDal.transaction(async (tx) => { const newSecrets = await secretDal.insertMany( inputSecrets.map(({ secretName, type, ...el }) => ({ @@ -384,6 +416,20 @@ export const secretServiceFactory = ({ })), tx ); + if (tags.length) { + await secretTagDal.saveTagsToSecret( + inputSecrets.flatMap(({ tags: secretTags = [], secretName }) => { + const secret = newSecrets.find( + ({ secretBlindIndex }) => secretBlindIndexes[secretName] === secretBlindIndex + ); + return secretTags.map((tag) => ({ + [`${TableName.SecretTag}Id`]: tag, + [`${TableName.Secret}Id`]: secret?.id || "" + })); + }), + tx + ); + } await secretVersionDal.insertMany( newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, @@ -478,6 +524,11 @@ export const secretServiceFactory = ({ {} ); + // get all tags + const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags); + const tags = tagIds.length ? await secretTagDal.findManyTagsById(projectId, tagIds) : []; + if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); + const secrets = await secretDal.transaction(async (tx) => { const newSecrets = await secretDal.bulkUpdate( inputSecrets.map(({ secretName, type, ...el }) => { @@ -500,6 +551,20 @@ export const secretServiceFactory = ({ }), tx ); + await secretTagDal.deleteTagsManySecret( + projectId, + newSecrets.map(({ id }) => id), + tx + ); + await secretTagDal.saveTagsToSecret( + inputSecrets.flatMap(({ secretName, tags: secretTags = [] }) => + secretTags.map((secretTag) => ({ + [`${TableName.Secret}Id`]: secretsGroupedByBlindIndex[secretName].id, + [`${TableName.SecretTag}Id`]: secretTag + })) + ), + tx + ); await secretVersionDal.insertMany( newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, diff --git a/backend-pg/src/services/secret/secret-types.ts b/backend-pg/src/services/secret/secret-types.ts index 163fef339..48ceb094e 100644 --- a/backend-pg/src/services/secret/secret-types.ts +++ b/backend-pg/src/services/secret/secret-types.ts @@ -18,6 +18,7 @@ export type TCreateSecretDTO = { skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + tags?: string[]; metadata?: { source?: string; }; @@ -35,6 +36,7 @@ export type TUpdateSecretDTO = { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; + tags?: string[]; secretCommentCiphertext?: string; secretCommentIV?: string; secretCommentTag?: string; @@ -77,6 +79,7 @@ export type TCreateBulkSecretDTO = { secretKeyTag: string; secretValueCiphertext: string; secretValueIV: string; + tags?: string[]; secretValueTag: string; secretCommentCiphertext?: string; secretCommentIV?: string; @@ -98,6 +101,7 @@ export type TUpdateBulkSecretDTO = { secretValueCiphertext?: string; secretValueIV?: string; secretValueTag?: string; + tags?: string[]; secretCommentCiphertext?: string; secretCommentIV?: string; secretCommentTag?: string; diff --git a/backend-pg/src/services/service-token/service-token-dal.ts b/backend-pg/src/services/service-token/service-token-dal.ts new file mode 100644 index 000000000..8821bc706 --- /dev/null +++ b/backend-pg/src/services/service-token/service-token-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TServiceTokenDalFactory = ReturnType; + +export const serviceTokenDalFactory = (db: TDbClient) => { + const stOrm = ormify(db, TableName.ServiceToken); + return stOrm; +}; diff --git a/backend-pg/src/services/service-token/service-token-service.ts b/backend-pg/src/services/service-token/service-token-service.ts new file mode 100644 index 000000000..4e1337d22 --- /dev/null +++ b/backend-pg/src/services/service-token/service-token-service.ts @@ -0,0 +1,139 @@ +import crypto from "node:crypto"; + +import { ForbiddenError } from "@casl/ability"; +import bcrypt from "bcrypt"; + +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 { BadRequestError } from "@app/lib/errors"; + +import { ActorType } from "../auth/auth-type"; +import { TProjectEnvDalFactory } from "../project-env/project-env-dal"; +import { TServiceTokenDalFactory } from "./service-token-dal"; +import { + TCreateServiceTokenDTO, + TDeleteServiceTokenDTO, + TGetServiceTokenInfoDTO, + TProjectServiceTokensDTO +} from "./service-token-types"; + +type TServiceTokenServiceFactoryDep = { + serviceTokenDal: TServiceTokenDalFactory; + permissionService: Pick; + projectEnvDal: Pick; +}; + +export type TServiceTokenServiceFactory = ReturnType; + +export const serviceTokenServiceFactory = ({ + serviceTokenDal, + permissionService, + projectEnvDal +}: TServiceTokenServiceFactoryDep) => { + const createServiceToken = async ({ + iv, + tag, + name, + actor, + scopes, + actorId, + projectId, + expiresIn, + permissions, + encryptedKey + }: TCreateServiceTokenDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.ServiceTokens + ); + const appCfg = getConfig(); + + // validates env + const scopeEnvs = [...new Set(scopes.map(({ environment }) => environment))]; + const inputEnvs = await projectEnvDal.findBySlugs(projectId, scopeEnvs); + if (inputEnvs.length !== scopeEnvs.length) + throw new BadRequestError({ message: "Environment not found" }); + + const secret = crypto.randomBytes(16).toString("hex"); + const secretHash = await bcrypt.hash(secret, appCfg.SALT_ROUNDS); + let expiresAt: Date | null = null; + if (expiresIn) { + expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } + const createdBy = actorId; + + const serviceToken = await serviceTokenDal.create({ + name, + createdBy, + encryptedKey, + iv, + tag, + expiresAt, + secretHash, + permissions, + scopes: JSON.stringify(scopes), + projectId + }); + + const token = `st.${serviceToken.id.toString()}.${secret}`; + // TODO(akhilmhdh-pg): audit log + + return { token, serviceToken }; + }; + + const deleteServiceToken = async ({ actorId, actor, id }: TDeleteServiceTokenDTO) => { + const serviceToken = await serviceTokenDal.findById(id); + if (!serviceToken) throw new BadRequestError({ message: "Token not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + serviceToken.projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.ServiceTokens + ); + + const deletedServiceToken = await serviceTokenDal.deleteById(id); + return deletedServiceToken; + }; + + const getServiceToken = async ({ actor, actorId }: TGetServiceTokenInfoDTO) => { + if (actor !== ActorType.SERVICE) + throw new BadRequestError({ message: "Service token not found" }); + + const serviceToken = await serviceTokenDal.findById(actorId); + if (!serviceToken) throw new BadRequestError({ message: "Token not found" }); + + return serviceToken; + }; + + const getProjectServiceTokens = async ({ + actorId, + actor, + projectId + }: TProjectServiceTokensDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.ServiceTokens + ); + + const tokens = await serviceTokenDal.find({ projectId }); + return tokens; + }; + + return { + createServiceToken, + deleteServiceToken, + getServiceToken, + getProjectServiceTokens + }; +}; diff --git a/backend-pg/src/services/service-token/service-token-types.ts b/backend-pg/src/services/service-token/service-token-types.ts new file mode 100644 index 000000000..b71286000 --- /dev/null +++ b/backend-pg/src/services/service-token/service-token-types.ts @@ -0,0 +1,19 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreateServiceTokenDTO = { + name: string; + scopes: Array<{ environment: string; secretPath: string }>; + encryptedKey: string; + iv: string; + tag: string; + expiresIn?: number | null; + permissions: ["read" | "write"]; +} & TProjectPermission; + +export type TGetServiceTokenInfoDTO = Omit; + +export type TDeleteServiceTokenDTO = { + id: string; +} & Omit; + +export type TProjectServiceTokensDTO = TProjectPermission; diff --git a/backend-pg/src/services/webhook/webhook-dal.ts b/backend-pg/src/services/webhook/webhook-dal.ts new file mode 100644 index 000000000..38bdd94f6 --- /dev/null +++ b/backend-pg/src/services/webhook/webhook-dal.ts @@ -0,0 +1,94 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName,TWebhooks } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TWebhookDalFactory = ReturnType; + +export const webhookDalFactory = (db: TDbClient) => { + const webhookOrm = ormify(db, TableName.Webhook); + + const webhookFindQuery = (tx: Knex, filter: Partial) => + 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.Integration)); + + const find = async (filter: Partial, tx?: Knex) => { + try { + const docs = await webhookFindQuery(tx || db, filter); + return docs.map(({ envId, envSlug, envName, ...el }) => ({ + ...el, + environment: { + id: envId, + slug: envSlug, + name: envName + } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find by id webhook" }); + } + }; + + const findOne = async (filter: Partial, 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, { id }).first(); + if (!doc) return; + + const { envName: name, envSlug: slug, envId, ...el } = doc; + return { ...el, 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.Integration)); + + return webhooks; + } catch (error) { + throw new DatabaseError({ error, name: "Find all webhooks" }); + } + }; + + return { ...webhookOrm, findById, findOne, find, findAllWebhooks }; +}; diff --git a/backend-pg/src/services/webhook/webhook-fns.ts b/backend-pg/src/services/webhook/webhook-fns.ts new file mode 100644 index 000000000..65a92f4f2 --- /dev/null +++ b/backend-pg/src/services/webhook/webhook-fns.ts @@ -0,0 +1,61 @@ +import crypto from "node:crypto"; + +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"; + +export const triggerWebhookRequest = async ( + { url, encryptedSecretKey, iv, tag, keyEncoding }: TWebhooks, + data: Record +) => { + const headers: Record = {}; + 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=${data.timestamp};${webhookSign}`; + } + } + const req = await request.post(url, payload, { headers }); + return req; +}; + +export const getWebhookPayload = ( + eventName: string, + workspaceId: string, + environment: string, + secretPath?: string +) => ({ + event: eventName, + project: { + workspaceId, + environment, + secretPath + } +}); diff --git a/backend-pg/src/services/webhook/webhook-service.ts b/backend-pg/src/services/webhook/webhook-service.ts new file mode 100644 index 000000000..75413129f --- /dev/null +++ b/backend-pg/src/services/webhook/webhook-service.ts @@ -0,0 +1,179 @@ +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; +}; + +export type TWebhookServiceFactory = ReturnType; + +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); + // TODO(akhilmhdh-pg): add audit log + return webhook; + }; + + 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 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 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 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 + }; +}; diff --git a/backend-pg/src/services/webhook/webhook-types.ts b/backend-pg/src/services/webhook/webhook-types.ts new file mode 100644 index 000000000..7a6e92c80 --- /dev/null +++ b/backend-pg/src/services/webhook/webhook-types.ts @@ -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; + +export type TTestWebhookDTO = { + id: string; +} & Omit; + +export type TDeleteWebhookDTO = { + id: string; +} & Omit; + +export type TListWebhookDTO = { + environment?: string; + secretPath?: string; +} & TProjectPermission;