feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
@@ -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 };
};
@@ -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;
};
@@ -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">;