list policies endpoint

This commit is contained in:
x032205
2025-12-05 02:36:10 -05:00
committed by =
parent 24f7445d56
commit e34df4c6e5
4 changed files with 148 additions and 36 deletions

View File

@@ -58,6 +58,33 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
}
});
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",

View File

@@ -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<typeof approvalPolicyDALFactory>;
@@ -25,25 +26,23 @@ export const approvalPolicyDALFactory = (db: TDbClient) => {
.whereIn("policyStepId", stepIds)
.select("policyStepId", "userId", "groupId");
const approversByStepId = approvers.reduce<Record<string, { type: string; id: string }[]>>((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<Record<string, { type: ApproverType; id: string }[]>>(
(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<string, ApprovalPolicyStep[]> = {};
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<Record<string, { type: ApproverType; id: string }[]>>(
(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
};
};

View File

@@ -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

View File

@@ -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