diff --git a/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts b/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts index 9cb4c2d5b..08c8adf05 100644 --- a/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts @@ -40,6 +40,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv approvers: z.string().array(), secretPath: z.string().optional().nullable() }), + commits: z + .object({ op: z.string(), secretId: z.string().nullable().optional() }) + .array(), + environment: z.string(), reviewers: z.object({ member: z.string(), status: z.string() }).array(), approvers: z.string().array() }) @@ -105,8 +109,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv approvers: z.string().array(), secretPath: z.string().optional().nullable() }), + environment: z.string(), reviewers: z.object({ member: z.string(), status: z.string() }).array(), approvers: z.string().array(), + secretPath: z.string(), commits: SaRequestSecretsSchema.omit({ secretBlindIndex: true }) .merge( z.object({ diff --git a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index b86034c5b..50b73ff25 100644 --- a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -53,16 +53,17 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { `${TableName.SarReviewer}.requestId` ) .select(selectAllTableCols(TableName.SecretApprovalRequest)) - .select(tx.ref("member").withSchema(TableName.SarReviewer).as("reviewerMemberId")) - .select(tx.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus")) - .select(tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId")) - .select(tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName")) - .select(tx.ref("projectId").withSchema(TableName.Environment)) .select( - tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath") - ) - .select(tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")) - .select(tx.ref("approverId").withSchema(TableName.SapApprover)); + tx.ref("member").withSchema(TableName.SarReviewer).as("reviewerMemberId"), + tx.ref("status").withSchema(TableName.SarReviewer).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.SapApprover) + ); const findById = async (id: string, tx?: Knex) => { try { @@ -74,6 +75,7 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { parentMapper: (el) => ({ ...SecretApprovalRequestsSchema.parse(el), projectId: el.projectId, + environment: el.environment, policy: { id: el.policyId, name: el.policyName, @@ -193,6 +195,11 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { `${TableName.SecretApprovalRequest}.id`, `${TableName.SarReviewer}.requestId` ) + .leftJoin( + TableName.SarSecret, + `${TableName.SarSecret}.requestId`, + `${TableName.SecretApprovalRequest}.id` + ) .where( stripUndefinedInWhere({ projectId, @@ -207,23 +214,23 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) ) .select(selectAllTableCols(TableName.SecretApprovalRequest)) - .select(db.ref("projectId").withSchema(TableName.Environment)) - .select(db.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId")) - .select(db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus")) - .select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId")) - .select(db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName")) .select( + db.ref("projectId").withSchema(TableName.Environment), + db.ref("slug").withSchema(TableName.Environment).as("environment"), + db.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId"), + db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus"), + db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"), + db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"), + db.ref("op").withSchema(TableName.SarSecret).as("commitOp"), + db.ref("secretId").withSchema(TableName.SarSecret).as("commitSecretId"), + db.ref("id").withSchema(TableName.SarSecret).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.SapApprover) ) - .select( - db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath") - ) - .select( - db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") - ) - .select(db.ref("approverId").withSchema(TableName.SapApprover)) .orderBy("createdAt", "desc"); const docs = await (tx || db) @@ -232,12 +239,12 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { .from[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, @@ -253,7 +260,20 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => member ? { member, status: s } : undefined }, - { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + { + 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) => ({ diff --git a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 86262c227..d2ff4078b 100644 --- a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -41,7 +41,10 @@ type TSecretApprovalRequestServiceFactoryDep = { secretApprovalRequestDal: TSecretApprovalRequestDalFactory; sarSecretDal: TSarSecretDalFactory; sarReviewerDal: TSarReviewerDalFactory; - folderDal: Pick; + folderDal: Pick< + TSecretFolderDalFactory, + "findBySecretPath" | "findById" | "findSecretPathByFolderIds" + >; secretBlindIndexDal: Pick; snapshotService: Pick; secretVersionDal: Pick; @@ -135,7 +138,10 @@ export const secretApprovalRequestServiceFactory = ({ } const secrets = await sarSecretDal.findByRequestId(secretApprovalRequest.id); - return { ...secretApprovalRequest, commits: secrets }; + 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) => { diff --git a/backend-pg/src/services/secret-folder/secret-folder-dal.ts b/backend-pg/src/services/secret-folder/secret-folder-dal.ts index 091b82696..b11a57cc2 100644 --- a/backend-pg/src/services/secret-folder/secret-folder-dal.ts +++ b/backend-pg/src/services/secret-folder/secret-folder-dal.ts @@ -8,6 +8,7 @@ import { TSecretFoldersUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export const validateFolderName = (folderName: string) => { @@ -176,6 +177,54 @@ const sqlFindFolderByPathQuery = ( ); }; +const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: string[]) => + db + .withRecursive("parent", (baseQb) => { + // first remember our folders are connected as a link list or known as adjacency list + // Thus each node has connection to parent node + // we first find the folder given in folder id + baseQb + .from(TableName.SecretFolder) + .select(selectAllTableCols(TableName.SecretFolder)) + .select({ + // this is for root condition + // if the given folder id is root folder id then intial path is set as / instead of /root + // if not root folder the path here will be / + path: db.raw( + `CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))` + ), + child: db.raw("NULL::uuid") + }) + .join( + TableName.Environment, + `${TableName.SecretFolder}.envId`, + `${TableName.Environment}.id` + ) + .where({ projectId }) + .whereIn(`${TableName.SecretFolder}.id`, folderIds) + .union((qb) => + // then we keep going up + // until parent id is null + qb + .select(selectAllTableCols(TableName.SecretFolder)) + .select({ + // then we join join this folder name behind previous as we are going from child to parent + // the root folder check is used to avoid last / and also root name in folders + path: db.raw( + `CONCAT( CASE + WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' + ELSE CONCAT('/', secret_folders.name) + END, parent.path )` + ), + child: db.raw("COALESCE(parent.child, parent.id)") + }) + .from(TableName.SecretFolder) + .join("parent", "parent.parentId", `${TableName.SecretFolder}.id`) + ); + }) + .select("*") + .from("parent"); + export type TSecretFolderDalFactory = ReturnType; // never change this. If u do write a migration for it export const ROOT_FOLDER_NAME = "root"; @@ -220,6 +269,21 @@ export const secretFolderDalFactory = (db: TDbClient) => { } }; + // this is used to do an inverse query in folders + // that is instances in which for a given folderid find the secret path + const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => { + try { + const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds); + const rootFolders = groupBy( + folders.filter(({ parentId }) => parentId === null), + (i) => i.child || i.id // root condition then child and parent will null + ); + return folderIds.map((folderId) => rootFolders[folderId]?.[0]); + } catch (error) { + throw new DatabaseError({ error, name: "Find by secret path" }); + } + }; + const update = async (filter: Partial, data: TSecretFoldersUpdate, tx?: Knex) => { try { const folder = await (tx || db)(TableName.SecretFolder) @@ -259,5 +323,12 @@ export const secretFolderDalFactory = (db: TDbClient) => { } }; - return { ...secretFolderOrm, update, findBySecretPath, findById, findByManySecretPath }; + return { + ...secretFolderOrm, + update, + findBySecretPath, + findById, + findByManySecretPath, + findSecretPathByFolderIds + }; }; diff --git a/backend-pg/src/services/service-token/service-token-service.ts b/backend-pg/src/services/service-token/service-token-service.ts index b8a3a94ce..4b85a0cf4 100644 --- a/backend-pg/src/services/service-token/service-token-service.ts +++ b/backend-pg/src/services/service-token/service-token-service.ts @@ -55,7 +55,7 @@ export const serviceTokenServiceFactory = ({ scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); }) diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 27b615649..32fe31c6b 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -46,7 +46,7 @@ export type TSecretApprovalRequest = { id: string; slug: string; createdAt: string; - committer: string; + committerId: string; reviewers: { member: string; status: ApprovalStatus; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 02af9c1bb..90133803b 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -89,7 +89,7 @@ export const SecretApprovalRequest = () => { members={membersGroupById} approvalRequestId={selectedApproval?.id || ""} onGoBack={handleGoBackSecretRequestDetail} - committer={membersGroupById?.[selectedApproval?.committer || ""]} + committer={membersGroupById?.[selectedApproval?.committerId || ""]} /> ) : ( @@ -195,7 +195,7 @@ export const SecretApprovalRequest = () => { const { id: reqId, commits, - committer, + committerId, createdAt, policy, reviewers, @@ -225,9 +225,9 @@ export const SecretApprovalRequest = () => { Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} - {membersGroupById?.[committer]?.user?.firstName}{" "} - {membersGroupById?.[committer]?.user?.lastName} ( - {membersGroupById?.[committer]?.user?.email}){" "} + {membersGroupById?.[committerId]?.user?.firstName}{" "} + {membersGroupById?.[committerId]?.user?.lastName} ( + {membersGroupById?.[committerId]?.user?.email}){" "} {isApprover && !isReviewed && status === "open" && "- Review required"}