From 992a82015aac946f6882e4313c37d6874b650182 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:05 -0700 Subject: [PATCH] Feat: Request access --- .../access-approval-request-dal.ts | 265 ++++++++++++++++++ .../access-approval-request-reviewer-dal.ts | 10 + .../access-approval-request-secret-dal.ts | 230 +++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-dal.ts create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts new file mode 100644 index 000000000..78fef7c8a --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -0,0 +1,265 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; + +import { ApprovalStatus } from "./access-approval-request-types"; + +export type TAccessApprovalRequestDALFactory = ReturnType; + +export const accessApprovalRequestDALFactory = (db: TDbClient) => { + const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest); + + const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => { + try { + const docs = await db(TableName.AccessApprovalRequest) + .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) + + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + .leftJoin( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId") + ) + + .select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)) + + .select( + db.ref("projectId").withSchema(TableName.Environment), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + + .select( + db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"), + db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus") + ) + + .select( + db + .ref("projectMembershipId") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeMembershipId"), + db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessEndTime"), + + db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") + ); + + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc), + projectId: doc.projectId, + environment: doc.envSlug, + environmentName: doc.envName, + policy: { + id: doc.policyId, + name: doc.policyName, + approvals: doc.policyApprovals, + secretPath: doc.policySecretPath, + envId: doc.policyEnvId + }, + privilege: doc.privilegeId + ? { + membershipId: doc.privilegeMembershipId, + isTemporary: doc.privilegeIsTemporary, + temporaryMode: doc.privilegeTemporaryMode, + temporaryRange: doc.privilegeTemporaryRange, + temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime, + temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime, + permissions: doc.privilegePermissions + } + : null, + + isApproved: !!doc.privilegeId + }), + 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 (!formattedDocs) return []; + + return formattedDocs.map((doc) => ({ + ...doc, + policy: { ...doc.policy, approvers: doc.approvers } + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" }); + } + }; + + const findQuery = (filter: TFindFilter, tx: Knex) => + tx(TableName.AccessApprovalRequest) + .where(filter) + .join( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + + .join( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"), + tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"), + tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + tx.ref("projectId").withSchema(TableName.Environment), + tx.ref("slug").withSchema(TableName.Environment).as("environment"), + tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover) + ); + + const findById = async (id: string, tx?: Knex) => { + try { + const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db); + const docs = await sql; + const formatedDoc = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...AccessApprovalRequestsSchema.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: "FindByIdAccessApprovalRequest" }); + } + }; + + const getCount = async ({ projectId }: { projectId: string }) => { + try { + const accessRequests = await db(TableName.AccessApprovalRequest) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + + .where(`${TableName.Environment}.projectId`, projectId) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) + .select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId")); + + const formattedRequests = sqlNestRelationships({ + data: accessRequests, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc) + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + } + ] + }); + + // an approval is pending if there is no reviewer rejections and no privilege ID is set + const pendingApprovals = formattedRequests.filter( + (req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) + ); + + // an approval is finalized if there are any rejections or a privilege ID is set + const finalizedApprovals = formattedRequests.filter( + (req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) + ); + + return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length }; + } catch (error) { + throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" }); + } + }; + + return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount }; +}; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts new file mode 100644 index 000000000..251015b22 --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAccessApprovalRequestReviewerDALFactory = ReturnType; + +export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => { + const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer); + return secretApprovalRequestReviewerOrm; +}; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts new file mode 100644 index 000000000..d458d58ff --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts @@ -0,0 +1,230 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { + SecretApprovalRequestsSecretsSchema, + TableName, + TSecretApprovalRequestsSecrets, + TSecretTags +} from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +export type TAccessApprovalRequestSecretDALFactory = ReturnType; + +export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { + const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); + const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); + + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { + try { + const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + if (data.length === 0) return []; + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedApprovalSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + 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 }) => ({ + // eslint-disable-next-line + id, + // eslint-disable-next-line + name, + // eslint-disable-next-line + slug, + // eslint-disable-next-line + color + }) + } + ] + } + ] + }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ + ...el, + secret: secret?.[0], + secretVersion: secretVersion?.[0] + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindByRequestId" }); + } + }; + return { + ...accessApprovalRequestSecretOrm, + findByRequestId, + bulkUpdateNoVersionIncrement, + insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany + }; +};