feat(infisical-pg): completed secret approval policy and services for approval requests

This commit is contained in:
Akhil Mohan
2024-01-02 20:17:53 +05:30
parent a6a60b7bbb
commit 17e61bfc68
40 changed files with 2114 additions and 43 deletions

View File

@@ -32,6 +32,7 @@
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"knex": "^3.0.1",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"passport-github": "^1.1.0",
@@ -5959,10 +5960,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz",
"integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==",
"funding": [
{
"type": "github",
@@ -5970,10 +5970,10 @@
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
"node": "^18 || >=20"
}
},
"node_modules/natural-compare": {
@@ -6772,6 +6772,24 @@
}
}
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",

View File

@@ -85,6 +85,7 @@
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"knex": "^3.0.1",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"passport-github": "^1.1.0",

View File

@@ -23,6 +23,7 @@ import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
@@ -90,6 +91,7 @@ declare module "fastify" {
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data

View File

@@ -67,6 +67,24 @@ import {
TProjects,
TProjectsInsert,
TProjectsUpdate,
TSapApprovers,
TSapApproversInsert,
TSapApproversUpdate,
TSaRequestSecrets,
TSaRequestSecretsInsert,
TSaRequestSecretsUpdate,
TSaRequestSecretTags,
TSaRequestSecretTagsInsert,
TSaRequestSecretTagsUpdate,
TSarReviewers,
TSarReviewersInsert,
TSarReviewersUpdate,
TSecretApprovalPolicies,
TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate,
TSecretApprovalRequests,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsUpdate,
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
TSecretBlindIndexesUpdate,
@@ -256,6 +274,36 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate
>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate
>;
[TableName.SapApprover]: Knex.CompositeTableType<
TSapApprovers,
TSapApproversInsert,
TSapApproversUpdate
>;
[TableName.SecretApprovalRequest]: Knex.CompositeTableType<
TSecretApprovalRequests,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsUpdate
>;
[TableName.SarReviewer]: Knex.CompositeTableType<
TSarReviewers,
TSarReviewersInsert,
TSarReviewersUpdate
>;
[TableName.SarSecret]: Knex.CompositeTableType<
TSaRequestSecrets,
TSaRequestSecretsInsert,
TSaRequestSecretsUpdate
>;
[TableName.SarSecretTag]: Knex.CompositeTableType<
TSaRequestSecretTags,
TSaRequestSecretTagsInsert,
TSaRequestSecretTagsUpdate
>;
// Junction tables
[TableName.JnSecretTag]: Knex.CompositeTableType<
TSecretTagJunction,

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -21,6 +21,12 @@ export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./projects";
export * from "./sa-request-secret-tags";
export * from "./sa-request-secrets";
export * from "./sap-approvers";
export * from "./sar-reviewers";
export * from "./secret-approval-policies";
export * from "./secret-approval-requests";
export * from "./secret-blind-indexes";
export * from "./secret-folders";
export * from "./secret-imports";

View File

@@ -34,7 +34,14 @@ export enum TableName {
IdentityUniversalAuth = "identity_universal_auths",
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = " identity_project_memberships",
IdentityProjectMembership = "identity_project_memberships",
SecretApprovalPolicy = "secret_approval_policies",
SapApprover = "sap_approvers", // sap: secret approval policy
SecretApprovalRequest = "secret_approval_requests",
SarReviewer = "sar_reviewers",
SarSecret = "sa_request_secrets",
SarSecretTag = "sa_request_secret_tags",
// junction tables
JnSecretTag = "secret_tag_junction",
JnSecretVersionTag = "secret_version_tag_junction"
}

View File

@@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SaRequestSecretTagsSchema = z.object({
id: z.string().uuid(),
secretId: z.string().uuid(),
tagId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TSaRequestSecretTags = z.infer<typeof SaRequestSecretTagsSchema>;
export type TSaRequestSecretTagsInsert = Omit<TSaRequestSecretTags, TImmutableDBKeys>;
export type TSaRequestSecretTagsUpdate = Partial<Omit<TSaRequestSecretTags, TImmutableDBKeys>>;

View File

@@ -0,0 +1,39 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SaRequestSecretsSchema = z.object({
id: z.string().uuid(),
version: z.number().default(1).nullable().optional(),
secretBlindIndex: z.string(),
secretKeyCiphertext: z.string(),
secretKeyIV: z.string(),
secretKeyTag: z.string(),
secretValueCiphertext: z.string(),
secretValueIV: z.string(),
secretValueTag: z.string(),
secretCommentCiphertext: z.string().nullable().optional(),
secretCommentIV: z.string().nullable().optional(),
secretCommentTag: z.string().nullable().optional(),
secretReminderNotice: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
algorithm: z.string().default('aes-256-gcm'),
keyEncoding: z.string().default('utf8'),
metadata: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
requestId: z.string().uuid(),
op: z.string(),
secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional(),
});
export type TSaRequestSecrets = z.infer<typeof SaRequestSecretsSchema>;
export type TSaRequestSecretsInsert = Omit<TSaRequestSecrets, TImmutableDBKeys>;
export type TSaRequestSecretsUpdate = Partial<Omit<TSaRequestSecrets, TImmutableDBKeys>>;

View File

@@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SapApproversSchema = z.object({
id: z.string().uuid(),
approverId: z.string().uuid(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TSapApprovers = z.infer<typeof SapApproversSchema>;
export type TSapApproversInsert = Omit<TSapApprovers, TImmutableDBKeys>;
export type TSapApproversUpdate = Partial<Omit<TSapApprovers, TImmutableDBKeys>>;

View File

@@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SarReviewersSchema = z.object({
id: z.string().uuid(),
member: z.string().uuid(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TSarReviewers = z.infer<typeof SarReviewersSchema>;
export type TSarReviewersInsert = Omit<TSarReviewers, TImmutableDBKeys>;
export type TSarReviewersUpdate = Partial<Omit<TSarReviewers, TImmutableDBKeys>>;

View File

@@ -0,0 +1,22 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesSchema = z.object({
id: z.string().uuid(),
name: z.string(),
secretPath: z.string().nullable().optional(),
approvals: z.number().default(1),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
export type TSecretApprovalPoliciesInsert = Omit<TSecretApprovalPolicies, TImmutableDBKeys>;
export type TSecretApprovalPoliciesUpdate = Partial<Omit<TSecretApprovalPolicies, TImmutableDBKeys>>;

View File

@@ -0,0 +1,28 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalRequestsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
hasMerged: z.boolean().default(false),
status: z.string().default("open"),
conflicts: z.unknown().nullable().optional(),
slug: z.string(),
folderId: z.string().uuid(),
statusChangeBy: z.string().uuid().optional().nullable(),
committerId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
export type TSecretApprovalRequestsInsert = Omit<TSecretApprovalRequests, TImmutableDBKeys>;
export type TSecretApprovalRequestsUpdate = Partial<
Omit<TSecretApprovalRequests, TImmutableDBKeys>
>;

View File

@@ -1,8 +1,10 @@
import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerProjectRoleRouter, { prefix: "/workspace" });
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
};

View File

@@ -0,0 +1,154 @@
import { z } from "zod";
import { nanoid } from "nanoid";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
schema: {
body: z
.object({
workspaceId: z.string(),
name: z.string().optional(),
environment: z.string(),
secretPath: z.string().optional().nullable(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
response: {
200: z.object({
approval: sapPubSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.createSap({
actor: req.permission.type,
actorId: req.permission.id,
projectId: req.body.workspaceId,
...req.body,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
});
return { approval };
}
});
server.route({
url: "/:sapId",
method: "PATCH",
schema: {
params: z.object({
sapId: z.string()
}),
body: z
.object({
name: z.string().optional(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
secretPath: z.string().optional().nullable()
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
response: {
200: z.object({
approval: sapPubSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.updateSap({
actor: req.permission.type,
actorId: req.permission.id,
...req.body,
secretPolicyId: req.params.sapId
});
return { approval };
}
});
server.route({
url: "/:sapId",
method: "DELETE",
schema: {
params: z.object({
sapId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.deleteSap({
actor: req.permission.type,
actorId: req.permission.id,
secretPolicyId: req.params.sapId
});
return { approval };
}
});
server.route({
url: "/",
method: "GET",
schema: {
querystring: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
approvals: sapPubSchema.merge(z.object({approvers:z.string().array()})).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approvals = await server.services.secretApprovalPolicy.getSapByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
projectId: req.query.workspaceId
});
return { approvals };
}
});
server.route({
url: "/board",
method: "GET",
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim()
}),
response: {
200: z.object({
policy: sapPubSchema.merge(z.object({approvers:z.string().array()})).optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const policy = await server.services.secretApprovalPolicy.getSapOfFolder({
actor: req.permission.type,
actorId: req.permission.id,
projectId: req.query.workspaceId,
...req.query
});
return { policy };
}
});
};

View File

@@ -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;
};

View File

@@ -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 };
};

View File

@@ -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
};
};

View File

@@ -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">;

View File

@@ -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;
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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
};
};

View File

@@ -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'>

View File

@@ -15,6 +15,21 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
...dal
});
export type TFindFilter<R extends {} = any> = Partial<R> & {
$in?: Partial<{ [K in keyof R]: R[K][] }>;
};
export const buildFindFilter =
<R extends {} = any>({ $in, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => {
bd.where(filter);
if ($in) {
Object.entries($in).forEach(([key, val]) => {
bd.whereIn(key as any, val as any);
});
}
return bd;
};
// What is ormify
// It is to inject typical operations like find, findOne, update, delete, create
// This will avoid writing most common ones each time
@@ -45,9 +60,10 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
throw new DatabaseError({ error, name: "Find one" });
}
},
find: (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
find: async (filter: TFindFilter<Tables[Tname]["base"]>, tx?: Knex) => {
try {
return (tx || db)(tableName).where(filter);
const res = await (tx || db)(tableName).where(buildFindFilter(filter));
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}

View File

@@ -2,7 +2,7 @@ export const mergeOneToManyRelation = <
T extends Record<string, any>,
Pk extends keyof T,
P extends Record<string, any>,
C extends Record<string, any>,
C extends any,
Ck extends string = "child"
>(
data: T[],
@@ -29,3 +29,61 @@ export const mergeOneToManyRelation = <
}
return groupedRecord;
};
export type TSqlPackRelationships<
T extends Record<string, any>,
P extends Record<string, any>,
C extends TChildMapper<T>[]
> = {
data: T[];
key: keyof T;
parentMapper: (arg: T) => P;
childrenMapper: C;
};
export type TChildMapper<T extends {}, U extends string = string, R extends unknown = unknown> = {
key: keyof T;
label: U;
mapper: (arg: T) => R;
};
type MappedRecord<T extends TChildMapper<any>> = {
[K in T["label"]]: ReturnType<Extract<T, { label: K }>["mapper"]>[];
};
export const sqlNestRelationships = <
T extends Record<string, any> = {},
P extends Record<string, any> = {},
C extends TChildMapper<T>[] = TChildMapper<T>[]
>({
data,
key,
parentMapper,
childrenMapper
}: TSqlPackRelationships<T, P, C>) => {
const parentLookup = new Set<string>();
const childLookUp = new Set<string>();
const recordsOrder: string[] = [];
type Cm = MappedRecord<(typeof childrenMapper)[number]>;
const recordsGroupedByPk: Record<string, P & Cm> = {};
data.forEach((el) => {
const pk = el[key];
if (!parentLookup.has(pk)) {
recordsGroupedByPk[pk] = parentMapper(el) as P & Cm;
recordsOrder.push(pk);
parentLookup.add(pk);
}
childrenMapper.forEach(({ label, mapper, key: cKey }) => {
const ck = `${pk}-${label}-${el[cKey]}`;
if (!childLookUp.has(ck)) {
if (!recordsGroupedByPk[pk][label]) recordsGroupedByPk[pk][label as keyof Cm] = [] as any;
const val = mapper(el);
if (typeof val !== "undefined") recordsGroupedByPk[pk][label].push(val);
childLookUp.add(ck);
}
});
});
return recordsOrder.map((pkId) => recordsGroupedByPk[pkId]);
};

View File

@@ -0,0 +1,4 @@
import { customAlphabet } from "nanoid";
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
export const alphaNumericNanoId = customAlphabet(SLUG_ALPHABETS, 10);

View File

@@ -0,0 +1,7 @@
import path from "node:path";
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some((char) => normalizedPath.includes(char));
}

View File

@@ -69,6 +69,9 @@ import { injectPermission } from "../plugins/auth/inject-permission";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
import { secretApprovalPolicyDalFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { sapApproverDalFactory } from "@app/ee/services/secret-approval-policy/sap-approver-dal";
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
export const registerRoutes = async (
server: FastifyZodProvider,
@@ -113,9 +116,18 @@ export const registerRoutes = async (
// ee db layer ops
const permissionDal = permissionDalFactory(db);
const sapApproverDal = sapApproverDalFactory(db);
const secretApprovalPolicyDal = secretApprovalPolicyDalFactory(db);
// ee services
const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal });
const sapService = secretApprovalPolicyServiceFactory({
projectMembershipDal,
projectEnvDal,
sapApproverDal,
permissionService,
secretApprovalPolicyDal
});
// service layers
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
@@ -274,7 +286,8 @@ export const registerRoutes = async (
identity: identityService,
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityUa: identityUaService
identityUa: identityUaService,
secretApprovalPolicy: sapService
});
server.decorate<FastifyZodProvider["store"]>("store", {

View File

@@ -1,4 +1,6 @@
import { IntegrationAuthsSchema } from "@app/db/schemas";
import { z } from "zod";
import { IntegrationAuthsSchema, SecretApprovalPoliciesSchema } from "@app/db/schemas";
// sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod
@@ -13,3 +15,14 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
createdAt: true,
updatedAt: true
});
export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
z.object({
environment: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
projectId: z.string()
})
);

View File

@@ -44,6 +44,23 @@ type TSecretServiceFactoryDep = {
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
export const generateSecretBlindIndexBySalt = async (
secretName: string,
secretBlindIndexDoc: TSecretBlindIndexes
) => {
const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({
secretName,
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
encryptionKey: appCfg.ENCRYPTION_KEY,
tag: secretBlindIndexDoc.saltTag,
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
iv: secretBlindIndexDoc.saltIV
});
return secretBlindIndex;
};
export const secretServiceFactory = ({
secretDal,
secretTagDal,
@@ -52,23 +69,6 @@ export const secretServiceFactory = ({
secretBlindIndexDal,
permissionService
}: TSecretServiceFactoryDep) => {
const generateSecretBlindIndexBySalt = async (
secretName: string,
secretBlindIndexDoc: TSecretBlindIndexes
) => {
const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({
secretName,
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
encryptionKey: appCfg.ENCRYPTION_KEY,
tag: secretBlindIndexDoc.saltTag,
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
iv: secretBlindIndexDoc.saltIV
});
return secretBlindIndex;
};
// utility function to get secret blind index data
const generateSecretBlindIndexByName = async (projectId: string, secretName: string) => {
const appCfg = getConfig();
@@ -516,7 +516,8 @@ export const secretServiceFactory = ({
type
}))
);
if (secretsWithNewName.length) throw new BadRequestError({ message: "Secret not found" });
if (secretsWithNewName.length)
throw new BadRequestError({ message: "Secret with new name already exist" });
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, TSecrets>>(
(prev, curr) => {

View File

@@ -1,10 +1,42 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TSecretVersions, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { Knex } from "knex";
export type TSecretVersionDalFactory = ReturnType<typeof secretVersionDalFactory>;
export const secretVersionDalFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion);
return secretVersionOrm;
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
try {
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(
TableName.SecretVersion
)
.where("folderId", folderId)
.whereIn(`${TableName.SecretVersion}.secretId`, secretIds)
.join(
(tx || db)(TableName.SecretVersion)
.groupBy("secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersion}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersion}.version`,
"latestVersion.max"
);
}
);
return docs.reduce<Record<string, TSecretVersions>>(
(prev, curr) => ({ ...prev, [curr.secretId]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
}
};
return { ...secretVersionOrm, findLatestVersionMany };
};

View File

@@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => {
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
const { data } = await apiRequest.post("/api/ee/v1/secret-approvals", {
environment,
workspaceId,
approvals,
@@ -31,7 +31,7 @@ export const useUpdateSecretApprovalPolicy = () => {
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
const { data } = await apiRequest.patch(`/api/ee/v1/secret-approvals/${id}`, {
approvals,
approvers,
secretPath,
@@ -50,7 +50,7 @@ export const useDeleteSecretApprovalPolicy = () => {
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
mutationFn: async ({ id }) => {
const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`);
const { data } = await apiRequest.delete(`/api/ee/v1/secret-approvals/${id}`);
return data;
},
onSuccess: (_, { workspaceId }) => {

View File

@@ -19,7 +19,7 @@ export const secretApprovalKeys = {
const fetchApprovalPolicies = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>(
"/api/v1/secret-approvals",
"/api/ee/v1/secret-approvals",
{ params: { workspaceId } }
);
return data.approvals;
@@ -49,7 +49,7 @@ const fetchApprovalPolicyOfABoard = async (
secretPath: string
) => {
const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>(
"/api/v1/secret-approvals/board",
"/api/ee/v1/secret-approvals/board",
{ params: { workspaceId, environment, secretPath } }
);
return data.policy || "";

View File

@@ -1,8 +1,11 @@
import { WorkspaceEnv } from "../workspace/types";
export type TSecretApprovalPolicy = {
id: string;
workspace: string;
name: string;
environment: string;
envId: string;
environment: WorkspaceEnv;
secretPath?: string;
approvers: string[];
approvals: number;

View File

@@ -11,8 +11,6 @@ export type WorkspaceEnv = {
id: string;
name: string;
slug: string;
isReadDenied: boolean;
isWriteDenied: boolean;
};
export type WorkspaceTag = { id: string; name: string; slug: string };

View File

@@ -41,7 +41,7 @@ export const SecretApprovalPolicyRow = ({
return (
<Tr>
<Td>{policy.name}</Td>
<Td>{policy.environment}</Td>
<Td>{policy.environment.slug}</Td>
<Td>{policy.secretPath || "*"}</Td>
<Td>
<DropdownMenu

View File

@@ -62,7 +62,7 @@ export const SecretPolicyForm = ({
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: editValues
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
});
const { currentWorkspace } = useWorkspace();
const { createNotification } = useNotificationContext();

View File

@@ -63,7 +63,7 @@ export const SecretApprovalRequest = () => {
(prev, curr) => ({ ...prev, [curr.id]: curr }),
{}
);
const myMembershipId = members?.find(({ user }) => user.id === presentUser.id)?.id;
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
const isSecretApprovalScreen = Boolean(selectedApproval);
const handleGoBackSecretRequestDetail = () => {