mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): completed secret approval policy and services for approval requests
This commit is contained in:
30
backend-pg/package-lock.json
generated
30
backend-pg/package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"knex": "^3.0.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.7",
|
||||
"ora": "^7.0.1",
|
||||
"passport-github": "^1.1.0",
|
||||
@@ -5959,10 +5960,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz",
|
||||
"integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5970,10 +5970,10 @@
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
@@ -6772,6 +6772,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"knex": "^3.0.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.7",
|
||||
"ora": "^7.0.1",
|
||||
"passport-github": "^1.1.0",
|
||||
|
||||
2
backend-pg/src/@types/fastify.d.ts
vendored
2
backend-pg/src/@types/fastify.d.ts
vendored
@@ -23,6 +23,7 @@ import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key
|
||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/services/secret-approval-policy/secret-approval-policy-service";
|
||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||
@@ -90,6 +91,7 @@ declare module "fastify" {
|
||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||
identityProject: TIdentityProjectServiceFactory;
|
||||
identityUa: TIdentityUaServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
};
|
||||
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
|
||||
48
backend-pg/src/@types/knex.d.ts
vendored
48
backend-pg/src/@types/knex.d.ts
vendored
@@ -67,6 +67,24 @@ import {
|
||||
TProjects,
|
||||
TProjectsInsert,
|
||||
TProjectsUpdate,
|
||||
TSapApprovers,
|
||||
TSapApproversInsert,
|
||||
TSapApproversUpdate,
|
||||
TSaRequestSecrets,
|
||||
TSaRequestSecretsInsert,
|
||||
TSaRequestSecretsUpdate,
|
||||
TSaRequestSecretTags,
|
||||
TSaRequestSecretTagsInsert,
|
||||
TSaRequestSecretTagsUpdate,
|
||||
TSarReviewers,
|
||||
TSarReviewersInsert,
|
||||
TSarReviewersUpdate,
|
||||
TSecretApprovalPolicies,
|
||||
TSecretApprovalPoliciesInsert,
|
||||
TSecretApprovalPoliciesUpdate,
|
||||
TSecretApprovalRequests,
|
||||
TSecretApprovalRequestsInsert,
|
||||
TSecretApprovalRequestsUpdate,
|
||||
TSecretBlindIndexes,
|
||||
TSecretBlindIndexesInsert,
|
||||
TSecretBlindIndexesUpdate,
|
||||
@@ -256,6 +274,36 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectMembershipsInsert,
|
||||
TIdentityProjectMembershipsUpdate
|
||||
>;
|
||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||
TSecretApprovalPolicies,
|
||||
TSecretApprovalPoliciesInsert,
|
||||
TSecretApprovalPoliciesUpdate
|
||||
>;
|
||||
[TableName.SapApprover]: Knex.CompositeTableType<
|
||||
TSapApprovers,
|
||||
TSapApproversInsert,
|
||||
TSapApproversUpdate
|
||||
>;
|
||||
[TableName.SecretApprovalRequest]: Knex.CompositeTableType<
|
||||
TSecretApprovalRequests,
|
||||
TSecretApprovalRequestsInsert,
|
||||
TSecretApprovalRequestsUpdate
|
||||
>;
|
||||
[TableName.SarReviewer]: Knex.CompositeTableType<
|
||||
TSarReviewers,
|
||||
TSarReviewersInsert,
|
||||
TSarReviewersUpdate
|
||||
>;
|
||||
[TableName.SarSecret]: Knex.CompositeTableType<
|
||||
TSaRequestSecrets,
|
||||
TSaRequestSecretsInsert,
|
||||
TSaRequestSecretsUpdate
|
||||
>;
|
||||
[TableName.SarSecretTag]: Knex.CompositeTableType<
|
||||
TSaRequestSecretTags,
|
||||
TSaRequestSecretTagsInsert,
|
||||
TSaRequestSecretTagsUpdate
|
||||
>;
|
||||
// Junction tables
|
||||
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
||||
TSecretTagJunction,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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.SecretApprovalPolicy))) {
|
||||
await knex.schema.createTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.string("secretPath");
|
||||
t.integer("approvals").defaultTo(1).notNullable();
|
||||
t.uuid("envId").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicy);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SapApprover))) {
|
||||
await knex.schema.createTable(TableName.SapApprover, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("approverId").notNullable();
|
||||
t.foreign("approverId")
|
||||
.references("id")
|
||||
.inTable(TableName.ProjectMembership)
|
||||
.onDelete("CASCADE");
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId")
|
||||
.references("id")
|
||||
.inTable(TableName.SecretApprovalPolicy)
|
||||
.onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SapApprover);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SapApprover);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicy);
|
||||
await dropOnUpdateTrigger(knex, TableName.SapApprover);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicy);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Knex } from "knex";
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequest))) {
|
||||
await knex.schema.createTable(TableName.SecretApprovalRequest, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("policyId").notNullable();
|
||||
t.boolean("hasMerged").defaultTo(false).notNullable();
|
||||
t.string("status").defaultTo("open").notNullable();
|
||||
t.jsonb("conflicts");
|
||||
t.foreign("policyId")
|
||||
.references("id")
|
||||
.inTable(TableName.SecretApprovalPolicy)
|
||||
.onDelete("CASCADE");
|
||||
t.string("slug").notNullable();
|
||||
t.uuid("folderId").notNullable();
|
||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||
t.uuid("statusChangeBy");
|
||||
t.foreign("statusChangeBy")
|
||||
.references("id")
|
||||
.inTable(TableName.ProjectMembership)
|
||||
.onDelete("CASCADE");
|
||||
t.uuid("committerId").notNullable();
|
||||
t.foreign("committerId")
|
||||
.references("id")
|
||||
.inTable(TableName.ProjectMembership)
|
||||
.onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SecretApprovalRequest);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SarReviewer))) {
|
||||
await knex.schema.createTable(TableName.SarReviewer, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("member").notNullable();
|
||||
t.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
t.string("status").notNullable();
|
||||
t.uuid("requestId").notNullable();
|
||||
t.foreign("requestId")
|
||||
.references("id")
|
||||
.inTable(TableName.SecretApprovalRequest)
|
||||
.onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SarReviewer);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SarSecret))) {
|
||||
await knex.schema.createTable(TableName.SarSecret, (t) => {
|
||||
// everything related to secret
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.integer("version").defaultTo(1);
|
||||
t.text("secretBlindIndex").notNullable();
|
||||
t.text("secretKeyCiphertext").notNullable();
|
||||
t.text("secretKeyIV").notNullable();
|
||||
t.text("secretKeyTag").notNullable();
|
||||
t.text("secretValueCiphertext").notNullable();
|
||||
t.text("secretValueIV").notNullable(); // symmetric encryption
|
||||
t.text("secretValueTag").notNullable();
|
||||
t.text("secretCommentCiphertext");
|
||||
t.text("secretCommentIV");
|
||||
t.text("secretCommentTag");
|
||||
t.string("secretReminderNotice");
|
||||
t.integer("secretReminderRepeatDays");
|
||||
t.boolean("skipMultilineEncoding").defaultTo(false);
|
||||
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
||||
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
||||
t.jsonb("metadata");
|
||||
t.timestamps(true, true, true);
|
||||
// commit details
|
||||
t.uuid("requestId").notNullable();
|
||||
t.foreign("requestId")
|
||||
.references("id")
|
||||
.inTable(TableName.SecretApprovalRequest)
|
||||
.onDelete("CASCADE");
|
||||
t.string("op").notNullable();
|
||||
t.uuid("secretId");
|
||||
t.foreign("secretId").references("id").inTable(TableName.Secret).onDelete("SET NULL");
|
||||
t.uuid("secretVersion");
|
||||
t.foreign("secretVersion")
|
||||
.references("id")
|
||||
.inTable(TableName.SecretVersion)
|
||||
.onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SarSecret);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SarSecretTag))) {
|
||||
await knex.schema.createTable(TableName.SarSecretTag, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("secretId").notNullable();
|
||||
t.foreign("secretId").references("id").inTable(TableName.SarSecret).onDelete("CASCADE");
|
||||
t.uuid("tagId").notNullable();
|
||||
t.foreign("tagId").references("id").inTable(TableName.SecretTag).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SarSecretTag);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretTag);
|
||||
await knex.schema.dropTableIfExists(TableName.SarSecret);
|
||||
await knex.schema.dropTableIfExists(TableName.SarReviewer);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequest);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SarSecretTag);
|
||||
await dropOnUpdateTrigger(knex, TableName.SarSecret);
|
||||
await dropOnUpdateTrigger(knex, TableName.SarReviewer);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretApprovalRequest);
|
||||
}
|
||||
@@ -21,6 +21,12 @@ export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./projects";
|
||||
export * from "./sa-request-secret-tags";
|
||||
export * from "./sa-request-secrets";
|
||||
export * from "./sap-approvers";
|
||||
export * from "./sar-reviewers";
|
||||
export * from "./secret-approval-policies";
|
||||
export * from "./secret-approval-requests";
|
||||
export * from "./secret-blind-indexes";
|
||||
export * from "./secret-folders";
|
||||
export * from "./secret-imports";
|
||||
|
||||
@@ -34,7 +34,14 @@ export enum TableName {
|
||||
IdentityUniversalAuth = "identity_universal_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = " identity_project_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SapApprover = "sap_approvers", // sap: secret approval policy
|
||||
SecretApprovalRequest = "secret_approval_requests",
|
||||
SarReviewer = "sar_reviewers",
|
||||
SarSecret = "sa_request_secrets",
|
||||
SarSecretTag = "sa_request_secret_tags",
|
||||
// junction tables
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
JnSecretVersionTag = "secret_version_tag_junction"
|
||||
}
|
||||
|
||||
20
backend-pg/src/db/schemas/sa-request-secret-tags.ts
Normal file
20
backend-pg/src/db/schemas/sa-request-secret-tags.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 SaRequestSecretTagsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
secretId: z.string().uuid(),
|
||||
tagId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TSaRequestSecretTags = z.infer<typeof SaRequestSecretTagsSchema>;
|
||||
export type TSaRequestSecretTagsInsert = Omit<TSaRequestSecretTags, TImmutableDBKeys>;
|
||||
export type TSaRequestSecretTagsUpdate = Partial<Omit<TSaRequestSecretTags, TImmutableDBKeys>>;
|
||||
39
backend-pg/src/db/schemas/sa-request-secrets.ts
Normal file
39
backend-pg/src/db/schemas/sa-request-secrets.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 SaRequestSecretsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
version: z.number().default(1).nullable().optional(),
|
||||
secretBlindIndex: z.string(),
|
||||
secretKeyCiphertext: z.string(),
|
||||
secretKeyIV: z.string(),
|
||||
secretKeyTag: z.string(),
|
||||
secretValueCiphertext: z.string(),
|
||||
secretValueIV: z.string(),
|
||||
secretValueTag: z.string(),
|
||||
secretCommentCiphertext: z.string().nullable().optional(),
|
||||
secretCommentIV: z.string().nullable().optional(),
|
||||
secretCommentTag: z.string().nullable().optional(),
|
||||
secretReminderNotice: z.string().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
algorithm: z.string().default('aes-256-gcm'),
|
||||
keyEncoding: z.string().default('utf8'),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
requestId: z.string().uuid(),
|
||||
op: z.string(),
|
||||
secretId: z.string().uuid().nullable().optional(),
|
||||
secretVersion: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export type TSaRequestSecrets = z.infer<typeof SaRequestSecretsSchema>;
|
||||
export type TSaRequestSecretsInsert = Omit<TSaRequestSecrets, TImmutableDBKeys>;
|
||||
export type TSaRequestSecretsUpdate = Partial<Omit<TSaRequestSecrets, TImmutableDBKeys>>;
|
||||
20
backend-pg/src/db/schemas/sap-approvers.ts
Normal file
20
backend-pg/src/db/schemas/sap-approvers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 SapApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
approverId: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TSapApprovers = z.infer<typeof SapApproversSchema>;
|
||||
export type TSapApproversInsert = Omit<TSapApprovers, TImmutableDBKeys>;
|
||||
export type TSapApproversUpdate = Partial<Omit<TSapApprovers, TImmutableDBKeys>>;
|
||||
21
backend-pg/src/db/schemas/sar-reviewers.ts
Normal file
21
backend-pg/src/db/schemas/sar-reviewers.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 SarReviewersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
member: z.string().uuid(),
|
||||
status: z.string(),
|
||||
requestId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TSarReviewers = z.infer<typeof SarReviewersSchema>;
|
||||
export type TSarReviewersInsert = Omit<TSarReviewers, TImmutableDBKeys>;
|
||||
export type TSarReviewersUpdate = Partial<Omit<TSarReviewers, TImmutableDBKeys>>;
|
||||
22
backend-pg/src/db/schemas/secret-approval-policies.ts
Normal file
22
backend-pg/src/db/schemas/secret-approval-policies.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 SecretApprovalPoliciesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
secretPath: z.string().nullable().optional(),
|
||||
approvals: z.number().default(1),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||
export type TSecretApprovalPoliciesInsert = Omit<TSecretApprovalPolicies, TImmutableDBKeys>;
|
||||
export type TSecretApprovalPoliciesUpdate = Partial<Omit<TSecretApprovalPolicies, TImmutableDBKeys>>;
|
||||
28
backend-pg/src/db/schemas/secret-approval-requests.ts
Normal file
28
backend-pg/src/db/schemas/secret-approval-requests.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 SecretApprovalRequestsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
hasMerged: z.boolean().default(false),
|
||||
status: z.string().default("open"),
|
||||
conflicts: z.unknown().nullable().optional(),
|
||||
slug: z.string(),
|
||||
folderId: z.string().uuid(),
|
||||
statusChangeBy: z.string().uuid().optional().nullable(),
|
||||
committerId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
||||
export type TSecretApprovalRequestsInsert = Omit<TSecretApprovalRequests, TImmutableDBKeys>;
|
||||
export type TSecretApprovalRequestsUpdate = Partial<
|
||||
Omit<TSecretApprovalRequests, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -1,8 +1,10 @@
|
||||
import { registerOrgRoleRouter } from "./org-role-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||
|
||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||
await server.register(registerProjectRoleRouter, { prefix: "/workspace" });
|
||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||
};
|
||||
|
||||
154
backend-pg/src/ee/routes/v1/secret-approval-policy-router.ts
Normal file
154
backend-pg/src/ee/routes/v1/secret-approval-policy-router.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
|
||||
export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.createSap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z.string().optional().nullable()
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.updateSap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
...req.body,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.deleteSap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: sapPubSchema.merge(z.object({approvers:z.string().array()})).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.secretApprovalPolicy.getSapByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/board",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
policy: sapPubSchema.merge(z.object({approvers:z.string().array()})).optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const policy = await server.services.secretApprovalPolicy.getSapOfFolder({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.query.workspaceId,
|
||||
...req.query
|
||||
});
|
||||
return { policy };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSapApproverDalFactory = ReturnType<typeof sapApproverDalFactory>;
|
||||
|
||||
export const sapApproverDalFactory = (db: TDbClient) => {
|
||||
const sapApproverOrm = ormify(db, TableName.SapApprover);
|
||||
return sapApproverOrm;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TSecretApprovalPolicies, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
TFindFilter,
|
||||
buildFindFilter,
|
||||
mergeOneToManyRelation,
|
||||
ormify,
|
||||
selectAllTableCols
|
||||
} from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TSecretApprovalPolicyDalFactory = ReturnType<typeof secretApprovalPolicyDalFactory>;
|
||||
|
||||
export const secretApprovalPolicyDalFactory = (db: TDbClient) => {
|
||||
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
|
||||
|
||||
const sapFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
|
||||
tx(TableName.SecretApprovalPolicy)
|
||||
.where(buildFindFilter(filter))
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretApprovalPolicy}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.select(tx.ref("approverId").withSchema(TableName.SapApprover))
|
||||
.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.SecretApprovalPolicy))
|
||||
.orderBy("createdAt", "asc");
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await sapFindQuery(tx || db, {
|
||||
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
|
||||
});
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
doc,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (
|
||||
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await sapFindQuery(tx || db, filter);
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
docs,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretApprovalPolicyOrm, findById, find };
|
||||
};
|
||||
@@ -0,0 +1,231 @@
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TSecretApprovalPolicyDalFactory } from "./secret-approval-policy-dal";
|
||||
import { TSapApproverDalFactory } from "./sap-approver-dal";
|
||||
import {
|
||||
TCreateSapDTO,
|
||||
TDeleteSapDTO,
|
||||
TGetBoardSapDTO,
|
||||
TListSapDTO,
|
||||
TUpdateSapDTO
|
||||
} from "./secret-approval-policy-types";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import picomatch from "picomatch";
|
||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||
import { TProjectMembershipDalFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||
|
||||
const getPolicyScore = (policy: { secretPath?: string | null }) =>
|
||||
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
|
||||
// eslint-disable-next-line
|
||||
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
|
||||
|
||||
type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretApprovalPolicyDal: TSecretApprovalPolicyDalFactory;
|
||||
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
|
||||
sapApproverDal: TSapApproverDalFactory;
|
||||
projectMembershipDal: Pick<TProjectMembershipDalFactory, "find">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<
|
||||
typeof secretApprovalPolicyServiceFactory
|
||||
>;
|
||||
|
||||
export const secretApprovalPolicyServiceFactory = ({
|
||||
secretApprovalPolicyDal,
|
||||
permissionService,
|
||||
sapApproverDal,
|
||||
projectEnvDal,
|
||||
projectMembershipDal
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createSap = async ({
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
approvals,
|
||||
approvers,
|
||||
projectId,
|
||||
secretPath,
|
||||
environment
|
||||
}: TCreateSapDTO) => {
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
const env = await projectEnvDal.findOne({ slug: environment, projectId });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const secretApprovers = await projectMembershipDal.find({
|
||||
projectId,
|
||||
$in: { id: approvers }
|
||||
});
|
||||
if (secretApprovers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
|
||||
const secretApproval = await secretApprovalPolicyDal.transaction(async (tx) => {
|
||||
const doc = await secretApprovalPolicyDal.create(
|
||||
{
|
||||
envId: env.id,
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
await sapApproverDal.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...secretApproval, environment: env, projectId };
|
||||
};
|
||||
|
||||
const updateSap = async ({
|
||||
approvers,
|
||||
secretPath,
|
||||
name,
|
||||
actorId,
|
||||
actor,
|
||||
approvals,
|
||||
secretPolicyId
|
||||
}: TUpdateSapDTO) => {
|
||||
const secretApprovalPolicy = await secretApprovalPolicyDal.findById(secretPolicyId);
|
||||
if (!secretApprovalPolicy)
|
||||
throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalPolicy.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const updatedSap = await secretApprovalPolicyDal.transaction(async (tx) => {
|
||||
const doc = await secretApprovalPolicyDal.updateById(
|
||||
secretApprovalPolicy.id,
|
||||
{
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
const secretApprovers = await projectMembershipDal.find(
|
||||
{
|
||||
projectId: secretApprovalPolicy.projectId,
|
||||
$in: { id: approvers }
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (secretApprovers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
if (doc.approvals > secretApprovers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
await sapApproverDal.delete({ policyId: doc.id }, tx);
|
||||
await sapApproverDal.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
...updatedSap,
|
||||
environment: secretApprovalPolicy.environment,
|
||||
projectId: secretApprovalPolicy.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const deleteSap = async ({ secretPolicyId, actor, actorId }: TDeleteSapDTO) => {
|
||||
const sapPolicy = await secretApprovalPolicyDal.findById(secretPolicyId);
|
||||
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
sapPolicy.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
await secretApprovalPolicyDal.deleteById(secretPolicyId);
|
||||
return sapPolicy;
|
||||
};
|
||||
|
||||
const getSapByProjectId = async ({ actorId, actor, projectId }: TListSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const sapPolicies = await secretApprovalPolicyDal.find({ projectId });
|
||||
return sapPolicies;
|
||||
};
|
||||
|
||||
const getSapPolicy = async (projectId: string, environment: string, secretPath: string) => {
|
||||
const env = await projectEnvDal.findOne({ slug: environment, projectId });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const policies = await secretApprovalPolicyDal.find({ envId: env.id });
|
||||
if (!policies.length) return;
|
||||
// this will filter policies either without scoped to secret path or the one that matches with secret path
|
||||
const policiesFilteredByPath = policies.filter(
|
||||
({ secretPath: policyPath }) =>
|
||||
!policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
|
||||
);
|
||||
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
|
||||
// if that is tie get by first createdAt
|
||||
const policiesByPriority = policiesFilteredByPath.sort(
|
||||
(a, b) => getPolicyScore(b) - getPolicyScore(a)
|
||||
);
|
||||
const finalPolicy = policiesByPriority.shift();
|
||||
return finalPolicy;
|
||||
};
|
||||
|
||||
const getSapOfFolder = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
environment,
|
||||
secretPath
|
||||
}: TGetBoardSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
return getSapPolicy(projectId, environment, secretPath);
|
||||
};
|
||||
|
||||
return {
|
||||
createSap,
|
||||
updateSap,
|
||||
deleteSap,
|
||||
getSapPolicy,
|
||||
getSapByProjectId,
|
||||
getSapOfFolder
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateSapDTO = {
|
||||
approvals: number;
|
||||
secretPath?: string | null;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
projectId: string;
|
||||
name: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateSapDTO = {
|
||||
secretPolicyId: string;
|
||||
approvals?: number;
|
||||
secretPath?: string | null;
|
||||
approvers: string[];
|
||||
name?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteSapDTO = {
|
||||
secretPolicyId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListSapDTO = TProjectPermission;
|
||||
|
||||
export type TGetBoardSapDTO = {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSarReviewerDalFactory = ReturnType<typeof sarReviewerDalFactory>;
|
||||
|
||||
export const sarReviewerDalFactory = (db: TDbClient) => {
|
||||
const sarReviewerOrm = ormify(db, TableName.SarReviewer);
|
||||
return sarReviewerOrm;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TSarSecretDalFactory = ReturnType<typeof sarSecretDalFactory>;
|
||||
|
||||
export const sarSecretDalFactory = (db: TDbClient) => {
|
||||
const sarSecretOrm = ormify(db, TableName.SarSecret);
|
||||
|
||||
const findByRequestId = (requestId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = (tx || db)(TableName.SarSecret)
|
||||
.where({ requestId })
|
||||
.leftJoin(TableName.Secret, `${TableName.SarSecret}.secretId`, `${TableName.Secret}.id`)
|
||||
.select(selectAllTableCols(TableName.SarSecret))
|
||||
.select(
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("latestSecretBlindIndex"),
|
||||
db.ref("version").withSchema(TableName.Secret).as("latestSecretVersion")
|
||||
);
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByRequestId" });
|
||||
}
|
||||
};
|
||||
return { ...sarSecretOrm, findByRequestId };
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TSecretApprovalRequests, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { TFindFilter, ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
import { RequestState } from "./secret-approval-request-types";
|
||||
|
||||
export type TSecretApprovalRequestDalFactory = ReturnType<typeof secretApprovalRequestDalFactory>;
|
||||
|
||||
type TFindQueryFilter = {
|
||||
projectId: string;
|
||||
membershipId: string;
|
||||
status?: RequestState;
|
||||
environment?: string;
|
||||
committer?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestOrm = ormify(db, TableName.SecretApprovalRequest);
|
||||
|
||||
const findQuery = (filter: TFindFilter<TSecretApprovalRequests>, tx: Knex) =>
|
||||
tx(TableName.SecretApprovalRequest)
|
||||
.where(filter)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SarReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SarReviewer}.requestId`
|
||||
)
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(tx.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId"))
|
||||
.select(tx.ref("statue").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"))
|
||||
.select(tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.SecretApprovalPolicy))
|
||||
.select(
|
||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath")
|
||||
)
|
||||
.select(tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"))
|
||||
.select(tx.ref("approverId").withSchema(TableName.SapApprover));
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await findQuery({ id }, tx || db);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: ({
|
||||
id: pk,
|
||||
projectId,
|
||||
hasMerged,
|
||||
status,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
policyName,
|
||||
policyApprovals,
|
||||
policySecretPath
|
||||
}) => ({
|
||||
id: pk,
|
||||
hasMerged,
|
||||
projectId,
|
||||
status,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
policy: {
|
||||
id: policyId,
|
||||
name: policyName,
|
||||
approvals: policyApprovals,
|
||||
secretPath: policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers",
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => ({ member, status })
|
||||
},
|
||||
{ key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId }
|
||||
] as const
|
||||
});
|
||||
if (!formatedDoc?.[0]) return;
|
||||
return {
|
||||
...formatedDoc[0],
|
||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdSAR" });
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where({ projectId })
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.groupBy("status")
|
||||
.count("status");
|
||||
console.log(JSON.stringify(doc, null, 4));
|
||||
return { open: 0, closed: 0 };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestCount" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
{ status, limit, offset, projectId, committer, environment, membershipId }: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where({ projectId, slug: environment, status, committerId: committer })
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(db.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId"))
|
||||
.select(db.ref("statue").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"))
|
||||
.select(
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath")
|
||||
)
|
||||
.select(
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
||||
)
|
||||
.select(db.ref("approverId").withSchema(TableName.SapApprover));
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: ({
|
||||
id: pk,
|
||||
hasMerged,
|
||||
status: pStatus,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
policyName,
|
||||
policyApprovals,
|
||||
policySecretPath
|
||||
}) => ({
|
||||
id: pk,
|
||||
hasMerged,
|
||||
projectId,
|
||||
status: pStatus,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
policy: {
|
||||
id: policyId,
|
||||
name: policyName,
|
||||
approvals: policyApprovals,
|
||||
secretPath: policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers",
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => ({ member, status: s })
|
||||
},
|
||||
{ key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId }
|
||||
] as const
|
||||
});
|
||||
return formatedDoc.map((el) => ({
|
||||
...el,
|
||||
policy: { ...el.policy, approvers: el.approvers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindSAR" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretApprovalRequestOrm, findById, findProjectRequestCount, findByProjectId };
|
||||
};
|
||||
@@ -0,0 +1,673 @@
|
||||
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretApprovalRequestDalFactory } from "./secret-approval-request-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
CommitType,
|
||||
TApprovalRequestCountDTO,
|
||||
TGenerateSecretApprovalRequestDTO,
|
||||
TListApprovalsDTO,
|
||||
TMergeSecretApprovalRequestDTO,
|
||||
TReviewRequestDTO,
|
||||
TSecretApprovalDetailsDTO,
|
||||
TStatusChangeDTO
|
||||
} from "./secret-approval-request-types";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { TSecretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal";
|
||||
import { generateSecretBlindIndexBySalt } from "@app/services/secret/secret-service";
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
SecretType,
|
||||
TSaRequestSecretsInsert,
|
||||
TSecrets
|
||||
} from "@app/db/schemas";
|
||||
import { TSecretDalFactory } from "@app/services/secret/secret-dal";
|
||||
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TSarSecretDalFactory } from "./sar-secret-dal";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TSarReviewerDalFactory } from "./sar-reviewer-dal";
|
||||
|
||||
type TSecretApprovalRequestServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretApprovalRequestDal: TSecretApprovalRequestDalFactory;
|
||||
secretDal: TSecretDalFactory;
|
||||
sarSecretDal: TSarSecretDalFactory;
|
||||
sarReviewerDal: TSarReviewerDalFactory;
|
||||
secretVersionDal: Pick<TSecretVersionDalFactory, "findLatestVersionMany" | "insertMany">;
|
||||
folderDal: Pick<TSecretFolderDalFactory, "findBySecretPath">;
|
||||
secretBlindIndexDal: Pick<TSecretBlindIndexDalFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<
|
||||
typeof secretApprovalRequestServiceFactory
|
||||
>;
|
||||
|
||||
export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequestDal,
|
||||
folderDal,
|
||||
secretDal,
|
||||
sarReviewerDal,
|
||||
sarSecretDal,
|
||||
secretVersionDal,
|
||||
secretBlindIndexDal,
|
||||
permissionService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId }: TApprovalRequestCountDTO) => {
|
||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const count = await secretApprovalRequestDal.findProjectRequestCount(projectId, membership.id);
|
||||
return count;
|
||||
};
|
||||
|
||||
const getSecretApprovals = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
status,
|
||||
environment,
|
||||
committer
|
||||
}: TListApprovalsDTO) => {
|
||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const approvals = await secretApprovalRequestDal.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
status,
|
||||
membershipId: membership.id
|
||||
});
|
||||
return approvals;
|
||||
};
|
||||
|
||||
const getSecretApprovalDetails = async ({ actor, actorId, id }: TSecretApprovalDetailsDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDal.findById(id);
|
||||
if (!secretApprovalRequest)
|
||||
throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const secrets = await sarSecretDal.findByRequestId(secretApprovalRequest.id);
|
||||
return { ...secretApprovalRequest, commits: secrets };
|
||||
};
|
||||
|
||||
const reviewApproval = async ({ approvalId, actor, status, actorId }: TReviewRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDal.findById(approvalId);
|
||||
if (!secretApprovalRequest)
|
||||
throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
const reviewStatus = await sarReviewerDal.transaction(async (tx) => {
|
||||
const review = await sarReviewerDal.findOne(
|
||||
{
|
||||
requestId: secretApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!review) {
|
||||
return sarReviewerDal.create(
|
||||
{
|
||||
status,
|
||||
requestId: secretApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return sarReviewerDal.updateById(review.id, { status }, tx);
|
||||
});
|
||||
return reviewStatus;
|
||||
};
|
||||
|
||||
const updateApprovalStatus = async ({ actorId, status, approvalId, actor }: TStatusChangeDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDal.findById(approvalId);
|
||||
if (!secretApprovalRequest)
|
||||
throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
if (secretApprovalRequest.hasMerged)
|
||||
throw new BadRequestError({ message: "Approval request has been merged" });
|
||||
if (secretApprovalRequest.status === "close" && status === "close")
|
||||
throw new BadRequestError({ message: "Approval request is already closed" });
|
||||
if (secretApprovalRequest.status === "open" && status === "open")
|
||||
throw new BadRequestError({ message: "Approval request is already open" });
|
||||
|
||||
const updatedRequest = await secretApprovalRequestDal.updateById(secretApprovalRequest.id, {
|
||||
status,
|
||||
statusChangeBy: membership.id
|
||||
});
|
||||
return updatedRequest;
|
||||
};
|
||||
|
||||
const mergeSecretApprovalRequest = async ({
|
||||
approvalId,
|
||||
actor,
|
||||
actorId
|
||||
}: TMergeSecretApprovalRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDal.findById(approvalId);
|
||||
if (!secretApprovalRequest)
|
||||
throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy, folderId } = secretApprovalRequest;
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
|
||||
{}
|
||||
);
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
|
||||
if (!hasMinApproval)
|
||||
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
const secretApprovalSecrets = await sarSecretDal.findByRequestId(secretApprovalRequest.id);
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
|
||||
const conflicts: Array<{ secretId: string; op: CommitType }> = [];
|
||||
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Create);
|
||||
if (secretCreationCommits.length) {
|
||||
const conflictedSecrets = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
secretCreationCommits.map(({ secretBlindIndex }) => ({
|
||||
type: SecretType.Shared,
|
||||
blindIndex: secretBlindIndex
|
||||
}))
|
||||
);
|
||||
const conflictGroupByBlindIndex = conflictedSecrets.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }),
|
||||
{}
|
||||
);
|
||||
secretCreationCommits
|
||||
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: CommitType.Create, secretId: el.id });
|
||||
});
|
||||
secretCreationCommits = secretCreationCommits.filter(
|
||||
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
|
||||
);
|
||||
}
|
||||
|
||||
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Update);
|
||||
if (secretUpdationCommits.length) {
|
||||
const conflictedByNewBlindIndex = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
secretUpdationCommits.map(({ secretBlindIndex }) => ({
|
||||
type: SecretType.Shared,
|
||||
blindIndex: secretBlindIndex
|
||||
}))
|
||||
);
|
||||
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
|
||||
(prev, curr) =>
|
||||
curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev,
|
||||
{}
|
||||
);
|
||||
secretUpdationCommits
|
||||
.filter(
|
||||
({ secretBlindIndex, secretId }) =>
|
||||
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
|
||||
)
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: CommitType.Update, secretId: el.id });
|
||||
});
|
||||
|
||||
secretUpdationCommits = secretUpdationCommits.filter(
|
||||
({ secretBlindIndex, secretId }) =>
|
||||
Boolean(secretId) &&
|
||||
(secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true)
|
||||
);
|
||||
}
|
||||
|
||||
const secretDeletionCommits = secretApprovalSecrets.filter(
|
||||
({ op }) => op === CommitType.Delete
|
||||
);
|
||||
|
||||
const mergeStatus = await secretDal.transaction(async (tx) => {
|
||||
const newSecrets = await secretDal.insertMany(
|
||||
secretCreationCommits.map(
|
||||
({
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays,
|
||||
version: 1,
|
||||
folderId,
|
||||
type: SecretType.Shared,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
const updatedSecrets = await secretDal.bulkUpdate(
|
||||
secretUpdationCommits.map(
|
||||
({
|
||||
secretId,
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
folderId,
|
||||
id: secretId as string,
|
||||
type: SecretType.Shared,
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
const deletedSecret = await secretDal.deleteMany(
|
||||
secretDeletionCommits.map(({ secretBlindIndex }) => ({
|
||||
blindIndex: secretBlindIndex,
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
folderId,
|
||||
actorId,
|
||||
tx
|
||||
);
|
||||
await secretVersionDal.insertMany(
|
||||
newSecrets
|
||||
.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
}))
|
||||
.concat(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
}))
|
||||
),
|
||||
tx
|
||||
);
|
||||
|
||||
const updatedSecretApproval = await secretApprovalRequestDal.updateById(
|
||||
secretApprovalRequest.id,
|
||||
{
|
||||
conflicts,
|
||||
hasMerged: true,
|
||||
status: "close",
|
||||
statusChangeBy: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return {
|
||||
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
|
||||
approval: updatedSecretApproval
|
||||
};
|
||||
});
|
||||
return mergeStatus;
|
||||
};
|
||||
|
||||
// function to save secret change to secret approval
|
||||
// this will keep a copy to do merge later when accepting
|
||||
const generateSecretApprovalRequest = async ({
|
||||
data,
|
||||
policy,
|
||||
projectId,
|
||||
secretPath,
|
||||
environment,
|
||||
commiterMembershipId
|
||||
}: TGenerateSecretApprovalRequestDTO) => {
|
||||
const folder = await folderDal.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
||||
const folderId = folder.id;
|
||||
|
||||
const blindIndexDoc = await secretBlindIndexDal.findOne({ projectId });
|
||||
if (!blindIndexDoc)
|
||||
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||
|
||||
const commits: Omit<TSaRequestSecretsInsert, "requestId">[] = [];
|
||||
// for created secret approval change
|
||||
const createdSecrets = data[CommitType.Create];
|
||||
if (createdSecrets && createdSecrets?.length) {
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
createdSecrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexBySalt(secretName, blindIndexDoc)
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
// eslint-disable-next-line
|
||||
prev[createdSecrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = createdSecrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const exists = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
createdSecrets.map(({ secretName }) => ({
|
||||
blindIndex: secretBlindIndexes[secretName],
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (exists.length) throw new BadRequestError({ message: "Secret already exist" });
|
||||
commits.push(
|
||||
...createdSecrets.map((el) => ({
|
||||
...el,
|
||||
op: CommitType.Create as const,
|
||||
version: 0,
|
||||
secretBlindIndex: secretBlindIndexes[el.secretName]
|
||||
}))
|
||||
);
|
||||
}
|
||||
// not secret approval for update operations
|
||||
const updatedSecrets = data[CommitType.Update];
|
||||
if (updatedSecrets && updatedSecrets?.length) {
|
||||
// get all blind index
|
||||
// Find all those secrets
|
||||
// if not throw not found
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
updatedSecrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexBySalt(secretName, blindIndexDoc)
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
// eslint-disable-next-line
|
||||
prev[updatedSecrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = updatedSecrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const secretsToBeUpdated = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
updatedSecrets.map(({ secretName }) => ({
|
||||
blindIndex: secretBlindIndexes[secretName],
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secretsToBeUpdated.length !== updatedSecrets.length)
|
||||
throw new BadRequestError({ message: "Secret not found" });
|
||||
|
||||
// now find any secret that needs to update its name
|
||||
// same process as above
|
||||
const nameUpdatedSecrets = updatedSecrets.filter(({ newSecretName }) =>
|
||||
Boolean(newSecretName)
|
||||
);
|
||||
const newSecretBlindIndexes = await Promise.all(
|
||||
nameUpdatedSecrets.map(({ newSecretName }) =>
|
||||
generateSecretBlindIndexBySalt(newSecretName as string, blindIndexDoc)
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
// eslint-disable-next-line
|
||||
prev[nameUpdatedSecrets[i].secretName] = curr;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
const secretsWithNewName = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
nameUpdatedSecrets.map(({ newSecretName }) => ({
|
||||
blindIndex: newSecretBlindIndexes[newSecretName as string],
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secretsWithNewName.length)
|
||||
throw new BadRequestError({ message: "Secret with new name already exist" });
|
||||
|
||||
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, TSecrets>>(
|
||||
(prev, curr) => {
|
||||
// eslint-disable-next-line
|
||||
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const updatedSecretIds = updatedSecrets.map(
|
||||
(el) => secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].id
|
||||
);
|
||||
const latestSecretVersions = await secretVersionDal.findLatestVersionMany(
|
||||
folderId,
|
||||
updatedSecretIds
|
||||
);
|
||||
commits.push(
|
||||
...updatedSecrets.map((el) => {
|
||||
const secretId = secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].id;
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
op: CommitType.Update as const,
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id,
|
||||
...el,
|
||||
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
|
||||
version: secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].version || 1
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
// deleted secrets
|
||||
const deletedSecrets = data[CommitType.Delete];
|
||||
if (deletedSecrets && deletedSecrets.length) {
|
||||
// get all blind index
|
||||
// Find all those secrets
|
||||
// if not throw not found
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
deletedSecrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexBySalt(secretName, blindIndexDoc)
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
// eslint-disable-next-line
|
||||
prev[deletedSecrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = deletedSecrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
// not find those secrets. if any of them not found throw an not found error
|
||||
const secretsToBeDeleted = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
deletedSecrets.map(({ secretName }) => ({
|
||||
blindIndex: secretBlindIndexes[secretName],
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secretsToBeDeleted.length !== deletedSecrets.length)
|
||||
throw new BadRequestError({ message: "Secret not found" });
|
||||
const secretsGroupedByBlindIndex = secretsToBeDeleted.reduce<Record<string, TSecrets>>(
|
||||
(prev, curr) => {
|
||||
// eslint-disable-next-line
|
||||
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const deletedSecretIds = deletedSecrets.map(
|
||||
(el) => secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].id
|
||||
);
|
||||
const latestSecretVersions = await secretVersionDal.findLatestVersionMany(
|
||||
folderId,
|
||||
deletedSecretIds
|
||||
);
|
||||
commits.push(
|
||||
...deletedSecrets.map((el) => {
|
||||
const secretId = secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].id;
|
||||
return {
|
||||
op: CommitType.Delete as const,
|
||||
...latestSecretVersions[secretId],
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDal.transaction(async (tx) => {
|
||||
const doc = await secretApprovalRequestDal.create(
|
||||
{
|
||||
folderId,
|
||||
slug: alphaNumericNanoId(),
|
||||
policyId: policy.id,
|
||||
status: "open",
|
||||
hasMerged: false,
|
||||
committerId: commiterMembershipId
|
||||
},
|
||||
tx
|
||||
);
|
||||
const approvalCommits = await sarSecretDal.insertMany(
|
||||
commits.map(
|
||||
({
|
||||
version,
|
||||
op,
|
||||
secretKeyTag,
|
||||
secretKeyIV,
|
||||
keyEncoding,
|
||||
secretId,
|
||||
metadata,
|
||||
algorithm,
|
||||
secretBlindIndex,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretVersion,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretReminderNotice,
|
||||
skipMultilineEncoding,
|
||||
secretCommentCiphertext,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
version,
|
||||
requestId: doc.id,
|
||||
op,
|
||||
secretKeyTag,
|
||||
secretKeyIV,
|
||||
keyEncoding,
|
||||
secretId,
|
||||
metadata,
|
||||
algorithm,
|
||||
secretBlindIndex,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretVersion,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretReminderNotice,
|
||||
skipMultilineEncoding,
|
||||
secretCommentCiphertext,
|
||||
secretReminderRepeatDays
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
return {
|
||||
generateSecretApprovalRequest,
|
||||
mergeSecretApprovalRequest,
|
||||
reviewApproval,
|
||||
updateApprovalStatus,
|
||||
getSecretApprovals,
|
||||
getSecretApprovalDetails,
|
||||
requestCount
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TImmutableDBKeys, TSaRequestSecrets, TSecretApprovalPolicies } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum CommitType {
|
||||
Create = "create",
|
||||
Update = "update",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum RequestState {
|
||||
Open = "open",
|
||||
Closed = "closed"
|
||||
}
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
type TApprovalCreateSecret = Omit<TSaRequestSecrets, TImmutableDBKeys | "version"> & {
|
||||
secretName: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
type TApprovalUpdateSecret = Partial<Omit<TSaRequestSecrets, TImmutableDBKeys | "version">> & {
|
||||
secretName: string;
|
||||
newSecretName?: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TGenerateSecretApprovalRequestDTO = {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
policy: TSecretApprovalPolicies;
|
||||
commiterMembershipId: string;
|
||||
data: {
|
||||
[CommitType.Create]: TApprovalCreateSecret[];
|
||||
[CommitType.Update]: TApprovalUpdateSecret[];
|
||||
[CommitType.Delete]: { secretName: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TMergeSecretApprovalRequestDTO = {
|
||||
approvalId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TStatusChangeDTO = {
|
||||
approvalId: string;
|
||||
status: "open" | "close";
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TReviewRequestDTO = {
|
||||
approvalId: string;
|
||||
status: ApprovalStatus;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TApprovalRequestCountDTO = TProjectPermission;
|
||||
|
||||
export type TListApprovalsDTO = {
|
||||
projectId: string;
|
||||
status?: RequestState;
|
||||
environment?: string;
|
||||
committer?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSecretApprovalDetailsDTO = {
|
||||
id:string;
|
||||
} & Omit<TProjectPermission, 'projectId'>
|
||||
@@ -15,6 +15,21 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||
...dal
|
||||
});
|
||||
|
||||
export type TFindFilter<R extends {} = any> = Partial<R> & {
|
||||
$in?: Partial<{ [K in keyof R]: R[K][] }>;
|
||||
};
|
||||
export const buildFindFilter =
|
||||
<R extends {} = any>({ $in, ...filter }: TFindFilter<R>) =>
|
||||
(bd: Knex.QueryBuilder<R, R>) => {
|
||||
bd.where(filter);
|
||||
if ($in) {
|
||||
Object.entries($in).forEach(([key, val]) => {
|
||||
bd.whereIn(key as any, val as any);
|
||||
});
|
||||
}
|
||||
return bd;
|
||||
};
|
||||
|
||||
// What is ormify
|
||||
// It is to inject typical operations like find, findOne, update, delete, create
|
||||
// This will avoid writing most common ones each time
|
||||
@@ -45,9 +60,10 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
},
|
||||
find: (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
|
||||
find: async (filter: TFindFilter<Tables[Tname]["base"]>, tx?: Knex) => {
|
||||
try {
|
||||
return (tx || db)(tableName).where(filter);
|
||||
const res = await (tx || db)(tableName).where(buildFindFilter(filter));
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const mergeOneToManyRelation = <
|
||||
T extends Record<string, any>,
|
||||
Pk extends keyof T,
|
||||
P extends Record<string, any>,
|
||||
C extends Record<string, any>,
|
||||
C extends any,
|
||||
Ck extends string = "child"
|
||||
>(
|
||||
data: T[],
|
||||
@@ -29,3 +29,61 @@ export const mergeOneToManyRelation = <
|
||||
}
|
||||
return groupedRecord;
|
||||
};
|
||||
|
||||
export type TSqlPackRelationships<
|
||||
T extends Record<string, any>,
|
||||
P extends Record<string, any>,
|
||||
C extends TChildMapper<T>[]
|
||||
> = {
|
||||
data: T[];
|
||||
key: keyof T;
|
||||
parentMapper: (arg: T) => P;
|
||||
childrenMapper: C;
|
||||
};
|
||||
|
||||
export type TChildMapper<T extends {}, U extends string = string, R extends unknown = unknown> = {
|
||||
key: keyof T;
|
||||
label: U;
|
||||
mapper: (arg: T) => R;
|
||||
};
|
||||
|
||||
type MappedRecord<T extends TChildMapper<any>> = {
|
||||
[K in T["label"]]: ReturnType<Extract<T, { label: K }>["mapper"]>[];
|
||||
};
|
||||
|
||||
export const sqlNestRelationships = <
|
||||
T extends Record<string, any> = {},
|
||||
P extends Record<string, any> = {},
|
||||
C extends TChildMapper<T>[] = TChildMapper<T>[]
|
||||
>({
|
||||
data,
|
||||
key,
|
||||
parentMapper,
|
||||
childrenMapper
|
||||
}: TSqlPackRelationships<T, P, C>) => {
|
||||
const parentLookup = new Set<string>();
|
||||
const childLookUp = new Set<string>();
|
||||
const recordsOrder: string[] = [];
|
||||
|
||||
type Cm = MappedRecord<(typeof childrenMapper)[number]>;
|
||||
|
||||
const recordsGroupedByPk: Record<string, P & Cm> = {};
|
||||
data.forEach((el) => {
|
||||
const pk = el[key];
|
||||
if (!parentLookup.has(pk)) {
|
||||
recordsGroupedByPk[pk] = parentMapper(el) as P & Cm;
|
||||
recordsOrder.push(pk);
|
||||
parentLookup.add(pk);
|
||||
}
|
||||
childrenMapper.forEach(({ label, mapper, key: cKey }) => {
|
||||
const ck = `${pk}-${label}-${el[cKey]}`;
|
||||
if (!childLookUp.has(ck)) {
|
||||
if (!recordsGroupedByPk[pk][label]) recordsGroupedByPk[pk][label as keyof Cm] = [] as any;
|
||||
const val = mapper(el);
|
||||
if (typeof val !== "undefined") recordsGroupedByPk[pk][label].push(val);
|
||||
childLookUp.add(ck);
|
||||
}
|
||||
});
|
||||
});
|
||||
return recordsOrder.map((pkId) => recordsGroupedByPk[pkId]);
|
||||
};
|
||||
|
||||
4
backend-pg/src/lib/nanoid/index.ts
Normal file
4
backend-pg/src/lib/nanoid/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
export const alphaNumericNanoId = customAlphabet(SLUG_ALPHABETS, 10);
|
||||
7
backend-pg/src/lib/picomatch/index.ts
Normal file
7
backend-pg/src/lib/picomatch/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import path from "node:path";
|
||||
|
||||
export function containsGlobPatterns(secretPath: string) {
|
||||
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
|
||||
const normalizedPath = path.normalize(secretPath);
|
||||
return globChars.some((char) => normalizedPath.includes(char));
|
||||
}
|
||||
@@ -69,6 +69,9 @@ import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
import { secretApprovalPolicyDalFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
||||
import { sapApproverDalFactory } from "@app/ee/services/secret-approval-policy/sap-approver-dal";
|
||||
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
|
||||
export const registerRoutes = async (
|
||||
server: FastifyZodProvider,
|
||||
@@ -113,9 +116,18 @@ export const registerRoutes = async (
|
||||
|
||||
// ee db layer ops
|
||||
const permissionDal = permissionDalFactory(db);
|
||||
const sapApproverDal = sapApproverDalFactory(db);
|
||||
const secretApprovalPolicyDal = secretApprovalPolicyDalFactory(db);
|
||||
|
||||
// ee services
|
||||
const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal });
|
||||
const sapService = secretApprovalPolicyServiceFactory({
|
||||
projectMembershipDal,
|
||||
projectEnvDal,
|
||||
sapApproverDal,
|
||||
permissionService,
|
||||
secretApprovalPolicyDal
|
||||
});
|
||||
|
||||
// service layers
|
||||
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
||||
@@ -274,7 +286,8 @@ export const registerRoutes = async (
|
||||
identity: identityService,
|
||||
identityAccessToken: identityAccessTokenService,
|
||||
identityProject: identityProjectService,
|
||||
identityUa: identityUaService
|
||||
identityUa: identityUaService,
|
||||
secretApprovalPolicy: sapService
|
||||
});
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IntegrationAuthsSchema } from "@app/db/schemas";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IntegrationAuthsSchema, SecretApprovalPoliciesSchema } from "@app/db/schemas";
|
||||
|
||||
// sometimes the return data must be santizied to avoid leaking important values
|
||||
// always prefer pick over omit in zod
|
||||
@@ -13,3 +15,14 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
});
|
||||
|
||||
export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
|
||||
z.object({
|
||||
environment: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
projectId: z.string()
|
||||
})
|
||||
);
|
||||
|
||||
@@ -44,6 +44,23 @@ type TSecretServiceFactoryDep = {
|
||||
|
||||
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
||||
|
||||
export const generateSecretBlindIndexBySalt = async (
|
||||
secretName: string,
|
||||
secretBlindIndexDoc: TSecretBlindIndexes
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
const secretBlindIndex = await buildSecretBlindIndexFromName({
|
||||
secretName,
|
||||
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
|
||||
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
|
||||
encryptionKey: appCfg.ENCRYPTION_KEY,
|
||||
tag: secretBlindIndexDoc.saltTag,
|
||||
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
|
||||
iv: secretBlindIndexDoc.saltIV
|
||||
});
|
||||
return secretBlindIndex;
|
||||
};
|
||||
|
||||
export const secretServiceFactory = ({
|
||||
secretDal,
|
||||
secretTagDal,
|
||||
@@ -52,23 +69,6 @@ export const secretServiceFactory = ({
|
||||
secretBlindIndexDal,
|
||||
permissionService
|
||||
}: TSecretServiceFactoryDep) => {
|
||||
const generateSecretBlindIndexBySalt = async (
|
||||
secretName: string,
|
||||
secretBlindIndexDoc: TSecretBlindIndexes
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
const secretBlindIndex = await buildSecretBlindIndexFromName({
|
||||
secretName,
|
||||
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
|
||||
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
|
||||
encryptionKey: appCfg.ENCRYPTION_KEY,
|
||||
tag: secretBlindIndexDoc.saltTag,
|
||||
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
|
||||
iv: secretBlindIndexDoc.saltIV
|
||||
});
|
||||
return secretBlindIndex;
|
||||
};
|
||||
|
||||
// utility function to get secret blind index data
|
||||
const generateSecretBlindIndexByName = async (projectId: string, secretName: string) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -516,7 +516,8 @@ export const secretServiceFactory = ({
|
||||
type
|
||||
}))
|
||||
);
|
||||
if (secretsWithNewName.length) throw new BadRequestError({ message: "Secret not found" });
|
||||
if (secretsWithNewName.length)
|
||||
throw new BadRequestError({ message: "Secret with new name already exist" });
|
||||
|
||||
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, TSecrets>>(
|
||||
(prev, curr) => {
|
||||
|
||||
@@ -1,10 +1,42 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TSecretVersions, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TSecretVersionDalFactory = ReturnType<typeof secretVersionDalFactory>;
|
||||
|
||||
export const secretVersionDalFactory = (db: TDbClient) => {
|
||||
const secretVersionOrm = ormify(db, TableName.SecretVersion);
|
||||
return secretVersionOrm;
|
||||
|
||||
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
|
||||
try {
|
||||
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(
|
||||
TableName.SecretVersion
|
||||
)
|
||||
.where("folderId", folderId)
|
||||
.whereIn(`${TableName.SecretVersion}.secretId`, secretIds)
|
||||
.join(
|
||||
(tx || db)(TableName.SecretVersion)
|
||||
.groupBy("secretId")
|
||||
.max("version")
|
||||
.select("secretId")
|
||||
.as("latestVersion"),
|
||||
(bd) => {
|
||||
bd.on(`${TableName.SecretVersion}.secretId`, "latestVersion.secretId").andOn(
|
||||
`${TableName.SecretVersion}.version`,
|
||||
"latestVersion.max"
|
||||
);
|
||||
}
|
||||
);
|
||||
return docs.reduce<Record<string, TSecretVersions>>(
|
||||
(prev, curr) => ({ ...prev, [curr.secretId]: curr }),
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretVersionOrm, findLatestVersionMany };
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||
const { data } = await apiRequest.post("/api/ee/v1/secret-approvals", {
|
||||
environment,
|
||||
workspaceId,
|
||||
approvals,
|
||||
@@ -31,7 +31,7 @@ export const useUpdateSecretApprovalPolicy = () => {
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||
const { data } = await apiRequest.patch(`/api/ee/v1/secret-approvals/${id}`, {
|
||||
approvals,
|
||||
approvers,
|
||||
secretPath,
|
||||
@@ -50,7 +50,7 @@ export const useDeleteSecretApprovalPolicy = () => {
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
|
||||
mutationFn: async ({ id }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`);
|
||||
const { data } = await apiRequest.delete(`/api/ee/v1/secret-approvals/${id}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const secretApprovalKeys = {
|
||||
|
||||
const fetchApprovalPolicies = async (workspaceId: string) => {
|
||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>(
|
||||
"/api/v1/secret-approvals",
|
||||
"/api/ee/v1/secret-approvals",
|
||||
{ params: { workspaceId } }
|
||||
);
|
||||
return data.approvals;
|
||||
@@ -49,7 +49,7 @@ const fetchApprovalPolicyOfABoard = async (
|
||||
secretPath: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>(
|
||||
"/api/v1/secret-approvals/board",
|
||||
"/api/ee/v1/secret-approvals/board",
|
||||
{ params: { workspaceId, environment, secretPath } }
|
||||
);
|
||||
return data.policy || "";
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { WorkspaceEnv } from "../workspace/types";
|
||||
|
||||
export type TSecretApprovalPolicy = {
|
||||
id: string;
|
||||
workspace: string;
|
||||
name: string;
|
||||
environment: string;
|
||||
envId: string;
|
||||
environment: WorkspaceEnv;
|
||||
secretPath?: string;
|
||||
approvers: string[];
|
||||
approvals: number;
|
||||
|
||||
@@ -11,8 +11,6 @@ export type WorkspaceEnv = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isReadDenied: boolean;
|
||||
isWriteDenied: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceTag = { id: string; name: string; slug: string };
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SecretApprovalPolicyRow = ({
|
||||
return (
|
||||
<Tr>
|
||||
<Td>{policy.name}</Td>
|
||||
<Td>{policy.environment}</Td>
|
||||
<Td>{policy.environment.slug}</Td>
|
||||
<Td>{policy.secretPath || "*"}</Td>
|
||||
<Td>
|
||||
<DropdownMenu
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SecretPolicyForm = ({
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: editValues
|
||||
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
@@ -63,7 +63,7 @@ export const SecretApprovalRequest = () => {
|
||||
(prev, curr) => ({ ...prev, [curr.id]: curr }),
|
||||
{}
|
||||
);
|
||||
const myMembershipId = members?.find(({ user }) => user.id === presentUser.id)?.id;
|
||||
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
|
||||
const isSecretApprovalScreen = Boolean(selectedApproval);
|
||||
|
||||
const handleGoBackSecretRequestDetail = () => {
|
||||
|
||||
Reference in New Issue
Block a user