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 4a84f40ab..dc3363497 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 @@ -19,7 +19,8 @@ export const registerApprovalPolicyEndpoints =

({ updatePolicySchema, policyResponseSchema, createRequestSchema, - requestResponseSchema + requestResponseSchema, + grantResponseSchema }: { server: FastifyZodProvider; policyType: ApprovalPolicyType; @@ -38,6 +39,7 @@ export const registerApprovalPolicyEndpoints =

({ policyResponseSchema: z.ZodTypeAny; createRequestSchema: z.ZodType; requestResponseSchema: z.ZodTypeAny; + grantResponseSchema: z.ZodTypeAny; }) => { // Policies server.route({ @@ -365,4 +367,93 @@ export const registerApprovalPolicyEndpoints =

({ return { request }; } }); + + // Grants + server.route({ + method: "GET", + url: "/grants", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval grants", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + grants: z.array(grantResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grants } = await server.services.approvalPolicy.listGrants( + policyType, + req.query.projectId, + req.permission + ); + + // TODO(andrey): Audit log + + return { grants }; + } + }); + + server.route({ + method: "GET", + url: "/grants/:grantId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval grant", + params: z.object({ + grantId: z.string().uuid() + }), + response: { + 200: z.object({ + grant: grantResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grant } = await server.services.approvalPolicy.getGrantById(req.params.grantId, req.permission); + + // TODO(andrey): Audit log + + return { grant }; + } + }); + + server.route({ + method: "POST", + url: "/grants/:grantId/revoke", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Revoke approval grant", + params: z.object({ + grantId: z.string().uuid() + }), + body: z.object({ + revocationReason: z.string().optional() + }), + response: { + 200: z.object({ + grant: grantResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grant } = await server.services.approvalPolicy.revokeGrant(req.params.grantId, req.body, req.permission); + + // TODO(andrey): Audit log + + return { grant }; + } + }); }; diff --git a/backend/src/server/routes/v1/approval-policy-routers/index.ts b/backend/src/server/routes/v1/approval-policy-routers/index.ts index e1c1a3644..c848e2e47 100644 --- a/backend/src/server/routes/v1/approval-policy-routers/index.ts +++ b/backend/src/server/routes/v1/approval-policy-routers/index.ts @@ -3,6 +3,7 @@ import { CreatePamAccessPolicySchema, CreatePamAccessRequestSchema, PamAccessPolicySchema, + PamAccessRequestGrantSchema, PamAccessRequestSchema, UpdatePamAccessPolicySchema } from "@app/services/approval-policy/pam-access/pam-access-policy-schemas"; @@ -21,7 +22,8 @@ export const APPROVAL_POLICY_REGISTER_ROUTER_MAP: Record< updatePolicySchema: UpdatePamAccessPolicySchema, policyResponseSchema: PamAccessPolicySchema, createRequestSchema: CreatePamAccessRequestSchema, - requestResponseSchema: PamAccessRequestSchema + requestResponseSchema: PamAccessRequestSchema, + grantResponseSchema: PamAccessRequestGrantSchema }); } }; diff --git a/backend/src/services/approval-policy/approval-policy-schemas.ts b/backend/src/services/approval-policy/approval-policy-schemas.ts index b848eb004..3c47b2c6b 100644 --- a/backend/src/services/approval-policy/approval-policy-schemas.ts +++ b/backend/src/services/approval-policy/approval-policy-schemas.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { ApprovalPoliciesSchema, ApprovalRequestApprovalsSchema, + ApprovalRequestGrantsSchema, ApprovalRequestsSchema, ApprovalRequestStepsSchema } from "@app/db/schemas"; @@ -89,3 +90,6 @@ export const BaseCreateApprovalRequestSchema = z.object({ .nullable() .optional() }); + +// Grants +export const BaseApprovalRequestGrantSchema = ApprovalRequestGrantsSchema; diff --git a/backend/src/services/approval-policy/approval-policy-service.ts b/backend/src/services/approval-policy/approval-policy-service.ts index 3c04a7866..4cb1d147c 100644 --- a/backend/src/services/approval-policy/approval-policy-service.ts +++ b/backend/src/services/approval-policy/approval-policy-service.ts @@ -1,7 +1,7 @@ import { ActionProjectType, ProjectMembershipRole, TApprovalPolicies, TApprovalRequests } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { OrgServiceActor } from "@app/lib/types"; import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; @@ -21,6 +21,7 @@ import { import { ApprovalPolicyType, ApprovalRequestApprovalDecision, + ApprovalRequestGrantStatus, ApprovalRequestStatus, ApprovalRequestStepStatus, ApproverType @@ -699,6 +700,50 @@ export const approvalPolicyServiceFactory = ({ return { request: { ...updatedRequest, steps } }; }; + const listGrants = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + // TODO(andrey): Perm check + + const grants = await approvalRequestGrantsDAL.find({ projectId, type: policyType }); + return { grants }; + }; + + const getGrantById = async (grantId: string, actor: OrgServiceActor) => { + // TODO(andrey): Perm check + + const grant = await approvalRequestGrantsDAL.findById(grantId); + if (!grant) { + throw new NotFoundError({ message: "Grant not found" }); + } + + return { grant }; + }; + + const revokeGrant = async ( + grantId: string, + { revocationReason }: { revocationReason?: string }, + actor: OrgServiceActor + ) => { + // TODO(andrey): Perm check + + const grant = await approvalRequestGrantsDAL.findById(grantId); + if (!grant) { + throw new NotFoundError({ message: "Grant not found" }); + } + + if (grant.status !== ApprovalRequestGrantStatus.Active) { + throw new BadRequestError({ message: "Grant is not active" }); + } + + const updatedGrant = await approvalRequestGrantsDAL.updateById(grantId, { + status: ApprovalRequestGrantStatus.Revoked, + revokedAt: new Date(), + revokedByUserId: actor.id, + revocationReason + }); + + return { grant: updatedGrant }; + }; + return { create, list, @@ -710,6 +755,9 @@ export const approvalPolicyServiceFactory = ({ getRequestById, approveRequest, rejectRequest, - cancelRequest + cancelRequest, + listGrants, + getGrantById, + revokeGrant }; }; diff --git a/backend/src/services/approval-policy/approval-policy-types.ts b/backend/src/services/approval-policy/approval-policy-types.ts index b0f2dd530..f92451c17 100644 --- a/backend/src/services/approval-policy/approval-policy-types.ts +++ b/backend/src/services/approval-policy/approval-policy-types.ts @@ -54,7 +54,7 @@ export interface TCreateRequestDTO { projectId: TApprovalRequest["projectId"]; requestData: TApprovalRequest["requestData"]["requestData"]; justification?: TApprovalRequest["justification"]; - requestDuration?: string; + requestDuration?: string | null; } // Factory diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts index 7d6dad9b9..d1eb13726 100644 --- a/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts @@ -4,6 +4,7 @@ import { ms } from "@app/lib/ms"; import { BaseApprovalPolicySchema, + BaseApprovalRequestGrantSchema, BaseApprovalRequestSchema, BaseCreateApprovalPolicySchema, BaseCreateApprovalRequestSchema, @@ -79,3 +80,8 @@ export const PamAccessRequestSchema = BaseApprovalRequestSchema.extend({ export const CreatePamAccessRequestSchema = BaseCreateApprovalRequestSchema.extend({ requestData: PamAccessPolicyRequestDataSchema }); + +// Grants +export const PamAccessRequestGrantSchema = BaseApprovalRequestGrantSchema.extend({ + attributes: PamAccessPolicyRequestDataSchema +});