mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 05:19:49 +00:00
feat: changed backend-pg to backend
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretApprovalRequestsSchema, TableName, TSecretApprovalRequests } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
ormify,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
stripUndefinedInWhere,
|
||||
TFindFilter
|
||||
} from "@app/lib/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.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SecretApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
tx.ref("member").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||
);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...SecretApprovalRequestsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
environment: el.environment,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) =>
|
||||
member ? { member, status } : undefined
|
||||
},
|
||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
||||
]
|
||||
});
|
||||
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 docs = await (tx || db)
|
||||
.with(
|
||||
"temp",
|
||||
(tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.where({ projectId })
|
||||
.andWhere((bd) =>
|
||||
bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
)
|
||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||
.count("status")
|
||||
)
|
||||
.select("status")
|
||||
.from("temp")
|
||||
.groupBy("status")
|
||||
.count("status");
|
||||
|
||||
return {
|
||||
open: parseInt(
|
||||
(docs.find(({ status }) => status === RequestState.Open)?.count as string) || "0",
|
||||
10
|
||||
),
|
||||
closed: parseInt(
|
||||
(docs.find(({ status }) => status === RequestState.Closed)?.count as string) || "0",
|
||||
10
|
||||
)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestCount" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
{
|
||||
status,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
membershipId
|
||||
}: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||
// this is the place u wanna look at.
|
||||
const query = (tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SecretApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestSecret,
|
||||
`${TableName.SecretApprovalRequestSecret}.requestId`,
|
||||
`${TableName.SecretApprovalRequest}.id`
|
||||
)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
[`${TableName.Environment}.slug` as "slug"]: environment,
|
||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||
committerId: committer
|
||||
})
|
||||
)
|
||||
.andWhere((bd) =>
|
||||
bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||
db.ref("op").withSchema(TableName.SecretApprovalRequestSecret).as("commitOp"),
|
||||
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecret).as("commitSecretId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecret).as("commitId"),
|
||||
db.raw(
|
||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||
),
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||
)
|
||||
.orderBy("createdAt", "desc");
|
||||
|
||||
const docs = await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", offset)
|
||||
.andWhere("w.rank", "<", offset + limit);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...SecretApprovalRequestsSchema.parse(el),
|
||||
environment: el.environment,
|
||||
projectId: el.projectId,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) =>
|
||||
member ? { member, status: s } : undefined
|
||||
},
|
||||
{
|
||||
key: "approverId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverId }) => approverId
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
label: "commits" as const,
|
||||
mapper: ({ commitSecretId: secretId, commitId: id, commitOp: op }) => ({
|
||||
op,
|
||||
id,
|
||||
secretId
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
return formatedDoc.map((el) => ({
|
||||
...el,
|
||||
policy: { ...el.policy, approvers: el.approvers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindSAR" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretApprovalRequestOrm, findById, findProjectRequestCount, findByProjectId };
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSecretApprovalRequestReviewerDALFactory = ReturnType<
|
||||
typeof secretApprovalRequestReviewerDALFactory
|
||||
>;
|
||||
|
||||
export const secretApprovalRequestReviewerDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestReviewerOrm = ormify(db, TableName.SecretApprovalRequestReviewer);
|
||||
return secretApprovalRequestReviewerOrm;
|
||||
};
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretApprovalRequestsSecretsSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
export type TSecretApprovalRequestSecretDALFactory = ReturnType<
|
||||
typeof secretApprovalRequestSecretDALFactory
|
||||
>;
|
||||
|
||||
export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
||||
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
||||
|
||||
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)({
|
||||
secVerTag: TableName.SecretTag
|
||||
})
|
||||
.from(TableName.SecretApprovalRequestSecret)
|
||||
.where({ requestId })
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestSecretTag,
|
||||
`${TableName.SecretApprovalRequestSecret}.id`,
|
||||
`${TableName.SecretApprovalRequestSecretTag}.secretId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretTag,
|
||||
`${TableName.SecretApprovalRequestSecretTag}.tagId`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.Secret,
|
||||
`${TableName.SecretApprovalRequestSecret}.secretId`,
|
||||
`${TableName.Secret}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretVersion,
|
||||
`${TableName.SecretVersion}.id`,
|
||||
`${TableName.SecretApprovalRequestSecret}.secretVersion`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretVersionTag,
|
||||
`${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`,
|
||||
`${TableName.SecretVersion}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
db.ref(TableName.SecretTag).as("secVerTag"),
|
||||
`${TableName.SecretVersionTag}.${TableName.SecretTag}Id`,
|
||||
db.ref("id").withSchema("secVerTag")
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequestSecret))
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
)
|
||||
.select(
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
|
||||
db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"),
|
||||
db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"),
|
||||
db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"),
|
||||
db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"),
|
||||
db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"),
|
||||
db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"),
|
||||
db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"),
|
||||
db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"),
|
||||
db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"),
|
||||
db
|
||||
.ref("secretCommentCiphertext")
|
||||
.withSchema(TableName.Secret)
|
||||
.as("orgSecCommentCiphertext")
|
||||
)
|
||||
.select(
|
||||
db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"),
|
||||
db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"),
|
||||
db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"),
|
||||
db
|
||||
.ref("secretKeyCiphertext")
|
||||
.withSchema(TableName.SecretVersion)
|
||||
.as("secVerKeyCiphertext"),
|
||||
db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"),
|
||||
db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"),
|
||||
db
|
||||
.ref("secretValueCiphertext")
|
||||
.withSchema(TableName.SecretVersion)
|
||||
.as("secVerValueCiphertext"),
|
||||
db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"),
|
||||
db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"),
|
||||
db
|
||||
.ref("secretCommentCiphertext")
|
||||
.withSchema(TableName.SecretVersion)
|
||||
.as("secVerCommentCiphertext")
|
||||
);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: doc,
|
||||
key: "id",
|
||||
parentMapper: (data) =>
|
||||
SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "secretId",
|
||||
label: "secret" as const,
|
||||
mapper: ({
|
||||
orgSecKeyIV,
|
||||
orgSecKeyTag,
|
||||
orgSecValueIV,
|
||||
orgSecVersion,
|
||||
orgSecValueTag,
|
||||
orgSecCommentIV,
|
||||
orgSecBlindIndex,
|
||||
orgSecCommentTag,
|
||||
orgSecKeyCiphertext,
|
||||
orgSecValueCiphertext,
|
||||
orgSecCommentCiphertext,
|
||||
secretId
|
||||
}) =>
|
||||
secretId
|
||||
? {
|
||||
id: secretId,
|
||||
version: orgSecVersion,
|
||||
secretBlindIndex: orgSecBlindIndex,
|
||||
secretKeyIV: orgSecKeyIV,
|
||||
secretKeyTag: orgSecKeyTag,
|
||||
secretKeyCiphertext: orgSecKeyCiphertext,
|
||||
secretValueIV: orgSecValueIV,
|
||||
secretValueTag: orgSecValueTag,
|
||||
secretValueCiphertext: orgSecValueCiphertext,
|
||||
secretCommentIV: orgSecCommentIV,
|
||||
secretCommentTag: orgSecCommentTag,
|
||||
secretCommentCiphertext: orgSecCommentCiphertext
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
key: "secretVersion",
|
||||
label: "secretVersion" as const,
|
||||
mapper: ({
|
||||
secVerCommentIV,
|
||||
secVerCommentCiphertext,
|
||||
secVerCommentTag,
|
||||
secVerValueCiphertext,
|
||||
secVerKeyIV,
|
||||
secVerKeyTag,
|
||||
secVerValueIV,
|
||||
secretVersion,
|
||||
secVerValueTag,
|
||||
secVerKeyCiphertext,
|
||||
secVerVersion
|
||||
}) =>
|
||||
secretVersion
|
||||
? {
|
||||
version: secVerVersion,
|
||||
id: secretVersion,
|
||||
secretKeyIV: secVerKeyIV,
|
||||
secretKeyTag: secVerKeyTag,
|
||||
secretKeyCiphertext: secVerKeyCiphertext,
|
||||
secretValueIV: secVerValueIV,
|
||||
secretValueTag: secVerValueTag,
|
||||
secretValueCiphertext: secVerValueCiphertext,
|
||||
secretCommentIV: secVerCommentIV,
|
||||
secretCommentTag: secVerCommentTag,
|
||||
secretCommentCiphertext: secVerCommentCiphertext
|
||||
}
|
||||
: undefined,
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({
|
||||
secVerTagId: id,
|
||||
secVerTagName: name,
|
||||
secVerTagSlug: slug,
|
||||
secVerTagColor: color
|
||||
}) => ({
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
|
||||
...el,
|
||||
secret: secret?.[0],
|
||||
secretVersion: secretVersion?.[0]
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByRequestId" });
|
||||
}
|
||||
};
|
||||
return {
|
||||
...secretApprovalRequestSecretOrm,
|
||||
findByRequestId,
|
||||
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,658 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
SecretType,
|
||||
TSecretApprovalRequestsSecretsInsert
|
||||
} from "@app/db/schemas";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy, pick, unique } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
|
||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
||||
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
CommitType,
|
||||
RequestState,
|
||||
TApprovalRequestCountDTO,
|
||||
TGenerateSecretApprovalRequestDTO,
|
||||
TListApprovalsDTO,
|
||||
TMergeSecretApprovalRequestDTO,
|
||||
TReviewRequestDTO,
|
||||
TSecretApprovalDetailsDTO,
|
||||
TStatusChangeDTO
|
||||
} from "./secret-approval-request-types";
|
||||
|
||||
type TSecretApprovalRequestServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
|
||||
secretApprovalRequestSecretDAL: TSecretApprovalRequestSecretDALFactory;
|
||||
secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
"findBySecretPath" | "findById" | "findSecretPathByFolderIds"
|
||||
>;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById">;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
|
||||
secretService: Pick<
|
||||
TSecretServiceFactory,
|
||||
| "fnSecretBulkInsert"
|
||||
| "fnSecretBulkUpdate"
|
||||
| "fnSecretBlindIndexCheck"
|
||||
| "fnSecretBulkDelete"
|
||||
| "fnSecretBlindIndexCheckV2"
|
||||
>;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<
|
||||
typeof secretApprovalRequestServiceFactory
|
||||
>;
|
||||
|
||||
export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequestDAL,
|
||||
folderDAL,
|
||||
secretTagDAL,
|
||||
secretApprovalRequestReviewerDAL,
|
||||
secretApprovalRequestSecretDAL,
|
||||
secretBlindIndexDAL,
|
||||
permissionService,
|
||||
snapshotService,
|
||||
secretService,
|
||||
secretVersionDAL,
|
||||
secretQueueService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE)
|
||||
throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor as ActorType.USER,
|
||||
actorId,
|
||||
projectId
|
||||
);
|
||||
|
||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
|
||||
return count;
|
||||
};
|
||||
|
||||
const getSecretApprovals = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
status,
|
||||
environment,
|
||||
committer,
|
||||
limit,
|
||||
offset
|
||||
}: TListApprovalsDTO) => {
|
||||
if (actor === ActorType.SERVICE)
|
||||
throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
status,
|
||||
membershipId: membership.id,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
return approvals;
|
||||
};
|
||||
|
||||
const getSecretApprovalDetails = async ({ actor, actorId, id }: TSecretApprovalDetailsDTO) => {
|
||||
if (actor === ActorType.SERVICE)
|
||||
throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
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 secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
|
||||
secretApprovalRequest.folderId
|
||||
]);
|
||||
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", 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 secretApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||
const review = await secretApprovalRequestReviewerDAL.findOne(
|
||||
{
|
||||
requestId: secretApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!review) {
|
||||
return secretApprovalRequestReviewerDAL.create(
|
||||
{
|
||||
status,
|
||||
requestId: secretApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return secretApprovalRequestReviewerDAL.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 === RequestState.Closed && status === RequestState.Closed)
|
||||
throw new BadRequestError({ message: "Approval request is already closed" });
|
||||
if (secretApprovalRequest.status === RequestState.Open && status === RequestState.Open)
|
||||
throw new BadRequestError({ message: "Approval request is already open" });
|
||||
|
||||
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
|
||||
status,
|
||||
statusChangeBy: membership.id
|
||||
});
|
||||
return { ...secretApprovalRequest, ...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, projectId } = secretApprovalRequest;
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
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 as ApprovalStatus }),
|
||||
{}
|
||||
);
|
||||
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 secretApprovalRequestSecretDAL.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 { secsGroupedByBlindIndex: conflictGroupByBlindIndex } =
|
||||
await secretService.fnSecretBlindIndexCheckV2({
|
||||
folderId,
|
||||
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => ({ secretBlindIndex }))
|
||||
});
|
||||
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 { secsGroupedByBlindIndex: conflictGroupByBlindIndex } =
|
||||
await secretService.fnSecretBlindIndexCheckV2({
|
||||
folderId,
|
||||
inputSecrets: secretUpdationCommits
|
||||
.filter(
|
||||
({ secretBlindIndex, secret }) =>
|
||||
secret && secret.secretBlindIndex !== secretBlindIndex
|
||||
)
|
||||
.map(({ secretBlindIndex }) => ({ secretBlindIndex }))
|
||||
});
|
||||
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 secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const newSecrets = secretCreationCommits.length
|
||||
? await secretService.fnSecretBulkInsert({
|
||||
tx,
|
||||
folderId,
|
||||
inputSecrets: secretCreationCommits.map((el) => ({
|
||||
...pick(el, [
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
"secretValueIV",
|
||||
"secretValueTag",
|
||||
"secretValueCiphertext",
|
||||
"secretKeyCiphertext",
|
||||
"secretKeyTag",
|
||||
"secretKeyIV",
|
||||
"metadata",
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"algorithm",
|
||||
"keyEncoding",
|
||||
"secretBlindIndex"
|
||||
]),
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
version: 1,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
})
|
||||
: [];
|
||||
const updatedSecrets = secretUpdationCommits.length
|
||||
? await secretService.fnSecretBulkUpdate({
|
||||
folderId,
|
||||
projectId,
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => ({
|
||||
filter: {
|
||||
id: el.secretId as string, // this null check is already checked at top on conflict strategy
|
||||
type: SecretType.Shared
|
||||
},
|
||||
data: {
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
...pick(el, [
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
"secretValueIV",
|
||||
"secretValueTag",
|
||||
"secretValueCiphertext",
|
||||
"secretKeyCiphertext",
|
||||
"secretKeyTag",
|
||||
"secretKeyIV",
|
||||
"metadata",
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"secretBlindIndex"
|
||||
])
|
||||
}
|
||||
}))
|
||||
})
|
||||
: [];
|
||||
const deletedSecret = secretDeletionCommits.length
|
||||
? await secretService.fnSecretBulkDelete({
|
||||
projectId,
|
||||
folderId,
|
||||
tx,
|
||||
actorId: "",
|
||||
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => ({
|
||||
secretBlindIndex,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
})
|
||||
: [];
|
||||
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
||||
secretApprovalRequest.id,
|
||||
{
|
||||
conflicts: JSON.stringify(conflicts),
|
||||
hasMerged: true,
|
||||
status: RequestState.Closed,
|
||||
statusChangeBy: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
return {
|
||||
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
|
||||
approval: updatedSecretApproval
|
||||
};
|
||||
});
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
const folder = await folderDAL.findById(folderId);
|
||||
// TODO(akhilmhdh-pg): change query to do secret path from folder
|
||||
await secretQueueService.syncSecrets({
|
||||
projectId,
|
||||
secretPath: "/",
|
||||
environment: folder?.environment.envSlug as string
|
||||
});
|
||||
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,
|
||||
actorId,
|
||||
actor,
|
||||
policy,
|
||||
projectId,
|
||||
secretPath,
|
||||
environment
|
||||
}: TGenerateSecretApprovalRequestDTO) => {
|
||||
if (actor === ActorType.SERVICE)
|
||||
throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
||||
const folderId = folder.id;
|
||||
|
||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||
if (!blindIndexCfg)
|
||||
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||
|
||||
const commits: Omit<TSecretApprovalRequestsSecretsInsert, "requestId">[] = [];
|
||||
const commitTagIds: Record<string, string[]> = {};
|
||||
// for created secret approval change
|
||||
const createdSecrets = data[CommitType.Create];
|
||||
if (createdSecrets && createdSecrets?.length) {
|
||||
const { keyName2BlindIndex } = await secretService.fnSecretBlindIndexCheck({
|
||||
inputSecrets: createdSecrets,
|
||||
folderId,
|
||||
isNew: true,
|
||||
blindIndexCfg
|
||||
});
|
||||
|
||||
commits.push(
|
||||
...createdSecrets.map(({ secretName, ...el }) => ({
|
||||
...el,
|
||||
op: CommitType.Create as const,
|
||||
version: 1,
|
||||
secretBlindIndex: keyName2BlindIndex[secretName],
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.BASE64
|
||||
}))
|
||||
);
|
||||
createdSecrets.forEach(({ tagIds, secretName }) => {
|
||||
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
||||
});
|
||||
}
|
||||
// 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 { keyName2BlindIndex, secrets: secretsToBeUpdated } =
|
||||
await secretService.fnSecretBlindIndexCheck({
|
||||
inputSecrets: updatedSecrets,
|
||||
folderId,
|
||||
isNew: false,
|
||||
blindIndexCfg
|
||||
});
|
||||
|
||||
// now find any secret that needs to update its name
|
||||
// same process as above
|
||||
const nameUpdatedSecrets = updatedSecrets.filter(({ newSecretName }) =>
|
||||
Boolean(newSecretName)
|
||||
);
|
||||
const { keyName2BlindIndex: newKeyName2BlindIndex } =
|
||||
await secretService.fnSecretBlindIndexCheck({
|
||||
inputSecrets: nameUpdatedSecrets,
|
||||
folderId,
|
||||
isNew: true,
|
||||
blindIndexCfg
|
||||
});
|
||||
|
||||
const secsGroupedByBlindIndex = groupBy(secretsToBeUpdated, (el) => el.secretBlindIndex);
|
||||
const updatedSecretIds = updatedSecrets.map(
|
||||
(el) => secsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id
|
||||
);
|
||||
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
|
||||
folderId,
|
||||
updatedSecretIds
|
||||
);
|
||||
commits.push(
|
||||
...updatedSecrets.map(({ newSecretName, secretName, tagIds, ...el }) => {
|
||||
const secretId = secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0].id;
|
||||
const secretBlindIndex =
|
||||
newSecretName && newKeyName2BlindIndex[newSecretName]
|
||||
? newKeyName2BlindIndex?.[secretName]
|
||||
: keyName2BlindIndex[secretName];
|
||||
// add tags
|
||||
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
...el,
|
||||
op: CommitType.Update as const,
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id,
|
||||
secretBlindIndex,
|
||||
version: secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0].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 { keyName2BlindIndex, secrets } = await secretService.fnSecretBlindIndexCheck({
|
||||
inputSecrets: deletedSecrets,
|
||||
folderId,
|
||||
isNew: false,
|
||||
blindIndexCfg
|
||||
});
|
||||
const secretsGroupedByBlindIndex = groupBy(secrets, (i) => i.secretBlindIndex);
|
||||
const deletedSecretIds = deletedSecrets.map(
|
||||
(el) => secretsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id
|
||||
);
|
||||
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
|
||||
folderId,
|
||||
deletedSecretIds
|
||||
);
|
||||
commits.push(
|
||||
...deletedSecrets.map((el) => {
|
||||
const secretId = secretsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id;
|
||||
return {
|
||||
op: CommitType.Delete as const,
|
||||
...latestSecretVersions[secretId],
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!commits.length) throw new BadRequestError({ message: "Empty commits" });
|
||||
|
||||
const tagIds = unique(Object.values(commitTagIds).flat());
|
||||
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
||||
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const doc = await secretApprovalRequestDAL.create(
|
||||
{
|
||||
folderId,
|
||||
slug: alphaNumericNanoId(),
|
||||
policyId: policy.id,
|
||||
status: "open",
|
||||
hasMerged: false,
|
||||
committerId: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(
|
||||
commits.map(
|
||||
({
|
||||
version,
|
||||
op,
|
||||
secretKeyTag,
|
||||
secretKeyIV,
|
||||
keyEncoding,
|
||||
secretId,
|
||||
metadata,
|
||||
algorithm,
|
||||
secretBlindIndex,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretVersion,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretReminderNote,
|
||||
skipMultilineEncoding,
|
||||
secretCommentCiphertext,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
version,
|
||||
requestId: doc.id,
|
||||
op,
|
||||
secretKeyTag,
|
||||
secretKeyIV,
|
||||
keyEncoding,
|
||||
secretId,
|
||||
metadata,
|
||||
algorithm,
|
||||
secretBlindIndex,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretVersion,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretReminderNote,
|
||||
skipMultilineEncoding,
|
||||
secretCommentCiphertext,
|
||||
secretReminderRepeatDays
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
const commitsGroupByBlindIndex = groupBy(approvalCommits, (i) => i.secretBlindIndex);
|
||||
if (tagIds.length) {
|
||||
await secretApprovalRequestSecretDAL.insertApprovalSecretTags(
|
||||
Object.keys(commitTagIds).flatMap((blindIndex) =>
|
||||
commitTagIds[blindIndex]
|
||||
? commitTagIds[blindIndex].map((tagId) => ({
|
||||
secretId: commitsGroupByBlindIndex[blindIndex][0].id,
|
||||
tagId
|
||||
}))
|
||||
: []
|
||||
),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
return {
|
||||
generateSecretApprovalRequest,
|
||||
mergeSecretApprovalRequest,
|
||||
reviewApproval,
|
||||
updateApprovalStatus,
|
||||
getSecretApprovals,
|
||||
getSecretApprovalDetails,
|
||||
requestCount
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
TImmutableDBKeys,
|
||||
TSecretApprovalPolicies,
|
||||
TSecretApprovalRequestsSecrets
|
||||
} 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 = "close"
|
||||
}
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
type TApprovalCreateSecret = Omit<
|
||||
TSecretApprovalRequestsSecrets,
|
||||
| TImmutableDBKeys
|
||||
| "version"
|
||||
| "algorithm"
|
||||
| "keyEncoding"
|
||||
| "requestId"
|
||||
| "op"
|
||||
| "secretVersion"
|
||||
| "secretBlindIndex"
|
||||
> & {
|
||||
secretName: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
type TApprovalUpdateSecret = Partial<TApprovalCreateSecret> & {
|
||||
secretName: string;
|
||||
newSecretName?: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TGenerateSecretApprovalRequestDTO = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
policy: TSecretApprovalPolicies;
|
||||
data: {
|
||||
[CommitType.Create]?: TApprovalCreateSecret[];
|
||||
[CommitType.Update]?: TApprovalUpdateSecret[];
|
||||
[CommitType.Delete]?: { secretName: string }[];
|
||||
};
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TMergeSecretApprovalRequestDTO = {
|
||||
approvalId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TStatusChangeDTO = {
|
||||
approvalId: string;
|
||||
status: RequestState;
|
||||
} & 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">;
|
||||
Reference in New Issue
Block a user