mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 19:28:09 +00:00
feat(infisical-pg): completed secret approval policy and services for approval requests
This commit is contained in:
Generated
+24
-6
@@ -32,6 +32,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
|
"nanoid": "^5.0.4",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.7",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
@@ -5959,10 +5960,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -5970,10 +5970,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"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": {
|
"node_modules/postgres-array": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
|
"nanoid": "^5.0.4",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.7",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
|
|||||||
Vendored
+2
@@ -23,6 +23,7 @@ import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key
|
|||||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-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 { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
@@ -90,6 +91,7 @@ declare module "fastify" {
|
|||||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||||
identityProject: TIdentityProjectServiceFactory;
|
identityProject: TIdentityProjectServiceFactory;
|
||||||
identityUa: TIdentityUaServiceFactory;
|
identityUa: TIdentityUaServiceFactory;
|
||||||
|
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
|
|||||||
Vendored
+48
@@ -67,6 +67,24 @@ import {
|
|||||||
TProjects,
|
TProjects,
|
||||||
TProjectsInsert,
|
TProjectsInsert,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
|
TSapApprovers,
|
||||||
|
TSapApproversInsert,
|
||||||
|
TSapApproversUpdate,
|
||||||
|
TSaRequestSecrets,
|
||||||
|
TSaRequestSecretsInsert,
|
||||||
|
TSaRequestSecretsUpdate,
|
||||||
|
TSaRequestSecretTags,
|
||||||
|
TSaRequestSecretTagsInsert,
|
||||||
|
TSaRequestSecretTagsUpdate,
|
||||||
|
TSarReviewers,
|
||||||
|
TSarReviewersInsert,
|
||||||
|
TSarReviewersUpdate,
|
||||||
|
TSecretApprovalPolicies,
|
||||||
|
TSecretApprovalPoliciesInsert,
|
||||||
|
TSecretApprovalPoliciesUpdate,
|
||||||
|
TSecretApprovalRequests,
|
||||||
|
TSecretApprovalRequestsInsert,
|
||||||
|
TSecretApprovalRequestsUpdate,
|
||||||
TSecretBlindIndexes,
|
TSecretBlindIndexes,
|
||||||
TSecretBlindIndexesInsert,
|
TSecretBlindIndexesInsert,
|
||||||
TSecretBlindIndexesUpdate,
|
TSecretBlindIndexesUpdate,
|
||||||
@@ -256,6 +274,36 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityProjectMembershipsInsert,
|
TIdentityProjectMembershipsInsert,
|
||||||
TIdentityProjectMembershipsUpdate
|
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
|
// Junction tables
|
||||||
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
||||||
TSecretTagJunction,
|
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-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./projects";
|
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-blind-indexes";
|
||||||
export * from "./secret-folders";
|
export * from "./secret-folders";
|
||||||
export * from "./secret-imports";
|
export * from "./secret-imports";
|
||||||
|
|||||||
@@ -34,7 +34,14 @@ export enum TableName {
|
|||||||
IdentityUniversalAuth = "identity_universal_auths",
|
IdentityUniversalAuth = "identity_universal_auths",
|
||||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
IdentityOrgMembership = "identity_org_memberships",
|
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",
|
JnSecretTag = "secret_tag_junction",
|
||||||
JnSecretVersionTag = "secret_version_tag_junction"
|
JnSecretVersionTag = "secret_version_tag_junction"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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 { registerOrgRoleRouter } from "./org-role-router";
|
||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
|
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||||
|
|
||||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||||
// org role starts with organization
|
// org role starts with organization
|
||||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||||
await server.register(registerProjectRoleRouter, { prefix: "/workspace" });
|
await server.register(registerProjectRoleRouter, { prefix: "/workspace" });
|
||||||
|
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
+673
@@ -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
|
...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
|
// What is ormify
|
||||||
// It is to inject typical operations like find, findOne, update, delete, create
|
// It is to inject typical operations like find, findOne, update, delete, create
|
||||||
// This will avoid writing most common ones each time
|
// 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" });
|
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 {
|
try {
|
||||||
return (tx || db)(tableName).where(filter);
|
const res = await (tx || db)(tableName).where(buildFindFilter(filter));
|
||||||
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find one" });
|
throw new DatabaseError({ error, name: "Find one" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const mergeOneToManyRelation = <
|
|||||||
T extends Record<string, any>,
|
T extends Record<string, any>,
|
||||||
Pk extends keyof T,
|
Pk extends keyof T,
|
||||||
P extends Record<string, any>,
|
P extends Record<string, any>,
|
||||||
C extends Record<string, any>,
|
C extends any,
|
||||||
Ck extends string = "child"
|
Ck extends string = "child"
|
||||||
>(
|
>(
|
||||||
data: T[],
|
data: T[],
|
||||||
@@ -29,3 +29,61 @@ export const mergeOneToManyRelation = <
|
|||||||
}
|
}
|
||||||
return groupedRecord;
|
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]);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
|
||||||
|
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
export const alphaNumericNanoId = customAlphabet(SLUG_ALPHABETS, 10);
|
||||||
@@ -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 { registerV1Routes } from "./v1";
|
||||||
import { registerV2Routes } from "./v2";
|
import { registerV2Routes } from "./v2";
|
||||||
import { registerV3Routes } from "./v3";
|
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 (
|
export const registerRoutes = async (
|
||||||
server: FastifyZodProvider,
|
server: FastifyZodProvider,
|
||||||
@@ -113,9 +116,18 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDal = permissionDalFactory(db);
|
const permissionDal = permissionDalFactory(db);
|
||||||
|
const sapApproverDal = sapApproverDalFactory(db);
|
||||||
|
const secretApprovalPolicyDal = secretApprovalPolicyDalFactory(db);
|
||||||
|
|
||||||
// ee services
|
// ee services
|
||||||
const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal });
|
const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal });
|
||||||
|
const sapService = secretApprovalPolicyServiceFactory({
|
||||||
|
projectMembershipDal,
|
||||||
|
projectEnvDal,
|
||||||
|
sapApproverDal,
|
||||||
|
permissionService,
|
||||||
|
secretApprovalPolicyDal
|
||||||
|
});
|
||||||
|
|
||||||
// service layers
|
// service layers
|
||||||
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
||||||
@@ -274,7 +286,8 @@ export const registerRoutes = async (
|
|||||||
identity: identityService,
|
identity: identityService,
|
||||||
identityAccessToken: identityAccessTokenService,
|
identityAccessToken: identityAccessTokenService,
|
||||||
identityProject: identityProjectService,
|
identityProject: identityProjectService,
|
||||||
identityUa: identityUaService
|
identityUa: identityUaService,
|
||||||
|
secretApprovalPolicy: sapService
|
||||||
});
|
});
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
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
|
// sometimes the return data must be santizied to avoid leaking important values
|
||||||
// always prefer pick over omit in zod
|
// always prefer pick over omit in zod
|
||||||
@@ -13,3 +15,14 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: 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 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 = ({
|
export const secretServiceFactory = ({
|
||||||
secretDal,
|
secretDal,
|
||||||
secretTagDal,
|
secretTagDal,
|
||||||
@@ -52,23 +69,6 @@ export const secretServiceFactory = ({
|
|||||||
secretBlindIndexDal,
|
secretBlindIndexDal,
|
||||||
permissionService
|
permissionService
|
||||||
}: TSecretServiceFactoryDep) => {
|
}: 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
|
// utility function to get secret blind index data
|
||||||
const generateSecretBlindIndexByName = async (projectId: string, secretName: string) => {
|
const generateSecretBlindIndexByName = async (projectId: string, secretName: string) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -516,7 +516,8 @@ export const secretServiceFactory = ({
|
|||||||
type
|
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>>(
|
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, TSecrets>>(
|
||||||
(prev, curr) => {
|
(prev, curr) => {
|
||||||
|
|||||||
@@ -1,10 +1,42 @@
|
|||||||
import { TDbClient } from "@app/db";
|
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 { ormify } from "@app/lib/knex";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export type TSecretVersionDalFactory = ReturnType<typeof secretVersionDalFactory>;
|
export type TSecretVersionDalFactory = ReturnType<typeof secretVersionDalFactory>;
|
||||||
|
|
||||||
export const secretVersionDalFactory = (db: TDbClient) => {
|
export const secretVersionDalFactory = (db: TDbClient) => {
|
||||||
const secretVersionOrm = ormify(db, TableName.SecretVersion);
|
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>({
|
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
|
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,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
approvals,
|
approvals,
|
||||||
@@ -31,7 +31,7 @@ export const useUpdateSecretApprovalPolicy = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
|
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,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPath,
|
||||||
@@ -50,7 +50,7 @@ export const useDeleteSecretApprovalPolicy = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
|
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
|
||||||
mutationFn: async ({ id }) => {
|
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;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId }) => {
|
onSuccess: (_, { workspaceId }) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const secretApprovalKeys = {
|
|||||||
|
|
||||||
const fetchApprovalPolicies = async (workspaceId: string) => {
|
const fetchApprovalPolicies = async (workspaceId: string) => {
|
||||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>(
|
const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>(
|
||||||
"/api/v1/secret-approvals",
|
"/api/ee/v1/secret-approvals",
|
||||||
{ params: { workspaceId } }
|
{ params: { workspaceId } }
|
||||||
);
|
);
|
||||||
return data.approvals;
|
return data.approvals;
|
||||||
@@ -49,7 +49,7 @@ const fetchApprovalPolicyOfABoard = async (
|
|||||||
secretPath: string
|
secretPath: string
|
||||||
) => {
|
) => {
|
||||||
const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>(
|
const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>(
|
||||||
"/api/v1/secret-approvals/board",
|
"/api/ee/v1/secret-approvals/board",
|
||||||
{ params: { workspaceId, environment, secretPath } }
|
{ params: { workspaceId, environment, secretPath } }
|
||||||
);
|
);
|
||||||
return data.policy || "";
|
return data.policy || "";
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { WorkspaceEnv } from "../workspace/types";
|
||||||
|
|
||||||
export type TSecretApprovalPolicy = {
|
export type TSecretApprovalPolicy = {
|
||||||
id: string;
|
id: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
name: string;
|
name: string;
|
||||||
environment: string;
|
envId: string;
|
||||||
|
environment: WorkspaceEnv;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvers: string[];
|
approvers: string[];
|
||||||
approvals: number;
|
approvals: number;
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ export type WorkspaceEnv = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
isReadDenied: boolean;
|
|
||||||
isWriteDenied: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceTag = { id: string; name: string; slug: string };
|
export type WorkspaceTag = { id: string; name: string; slug: string };
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
return (
|
return (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>{policy.name}</Td>
|
<Td>{policy.name}</Td>
|
||||||
<Td>{policy.environment}</Td>
|
<Td>{policy.environment.slug}</Td>
|
||||||
<Td>{policy.secretPath || "*"}</Td>
|
<Td>{policy.secretPath || "*"}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ export const SecretPolicyForm = ({
|
|||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
values: editValues
|
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
||||||
});
|
});
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
(prev, curr) => ({ ...prev, [curr.id]: curr }),
|
(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 isSecretApprovalScreen = Boolean(selectedApproval);
|
||||||
|
|
||||||
const handleGoBackSecretRequestDetail = () => {
|
const handleGoBackSecretRequestDetail = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user