diff --git a/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts b/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts index b72f5eaa52..41597182cc 100644 --- a/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts +++ b/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts @@ -58,6 +58,33 @@ export const registerApprovalPolicyEndpoints =

({ } }); + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval policies", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + policies: z.array(policyResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policies } = await server.services.approvalPolicy.list(policyType, req.query.projectId, req.permission); + + // TODO: Audit log + + return { policies }; + } + }); + server.route({ method: "GET", url: "/:policyId", diff --git a/backend/src/services/approval-policy/approval-policy-dal.ts b/backend/src/services/approval-policy/approval-policy-dal.ts index 6276b14fc2..bb1854caec 100644 --- a/backend/src/services/approval-policy/approval-policy-dal.ts +++ b/backend/src/services/approval-policy/approval-policy-dal.ts @@ -3,7 +3,8 @@ import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; -import { ApproverType } from "./approval-policy-enums"; +import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums"; +import { ApprovalPolicyStep } from "./approval-policy-types"; // Approval Policy export type TApprovalPolicyDALFactory = ReturnType; @@ -25,25 +26,23 @@ export const approvalPolicyDALFactory = (db: TDbClient) => { .whereIn("policyStepId", stepIds) .select("policyStepId", "userId", "groupId"); - const approversByStepId = approvers.reduce>((acc, approver) => { - const stepApprovers = acc[approver.policyStepId] || []; - stepApprovers.push({ - type: approver.userId ? ApproverType.User : ApproverType.Group, - id: (approver.userId || approver.groupId) as string - }); - acc[approver.policyStepId] = stepApprovers; - return acc; - }, {}); + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.policyStepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.policyStepId] = stepApprovers; + return acc; + }, + {} + ); return steps.map((step) => { const stepApprovers = approversByStepId[step.id] || []; - const formattedStep: { - name?: string; - requiredApprovals: number; - notifyApprovers?: boolean; - approvers: { type: string; id: string }[]; - } = { + const formattedStep: ApprovalPolicyStep = { requiredApprovals: step.requiredApprovals, approvers: stepApprovers }; @@ -62,9 +61,77 @@ export const approvalPolicyDALFactory = (db: TDbClient) => { } }; + const findByProjectId = async (policyType: ApprovalPolicyType, projectId: string) => { + try { + const dbInstance = db.replicaNode(); + const policies = await dbInstance(TableName.ApprovalPolicies).where({ type: policyType, projectId }); + + if (!policies.length) { + return []; + } + + const policyIds = policies.map((p) => p.id); + + const steps = await dbInstance(TableName.ApprovalPolicySteps) + .whereIn("policyId", policyIds) + .orderBy("stepNumber", "asc"); + + const stepsByPolicyId: Record = {}; + + if (steps.length) { + const stepIds = steps.map((step) => step.id); + + const approvers = await dbInstance(TableName.ApprovalPolicyStepApprovers) + .whereIn("policyStepId", stepIds) + .select("policyStepId", "userId", "groupId"); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.policyStepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.policyStepId] = stepApprovers; + return acc; + }, + {} + ); + + steps.forEach((step) => { + const stepApprovers = approversByStepId[step.id] || []; + const formattedStep: ApprovalPolicyStep = { + requiredApprovals: step.requiredApprovals, + approvers: stepApprovers + }; + + if (step.name) { + formattedStep.name = step.name; + } + if (typeof step.notifyApprovers === "boolean") { + formattedStep.notifyApprovers = step.notifyApprovers; + } + + if (!stepsByPolicyId[step.policyId]) { + stepsByPolicyId[step.policyId] = []; + } + stepsByPolicyId[step.policyId].push(formattedStep); + }); + } + + return policies.map((policy) => ({ + ...policy, + steps: stepsByPolicyId[policy.id] || [] + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval policies by project id" }); + } + }; + return { ...orm, - findStepsByPolicyId + findStepsByPolicyId, + findByProjectId }; }; diff --git a/backend/src/services/approval-policy/approval-policy-service.ts b/backend/src/services/approval-policy/approval-policy-service.ts index 94dbc3d934..5c16dc7291 100644 --- a/backend/src/services/approval-policy/approval-policy-service.ts +++ b/backend/src/services/approval-policy/approval-policy-service.ts @@ -122,6 +122,25 @@ export const approvalPolicyServiceFactory = ({ }; }; + const list = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + const policies = await approvalPolicyDAL.findByProjectId(policyType, projectId); + + return { policies }; + }; + const getById = async (policyId: string, actor: OrgServiceActor) => { const policy = await approvalPolicyDAL.findById(policyId); if (!policy) { @@ -270,6 +289,7 @@ export const approvalPolicyServiceFactory = ({ return { create, + list, getById, updateById, deleteById diff --git a/backend/src/services/approval-policy/approval-policy-types.ts b/backend/src/services/approval-policy/approval-policy-types.ts index c2cc858b1f..0c5ffa71f3 100644 --- a/backend/src/services/approval-policy/approval-policy-types.ts +++ b/backend/src/services/approval-policy/approval-policy-types.ts @@ -1,4 +1,8 @@ -import { TApprovalPolicyDALFactory, TApprovalRequestGrantsDALFactory } from "./approval-policy-dal"; +import { + TApprovalPolicyDALFactory, + TApprovalRequestGrantsDALFactory +} from "@app/services/approval-policy/approval-policy-dal"; + import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums"; import { TPamAccessPolicy, @@ -12,6 +16,16 @@ export type TApprovalPolicyInputs = TPamAccessPolicyInputs; export type TApprovalPolicyConditions = TPamAccessPolicyConditions; export type TApprovalPolicyConstraints = TPamAccessPolicyConstraints; +export interface ApprovalPolicyStep { + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean; + approvers: { + type: ApproverType; + id: string; + }[]; +} + // DTOs export interface TCreatePolicyDTO { projectId: TApprovalPolicy["projectId"]; @@ -19,15 +33,7 @@ export interface TCreatePolicyDTO { maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"]; conditions: TApprovalPolicy["conditions"]["conditions"]; constraints: TApprovalPolicy["constraints"]["constraints"]; - steps: { - name?: string | null; - requiredApprovals: number; - notifyApprovers?: boolean; - approvers: { - type: ApproverType.User | ApproverType.Group; - id: string; - }[]; - }[]; + steps: ApprovalPolicyStep[]; } export interface TUpdatePolicyDTO { @@ -35,15 +41,7 @@ export interface TUpdatePolicyDTO { maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"]; conditions?: TApprovalPolicy["conditions"]["conditions"]; constraints?: TApprovalPolicy["constraints"]["constraints"]; - steps?: { - name?: string | null; - requiredApprovals: number; - notifyApprovers?: boolean; - approvers: { - type: ApproverType.User | ApproverType.Group; - id: string; - }[]; - }[]; + steps?: ApprovalPolicyStep[]; } // Factory