mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg: completed webhook and service token
This commit is contained in:
14
backend-pg/src/@types/knex.d.ts
vendored
14
backend-pg/src/@types/knex.d.ts
vendored
@@ -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<TWebhooks, TWebhooksInsert, TWebhooksUpdate>;
|
||||
[TableName.ServiceToken]: Knex.CompositeTableType<
|
||||
TServiceTokens,
|
||||
TServiceTokensInsert,
|
||||
TServiceTokensUpdate
|
||||
>;
|
||||
[TableName.IntegrationAuth]: Knex.CompositeTableType<
|
||||
TIntegrationAuths,
|
||||
TIntegrationAuthsInsert,
|
||||
|
||||
@@ -9,10 +9,10 @@ export async function up(knex: Knex): Promise<void> {
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -64,8 +64,8 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
32
backend-pg/src/db/migrations/20231225072545_service-token.ts
Normal file
32
backend-pg/src/db/migrations/20231225072545_service-token.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ServiceToken);
|
||||
await dropOnUpdateTrigger(knex, TableName.ServiceToken);
|
||||
}
|
||||
32
backend-pg/src/db/migrations/20231225072552_webhook.ts
Normal file
32
backend-pg/src/db/migrations/20231225072552_webhook.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.Webhook);
|
||||
await dropOnUpdateTrigger(knex, TableName.Webhook);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
29
backend-pg/src/db/schemas/service-tokens.ts
Normal file
29
backend-pg/src/db/schemas/service-tokens.ts
Normal file
@@ -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<typeof ServiceTokensSchema>;
|
||||
export type TServiceTokensInsert = Omit<TServiceTokens, TImmutableDBKeys>;
|
||||
export type TServiceTokensUpdate = Partial<Omit<TServiceTokens, TImmutableDBKeys>>;
|
||||
29
backend-pg/src/db/schemas/webhooks.ts
Normal file
29
backend-pg/src/db/schemas/webhooks.ts
Normal file
@@ -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<typeof WebhooksSchema>;
|
||||
export type TWebhooksInsert = Omit<TWebhooks, TImmutableDBKeys>;
|
||||
export type TWebhooksUpdate = Partial<Omit<TWebhooks, TImmutableDBKeys>>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TIntegrations>) =>
|
||||
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"))
|
||||
|
||||
48
backend-pg/src/services/secret-tag/secret-tag-dal.ts
Normal file
48
backend-pg/src/services/secret-tag/secret-tag-dal.ts
Normal file
@@ -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<typeof secretTagDalFactory>;
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
74
backend-pg/src/services/secret-tag/secret-tag-service.ts
Normal file
74
backend-pg/src/services/secret-tag/secret-tag-service.ts
Normal file
@@ -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<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
|
||||
|
||||
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 };
|
||||
};
|
||||
13
backend-pg/src/services/secret-tag/secret-tag-types.ts
Normal file
13
backend-pg/src/services/secret-tag/secret-tag-types.ts
Normal file
@@ -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<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListProjectTagsDTO = TProjectPermission;
|
||||
@@ -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<typeof secretDalFactory>;
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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<typeof secretServiceFactory>;
|
||||
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
10
backend-pg/src/services/service-token/service-token-dal.ts
Normal file
10
backend-pg/src/services/service-token/service-token-dal.ts
Normal file
@@ -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<typeof serviceTokenDalFactory>;
|
||||
|
||||
export const serviceTokenDalFactory = (db: TDbClient) => {
|
||||
const stOrm = ormify(db, TableName.ServiceToken);
|
||||
return stOrm;
|
||||
};
|
||||
139
backend-pg/src/services/service-token/service-token-service.ts
Normal file
139
backend-pg/src/services/service-token/service-token-service.ts
Normal file
@@ -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<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectEnvDal: Pick<TProjectEnvDalFactory, "findBySlugs">;
|
||||
};
|
||||
|
||||
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
19
backend-pg/src/services/service-token/service-token-types.ts
Normal file
19
backend-pg/src/services/service-token/service-token-types.ts
Normal file
@@ -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<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteServiceTokenDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TProjectServiceTokensDTO = TProjectPermission;
|
||||
94
backend-pg/src/services/webhook/webhook-dal.ts
Normal file
94
backend-pg/src/services/webhook/webhook-dal.ts
Normal file
@@ -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<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.Integration));
|
||||
|
||||
const find = async (filter: Partial<TWebhooks>, 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<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, { 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 };
|
||||
};
|
||||
61
backend-pg/src/services/webhook/webhook-fns.ts
Normal file
61
backend-pg/src/services/webhook/webhook-fns.ts
Normal file
@@ -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<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=${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
|
||||
}
|
||||
});
|
||||
179
backend-pg/src/services/webhook/webhook-service.ts
Normal file
179
backend-pg/src/services/webhook/webhook-service.ts
Normal file
@@ -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<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);
|
||||
// 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
|
||||
};
|
||||
};
|
||||
26
backend-pg/src/services/webhook/webhook-types.ts
Normal file
26
backend-pg/src/services/webhook/webhook-types.ts
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user