From f458e34c3734f797f50317447d21bfa979bd3e38 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH] Feat: Request access (new routes) --- .../v1/access-approval-request-router.ts | 176 ++++++++++++++++++ backend/src/ee/routes/v1/index.ts | 2 + 2 files changed, 178 insertions(+) create mode 100644 backend/src/ee/routes/v1/access-approval-request-router.ts diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts new file mode 100644 index 000000000..696c8b76f --- /dev/null +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -0,0 +1,176 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; +import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), + permissions: z.any().array(), + isTemporary: z.boolean(), + temporaryRange: z.string().optional() + }), + querystring: z.object({ + projectSlug: z.string().trim(), + secretPath: z.string().trim(), + envSlug: z.string().trim() + }), + response: { + 200: z.object({ + approval: AccessApprovalRequestsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + permissions: req.body.permissions, + envSlug: req.query.envSlug, + actorOrgId: req.permission.orgId, + secretPath: req.query.secretPath, + projectSlug: req.query.projectSlug, + temporaryRange: req.body.temporaryRange, + isTemporary: req.body.isTemporary + }); + return { approval: request }; + } + }); + + server.route({ + url: "/count", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string().trim() + }), + response: { + 200: z.object({ + pendingCount: z.number(), + finalizedCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { count } = await server.services.accessApprovalRequest.getCount({ + projectSlug: req.query.projectSlug, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { ...count }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string().trim(), + authorProjectMembershipId: z.string().trim().optional(), + envSlug: z.string().trim().optional() + }), + response: { + 200: z.object({ + requests: AccessApprovalRequestsSchema.extend({ + environmentName: z.string(), + isApproved: z.boolean(), + privilege: z + .object({ + membershipId: z.string(), + isTemporary: z.boolean(), + temporaryMode: z.string().nullish(), + temporaryRange: z.string().nullish(), + temporaryAccessStartTime: z.date().nullish(), + temporaryAccessEndTime: z.date().nullish(), + permissions: z.unknown() + }) + .nullable(), + policy: z.object({ + id: z.string(), + name: z.string(), + approvals: z.number(), + approvers: z.string().array(), + secretPath: z.string().nullish(), + envId: z.string() + }), + reviewers: z + .object({ + member: z.string(), + status: z.string() + }) + .array() + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({ + projectSlug: req.query.projectSlug, + authorProjectMembershipId: req.query.authorProjectMembershipId, + envSlug: req.query.envSlug, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { requests }; + } + }); + + server.route({ + url: "/:requestId/review", + method: "POST", + schema: { + params: z.object({ + requestId: z.string().trim() + }), + body: z.object({ + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + }), + response: { + 200: z.object({ + review: AccessApprovalRequestsReviewersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const review = await server.services.accessApprovalRequest.reviewAccessRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + requestId: req.params.requestId, + status: req.body.status + }); + + return { review }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 086a1b3d6..c73ed24c5 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,5 +1,6 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; +import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; @@ -43,6 +44,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }); await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); + await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approval-requests" }); await server.register( async (dynamicSecretRouter) => {