mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 23:27:14 +00:00
feat(infisical-pg: completed webhook and service token
This commit is contained in:
Vendored
+13
-1
@@ -70,6 +70,9 @@ import {
|
|||||||
TSecretVersions,
|
TSecretVersions,
|
||||||
TSecretVersionsInsert,
|
TSecretVersionsInsert,
|
||||||
TSecretVersionsUpdate,
|
TSecretVersionsUpdate,
|
||||||
|
TServiceTokens,
|
||||||
|
TServiceTokensInsert,
|
||||||
|
TServiceTokensUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@@ -81,7 +84,10 @@ import {
|
|||||||
TUserEncryptionKeysUpdate,
|
TUserEncryptionKeysUpdate,
|
||||||
TUsers,
|
TUsers,
|
||||||
TUsersInsert,
|
TUsersInsert,
|
||||||
TUsersUpdate
|
TUsersUpdate,
|
||||||
|
TWebhooks,
|
||||||
|
TWebhooksInsert,
|
||||||
|
TWebhooksUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
|
||||||
declare module "knex/types/tables" {
|
declare module "knex/types/tables" {
|
||||||
@@ -191,6 +197,12 @@ declare module "knex/types/tables" {
|
|||||||
TIntegrationsInsert,
|
TIntegrationsInsert,
|
||||||
TIntegrationsUpdate
|
TIntegrationsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.Webhook]: Knex.CompositeTableType<TWebhooks, TWebhooksInsert, TWebhooksUpdate>;
|
||||||
|
[TableName.ServiceToken]: Knex.CompositeTableType<
|
||||||
|
TServiceTokens,
|
||||||
|
TServiceTokensInsert,
|
||||||
|
TServiceTokensUpdate
|
||||||
|
>;
|
||||||
[TableName.IntegrationAuth]: Knex.CompositeTableType<
|
[TableName.IntegrationAuth]: Knex.CompositeTableType<
|
||||||
TIntegrationAuths,
|
TIntegrationAuths,
|
||||||
TIntegrationAuthsInsert,
|
TIntegrationAuthsInsert,
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.string("slug").notNullable();
|
t.string("slug").notNullable();
|
||||||
t.string("tagColor").notNullable();
|
t.string("color");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
t.uuid("createdBy").notNullable();
|
t.uuid("createdBy");
|
||||||
t.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("NO ACTION");
|
t.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
t.uuid("projectId").notNullable();
|
t.uuid("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
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> {
|
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.Integration);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IntegrationAuth);
|
||||||
await dropOnUpdateTrigger(knex, TableName.IntegrationAuth);
|
await dropOnUpdateTrigger(knex, TableName.IntegrationAuth);
|
||||||
await dropOnUpdateTrigger(knex, TableName.Integration);
|
await dropOnUpdateTrigger(knex, TableName.Integration);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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-tags";
|
||||||
export * from "./secret-versions";
|
export * from "./secret-versions";
|
||||||
export * from "./secrets";
|
export * from "./secrets";
|
||||||
|
export * from "./service-tokens";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./user-actions";
|
export * from "./user-actions";
|
||||||
export * from "./user-encryption-keys";
|
export * from "./user-encryption-keys";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
|
export * from "./webhooks";
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export enum TableName {
|
|||||||
SecretTag = "secret_tags",
|
SecretTag = "secret_tags",
|
||||||
Integration = "integrations",
|
Integration = "integrations",
|
||||||
IntegrationAuth = "integration_auths",
|
IntegrationAuth = "integration_auths",
|
||||||
|
ServiceToken = "service_tokens",
|
||||||
|
Webhook = "webhooks",
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
JnSecretVersionTag = "secret_version_tag_junction"
|
JnSecretVersionTag = "secret_version_tag_junction"
|
||||||
}
|
}
|
||||||
@@ -41,6 +43,13 @@ export const UserDeviceSchema = z
|
|||||||
.array()
|
.array()
|
||||||
.default([]);
|
.default([]);
|
||||||
|
|
||||||
|
export const ServiceTokenScopes = z
|
||||||
|
.object({
|
||||||
|
environment: z.string(),
|
||||||
|
secretPath: z.string().default("/")
|
||||||
|
})
|
||||||
|
.array();
|
||||||
|
|
||||||
export enum OrgMembershipRole {
|
export enum OrgMembershipRole {
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ export const SecretTagsSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
tagColor: z.string(),
|
color: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
createdBy: z.string().uuid(),
|
createdBy: z.string().uuid().nullable().optional(),
|
||||||
projectId: z.string().uuid(),
|
projectId: z.string().uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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 { secretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
import { secretImportDalFactory } from "@app/services/secret-import/secret-import-dal";
|
import { secretImportDalFactory } from "@app/services/secret-import/secret-import-dal";
|
||||||
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
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 { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
import { superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -76,6 +77,7 @@ export const registerRoutes = async (
|
|||||||
const projectBotDal = projectBotDalFactory(db);
|
const projectBotDal = projectBotDalFactory(db);
|
||||||
|
|
||||||
const secretDal = secretDalFactory(db);
|
const secretDal = secretDalFactory(db);
|
||||||
|
const secretTagDal = secretTagDalFactory(db);
|
||||||
const folderDal = secretFolderDalFactory(db);
|
const folderDal = secretFolderDalFactory(db);
|
||||||
const secretImportDal = secretImportDalFactory(db);
|
const secretImportDal = secretImportDalFactory(db);
|
||||||
const secretVersionDal = secretVersionDalFactory(db);
|
const secretVersionDal = secretVersionDalFactory(db);
|
||||||
@@ -156,7 +158,8 @@ export const registerRoutes = async (
|
|||||||
secretVersionDal,
|
secretVersionDal,
|
||||||
secretBlindIndexDal,
|
secretBlindIndexDal,
|
||||||
permissionService,
|
permissionService,
|
||||||
secretDal
|
secretDal,
|
||||||
|
secretTagDal
|
||||||
});
|
});
|
||||||
const folderService = secretFolderServiceFactory({
|
const folderService = secretFolderServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export const integrationDalFactory = (db: TDbClient) => {
|
|||||||
const integrationFindQuery = (tx: Knex, filter: Partial<TIntegrations>) =>
|
const integrationFindQuery = (tx: Knex, filter: Partial<TIntegrations>) =>
|
||||||
tx(TableName.Integration)
|
tx(TableName.Integration)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
|
.join(TableName.Environment, `${TableName.Integration}.envId`, `${TableName.Environment}.id`)
|
||||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -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 { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { 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>;
|
export type TSecretDalFactory = ReturnType<typeof secretDalFactory>;
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ export const secretDalFactory = (db: TDbClient) => {
|
|||||||
try {
|
try {
|
||||||
const secs = await (tx || db)(TableName.Secret)
|
const secs = await (tx || db)(TableName.Secret)
|
||||||
.insert(data as TSecretsInsert[])
|
.insert(data as TSecretsInsert[])
|
||||||
|
.increment("version", 1)
|
||||||
.onConflict("id")
|
.onConflict("id")
|
||||||
.merge()
|
.merge()
|
||||||
.returning("*");
|
.returning("*");
|
||||||
@@ -70,12 +71,39 @@ export const secretDalFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => {
|
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const sec = await (tx || db)(TableName.Secret)
|
const secs = await (tx || db)(TableName.Secret)
|
||||||
.where({ folderId })
|
.where({ folderId })
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
bd.whereNull("userId").orWhere({ userId: userId || null });
|
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) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "get all secret" });
|
throw new DatabaseError({ error, name: "get all secret" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
SecretEncryptionAlgo,
|
SecretEncryptionAlgo,
|
||||||
SecretKeyEncoding,
|
SecretKeyEncoding,
|
||||||
SecretType,
|
SecretType,
|
||||||
|
TableName,
|
||||||
TSecretBlindIndexes,
|
TSecretBlindIndexes,
|
||||||
TSecrets
|
TSecrets} from "@app/db/schemas";
|
||||||
} from "@app/db/schemas";
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
@@ -17,6 +17,7 @@ import { buildSecretBlindIndexFromName } from "@app/lib/crypto";
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretTagDalFactory } from "../secret-tag/secret-tag-dal";
|
||||||
import { TSecretBlindIndexDalFactory } from "./secret-blind-index-dal";
|
import { TSecretBlindIndexDalFactory } from "./secret-blind-index-dal";
|
||||||
import { TSecretDalFactory } from "./secret-dal";
|
import { TSecretDalFactory } from "./secret-dal";
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ import { TSecretVersionDalFactory } from "./secret-version-dal";
|
|||||||
|
|
||||||
type TSecretServiceFactoryDep = {
|
type TSecretServiceFactoryDep = {
|
||||||
secretDal: TSecretDalFactory;
|
secretDal: TSecretDalFactory;
|
||||||
|
secretTagDal: TSecretTagDalFactory;
|
||||||
secretVersionDal: TSecretVersionDalFactory;
|
secretVersionDal: TSecretVersionDalFactory;
|
||||||
folderDal: TSecretFolderDalFactory;
|
folderDal: TSecretFolderDalFactory;
|
||||||
secretBlindIndexDal: TSecretBlindIndexDalFactory;
|
secretBlindIndexDal: TSecretBlindIndexDalFactory;
|
||||||
@@ -43,6 +45,7 @@ export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
|||||||
|
|
||||||
export const secretServiceFactory = ({
|
export const secretServiceFactory = ({
|
||||||
secretDal,
|
secretDal,
|
||||||
|
secretTagDal,
|
||||||
secretVersionDal,
|
secretVersionDal,
|
||||||
folderDal,
|
folderDal,
|
||||||
secretBlindIndexDal,
|
secretBlindIndexDal,
|
||||||
@@ -108,6 +111,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
// check if secret exist by finding the secret blindIndex
|
||||||
const existingSecret = await secretDal.findOne({
|
const existingSecret = await secretDal.findOne({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
folderId,
|
folderId,
|
||||||
@@ -115,6 +119,7 @@ export const secretServiceFactory = ({
|
|||||||
userId: inputSecret.type === SecretType.Personal ? actorId : null
|
userId: inputSecret.type === SecretType.Personal ? actorId : null
|
||||||
});
|
});
|
||||||
if (existingSecret) throw new BadRequestError({ message: "Secret already exist" });
|
if (existingSecret) throw new BadRequestError({ message: "Secret already exist" });
|
||||||
|
|
||||||
// if user creating personal check its shared also exist
|
// if user creating personal check its shared also exist
|
||||||
if (inputSecret.type === SecretType.Personal) {
|
if (inputSecret.type === SecretType.Personal) {
|
||||||
const sharedExist = await secretDal.findOne({
|
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 secret = await secretDal.transaction(async (tx) => {
|
||||||
const { secretName, type, ...el } = inputSecret;
|
const { secretName, type, ...el } = inputSecret;
|
||||||
const doc = await secretDal.create(
|
const doc = await secretDal.create(
|
||||||
@@ -143,6 +156,10 @@ export const secretServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await secretTagDal.saveTagsToSecret(
|
||||||
|
tags.map(({ id }) => ({ secretsId: doc.id, secret_tagsId: id })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
await secretVersionDal.create(
|
await secretVersionDal.create(
|
||||||
{
|
{
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
@@ -161,7 +178,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||||
return secret;
|
return { ...secret, tags };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSecret = async ({
|
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 updatedSecret = await secretDal.transaction(async (tx) => {
|
||||||
const { secretName, ...el } = inputSecret;
|
const { secretName, ...el } = inputSecret;
|
||||||
const [doc] = await secretDal.update(
|
const [doc] = await secretDal.update(
|
||||||
@@ -222,6 +245,12 @@ export const secretServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
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;
|
const { id, createdAt, updatedAt, ...newVersion } = doc;
|
||||||
await secretVersionDal.create(
|
await secretVersionDal.create(
|
||||||
{
|
{
|
||||||
@@ -290,7 +319,6 @@ export const secretServiceFactory = ({
|
|||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
|
|
||||||
const secrets = await secretDal.findByFolderId(folderId, actorId);
|
const secrets = await secretDal.findByFolderId(folderId, actorId);
|
||||||
|
|
||||||
return secrets;
|
return secrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -370,6 +398,10 @@ export const secretServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (exists.length) throw new BadRequestError({ message: "Secret already exist" });
|
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 secrets = await secretDal.transaction(async (tx) => {
|
||||||
const newSecrets = await secretDal.insertMany(
|
const newSecrets = await secretDal.insertMany(
|
||||||
inputSecrets.map(({ secretName, type, ...el }) => ({
|
inputSecrets.map(({ secretName, type, ...el }) => ({
|
||||||
@@ -384,6 +416,20 @@ export const secretServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
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(
|
await secretVersionDal.insertMany(
|
||||||
newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...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 secrets = await secretDal.transaction(async (tx) => {
|
||||||
const newSecrets = await secretDal.bulkUpdate(
|
const newSecrets = await secretDal.bulkUpdate(
|
||||||
inputSecrets.map(({ secretName, type, ...el }) => {
|
inputSecrets.map(({ secretName, type, ...el }) => {
|
||||||
@@ -500,6 +551,20 @@ export const secretServiceFactory = ({
|
|||||||
}),
|
}),
|
||||||
tx
|
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(
|
await secretVersionDal.insertMany(
|
||||||
newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type TCreateSecretDTO = {
|
|||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
secretReminderRepeatDays?: number | null;
|
secretReminderRepeatDays?: number | null;
|
||||||
secretReminderNote?: string | null;
|
secretReminderNote?: string | null;
|
||||||
|
tags?: string[];
|
||||||
metadata?: {
|
metadata?: {
|
||||||
source?: string;
|
source?: string;
|
||||||
};
|
};
|
||||||
@@ -35,6 +36,7 @@ export type TUpdateSecretDTO = {
|
|||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
|
tags?: string[];
|
||||||
secretCommentCiphertext?: string;
|
secretCommentCiphertext?: string;
|
||||||
secretCommentIV?: string;
|
secretCommentIV?: string;
|
||||||
secretCommentTag?: string;
|
secretCommentTag?: string;
|
||||||
@@ -77,6 +79,7 @@ export type TCreateBulkSecretDTO = {
|
|||||||
secretKeyTag: string;
|
secretKeyTag: string;
|
||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
|
tags?: string[];
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
secretCommentCiphertext?: string;
|
secretCommentCiphertext?: string;
|
||||||
secretCommentIV?: string;
|
secretCommentIV?: string;
|
||||||
@@ -98,6 +101,7 @@ export type TUpdateBulkSecretDTO = {
|
|||||||
secretValueCiphertext?: string;
|
secretValueCiphertext?: string;
|
||||||
secretValueIV?: string;
|
secretValueIV?: string;
|
||||||
secretValueTag?: string;
|
secretValueTag?: string;
|
||||||
|
tags?: string[];
|
||||||
secretCommentCiphertext?: string;
|
secretCommentCiphertext?: string;
|
||||||
secretCommentIV?: string;
|
secretCommentIV?: string;
|
||||||
secretCommentTag?: string;
|
secretCommentTag?: string;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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