From 6ead0066e9bcc6a6076e787525c11abf636f27e1 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 15 Apr 2025 10:16:15 -0400 Subject: [PATCH] add create, delete, list for idp org policy --- server/routers/external.ts | 18 ++++ server/routers/idp/createIdpOrgPolicy.ts | 121 +++++++++++++++++++++++ server/routers/idp/deleteIdp.ts | 14 +-- server/routers/idp/deleteIdpOrgPolicy.ts | 90 +++++++++++++++++ server/routers/idp/getIdp.ts | 18 ++++ server/routers/idp/index.ts | 3 + server/routers/idp/listIdpOrgPolicies.ts | 116 ++++++++++++++++++++++ server/routers/idp/listIdps.ts | 4 +- server/routers/user/listUsers.ts | 5 +- 9 files changed, 375 insertions(+), 14 deletions(-) create mode 100644 server/routers/idp/createIdpOrgPolicy.ts create mode 100644 server/routers/idp/deleteIdpOrgPolicy.ts create mode 100644 server/routers/idp/listIdpOrgPolicies.ts diff --git a/server/routers/external.ts b/server/routers/external.ts index df44c599..cf7eac65 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -519,6 +519,24 @@ authenticated.get( idp.getIdp ); +authenticated.put( + "/idp/:idpId/org/:orgId", + verifyUserIsServerAdmin, + idp.createIdpOrgPolicy +); + +authenticated.delete( + "/idp/:idpId/org/:orgId", + verifyUserIsServerAdmin, + idp.deleteIdpOrgPolicy +); + +authenticated.get( + "/idp/:idpId/org", + verifyUserIsServerAdmin, + idp.listIdpOrgPolicies +); + // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts new file mode 100644 index 00000000..13c5c1b8 --- /dev/null +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -0,0 +1,121 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import config from "@server/lib/config"; +import { eq, and } from "drizzle-orm"; +import { idp, idpOrg } from "@server/db/schemas"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number(), + orgId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + roleMapping: z.string().optional(), + orgMapping: z.string().optional() + }) + .strict(); + +export type CreateIdpOrgPolicyResponse = {}; + +registry.registerPath({ + method: "put", + path: "/idp/{idpId}/org/{orgId}", + description: "Create an IDP policy for an existing IDP on an organization.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function createIdpOrgPolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { idpId, orgId } = parsedParams.data; + const { roleMapping, orgMapping } = parsedBody.data; + + const [existing] = await db + .select() + .from(idp) + .leftJoin(idpOrg, eq(idpOrg.orgId, orgId)) + .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))); + + if (!existing.idp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP with this ID does not exist." + ) + ); + } + + if (existing.idpOrg) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP org policy already exists." + ) + ); + } + + await db.insert(idpOrg).values({ + idpId, + orgId, + roleMapping, + orgMapping + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Idp created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index e7878ed5..ac84c4f7 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -18,17 +18,11 @@ const paramsSchema = z registry.registerPath({ method: "delete", - path: "/org/{orgId}/idp/oidc", - description: "Create an OIDC IdP for an organization.", - tags: [OpenAPITags.Org, OpenAPITags.Idp], + path: "/idp/{idpId}", + description: "Delete IDP.", + tags: [OpenAPITags.Idp], request: { - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } + params: paramsSchema }, responses: {} }); diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts new file mode 100644 index 00000000..9a6f6e72 --- /dev/null +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -0,0 +1,90 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { idp, idpOrg } from "@server/db/schemas"; +import { eq, and } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number(), + orgId: z.string() + }) + .strict(); + +registry.registerPath({ + method: "delete", + path: "/idp/{idpId}/org/{orgId}", + description: "Create an OIDC IdP for an organization.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function deleteIdpOrgPolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { idpId, orgId } = parsedParams.data; + + const [existing] = await db + .select() + .from(idp) + .leftJoin(idpOrg, eq(idpOrg.orgId, orgId)) + .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))); + + if (!existing.idp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP with this ID does not exist." + ) + ); + } + + if (!existing.idpOrg) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A policy for this IDP and org does not exist." + ) + ); + } + + await db + .delete(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); + + return response(res, { + data: null, + success: true, + error: false, + message: "Policy deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index 6598b542..794daade 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -9,6 +9,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import config from "@server/lib/config"; +import { decrypt } from "@server/lib/crypto"; const paramsSchema = z .object({ @@ -63,6 +65,22 @@ export async function getIdp( return next(createHttpError(HttpCode.NOT_FOUND, "Idp not found")); } + const key = config.getRawConfig().server.secret; + + if (idpRes.idp.type === "oidc") { + const clientSecret = idpRes.idpOidcConfig!.clientSecret; + const clientId = idpRes.idpOidcConfig!.clientId; + + idpRes.idpOidcConfig!.clientSecret = decrypt( + clientSecret, + key + ); + idpRes.idpOidcConfig!.clientId = decrypt( + clientId, + key + ); + } + return response(res, { data: idpRes, success: true, diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index f9dfc5fc..b0b28256 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -4,3 +4,6 @@ export * from "./listIdps"; export * from "./generateOidcUrl"; export * from "./validateOidcCallback"; export * from "./getIdp"; +export * from "./createIdpOrgPolicy"; +export * from "./deleteIdpOrgPolicy"; +export * from "./listIdpOrgPolicies"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts new file mode 100644 index 00000000..08ad110c --- /dev/null +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -0,0 +1,116 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { idpOrg } from "@server/db/schemas"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, sql } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z.object({ + idpId: z.coerce.number() +}); + +const querySchema = z + .object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) + }) + .strict(); + +async function query(idpId: number, limit: number, offset: number) { + const res = await db + .select() + .from(idpOrg) + .where(eq(idpOrg.idpId, idpId)) + .limit(limit) + .offset(offset); + return res; +} + +export type ListIdpOrgPoliciesResponse = { + policies: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/idp/{idpId}/org", + description: "List all org policies on an IDP.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema, + query: querySchema + }, + responses: {} +}); + +export async function listIdpOrgPolicies( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + const { idpId } = parsedParams.data; + + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const list = await query(idpId, limit, offset); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + + return response(res, { + data: { + policies: list, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Policies retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 0a7c5e4c..7b6f84e3 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { domains, orgDomains, users } from "@server/db/schemas"; +import { domains, idp, orgDomains, users } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -69,7 +69,7 @@ export async function listIdps( const [{ count }] = await db .select({ count: sql`count(*)` }) - .from(domains); + .from(idp); return response(res, { data: { diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 53b9f8e7..2b4a5d42 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -5,7 +5,7 @@ import { idp, roles, userOrgs, users } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { sql } from "drizzle-orm"; +import { and, sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -114,7 +114,8 @@ export async function listUsers( const [{ count }] = await db .select({ count: sql`count(*)` }) - .from(users); + .from(users) + .where(eq(userOrgs.orgId, orgId)); return response(res, { data: {