From 0a537c6830fd6c019d4f5b1a21558d557add6231 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 11 Jan 2026 10:44:32 -0800 Subject: [PATCH] add org only idp to integration api --- server/middlewares/integration/index.ts | 1 + .../integration/verifyApiKeyIdpAccess.ts | 88 +++++++++++++++++++ server/private/routers/integration.ts | 51 ++++++++++- .../routers/orgIdp/createOrgOidcIdp.ts | 33 +++---- server/private/routers/orgIdp/deleteOrgIdp.ts | 6 +- server/private/routers/orgIdp/getOrgIdp.ts | 20 ++--- server/private/routers/orgIdp/listOrgIdps.ts | 21 ++--- .../routers/orgIdp/updateOrgOidcIdp.ts | 34 +++---- src/components/PermissionsSelectBox.tsx | 31 ++++--- 9 files changed, 216 insertions(+), 69 deletions(-) create mode 100644 server/middlewares/integration/verifyApiKeyIdpAccess.ts diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 2e2e8ff0..56575191 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -13,3 +13,4 @@ export * from "./verifyApiKeyIsRoot"; export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyClientAccess"; export * from "./verifyApiKeySiteResourceAccess"; +export * from "./verifyApiKeyIdpAccess"; diff --git a/server/middlewares/integration/verifyApiKeyIdpAccess.ts b/server/middlewares/integration/verifyApiKeyIdpAccess.ts new file mode 100644 index 00000000..99b7e76b --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyIdpAccess.ts @@ -0,0 +1,88 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { idp, idpOrg, apiKeyOrg } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyIdpAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const idpId = req.params.idpId || req.body.idpId || req.query.idpId; + const orgId = req.params.orgId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + if (!idpId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID") + ); + } + + if (apiKey.isRoot) { + // Root keys can access any IDP in any org + return next(); + } + + const [idpRes] = await db + .select() + .from(idp) + .innerJoin(idpOrg, eq(idp.idpId, idpOrg.idpId)) + .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))) + .limit(1); + + if (!idpRes || !idpRes.idp || !idpRes.idpOrg) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `IdP with ID ${idpId} not found for organization ${orgId}` + ) + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, idpRes.idpOrg.orgId) + ) + ); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying IDP access" + ) + ); + } +} diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts index 9eefff8f..25861a54 100644 --- a/server/private/routers/integration.ts +++ b/server/private/routers/integration.ts @@ -18,7 +18,8 @@ import * as logs from "#private/routers/auditLogs"; import { verifyApiKeyHasAction, verifyApiKeyIsRoot, - verifyApiKeyOrgAccess + verifyApiKeyOrgAccess, + verifyApiKeyIdpAccess } from "@server/middlewares"; import { verifyValidSubscription, @@ -31,6 +32,8 @@ import { authenticated as a } from "@server/routers/integration"; import { logActionAudit } from "#private/middlewares"; +import config from "#private/lib/config"; +import { build } from "@server/build"; export const unauthenticated = ua; export const authenticated = a; @@ -88,3 +91,49 @@ authenticated.get( logActionAudit(ActionsEnum.exportLogs), logs.exportAccessAuditLogs ); + +authenticated.put( + "/org/:orgId/idp/oidc", + verifyValidLicense, + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createIdp), + logActionAudit(ActionsEnum.createIdp), + orgIdp.createOrgOidcIdp +); + +authenticated.post( + "/org/:orgId/idp/:idpId/oidc", + verifyValidLicense, + verifyApiKeyOrgAccess, + verifyApiKeyIdpAccess, + verifyApiKeyHasAction(ActionsEnum.updateIdp), + logActionAudit(ActionsEnum.updateIdp), + orgIdp.updateOrgOidcIdp +); + +authenticated.delete( + "/org/:orgId/idp/:idpId", + verifyValidLicense, + verifyApiKeyOrgAccess, + verifyApiKeyIdpAccess, + verifyApiKeyHasAction(ActionsEnum.deleteIdp), + logActionAudit(ActionsEnum.deleteIdp), + orgIdp.deleteOrgIdp +); + +authenticated.get( + "/org/:orgId/idp/:idpId", + verifyValidLicense, + verifyApiKeyOrgAccess, + verifyApiKeyIdpAccess, + verifyApiKeyHasAction(ActionsEnum.getIdp), + orgIdp.getOrgIdp +); + +authenticated.get( + "/org/:orgId/idp", + verifyValidLicense, + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listIdps), + orgIdp.listOrgIdps +); diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index 709f6167..36a5487e 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -46,22 +46,23 @@ const bodySchema = z.strictObject({ roleMapping: z.string().optional() }); -// registry.registerPath({ -// method: "put", -// path: "/idp/oidc", -// description: "Create an OIDC IdP.", -// tags: [OpenAPITags.Idp], -// request: { -// body: { -// content: { -// "application/json": { -// schema: bodySchema -// } -// } -// } -// }, -// responses: {} -// }); +registry.registerPath({ + method: "put", + path: "/org/{orgId}/idp/oidc", + description: "Create an OIDC IdP for a specific organization.", + tags: [OpenAPITags.Idp, OpenAPITags.Org], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); export async function createOrgOidcIdp( req: Request, diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index 721b91cb..176f4238 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -32,9 +32,9 @@ const paramsSchema = z registry.registerPath({ method: "delete", - path: "/idp/{idpId}", - description: "Delete IDP.", - tags: [OpenAPITags.Idp], + path: "/org/{orgId}/idp/{idpId}", + description: "Delete IDP for a specific organization.", + tags: [OpenAPITags.Idp, OpenAPITags.Org], request: { params: paramsSchema }, diff --git a/server/private/routers/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts index 01ddc0f7..dd987c44 100644 --- a/server/private/routers/orgIdp/getOrgIdp.ts +++ b/server/private/routers/orgIdp/getOrgIdp.ts @@ -48,16 +48,16 @@ async function query(idpId: number, orgId: string) { return res; } -// registry.registerPath({ -// method: "get", -// path: "/idp/{idpId}", -// description: "Get an IDP by its IDP ID.", -// tags: [OpenAPITags.Idp], -// request: { -// params: paramsSchema -// }, -// responses: {} -// }); +registry.registerPath({ + method: "get", + path: "/org/:orgId/idp/:idpId", + description: "Get an IDP by its IDP ID for a specific organization.", + tags: [OpenAPITags.Idp, OpenAPITags.Org], + request: { + params: paramsSchema + }, + responses: {} +}); export async function getOrgIdp( req: Request, diff --git a/server/private/routers/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts index 36cbc627..61049c49 100644 --- a/server/private/routers/orgIdp/listOrgIdps.ts +++ b/server/private/routers/orgIdp/listOrgIdps.ts @@ -62,16 +62,17 @@ async function query(orgId: string, limit: number, offset: number) { return res; } -// registry.registerPath({ -// method: "get", -// path: "/idp", -// description: "List all IDP in the system.", -// tags: [OpenAPITags.Idp], -// request: { -// query: querySchema -// }, -// responses: {} -// }); +registry.registerPath({ + method: "get", + path: "/org/{orgId}/idp", + description: "List all IDP for a specific organization.", + tags: [OpenAPITags.Idp, OpenAPITags.Org], + request: { + query: querySchema, + params: paramsSchema + }, + responses: {} +}); export async function listOrgIdps( req: Request, diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts index f29e4fc2..6474abda 100644 --- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -53,23 +53,23 @@ export type UpdateOrgIdpResponse = { idpId: number; }; -// registry.registerPath({ -// method: "post", -// path: "/idp/{idpId}/oidc", -// description: "Update an OIDC IdP.", -// tags: [OpenAPITags.Idp], -// request: { -// params: paramsSchema, -// body: { -// content: { -// "application/json": { -// schema: bodySchema -// } -// } -// } -// }, -// responses: {} -// }); +registry.registerPath({ + method: "post", + path: "/org/{orgId}/idp/{idpId}/oidc", + description: "Update an OIDC IdP for a specific organization.", + tags: [OpenAPITags.Idp, OpenAPITags.Org], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); export async function updateOrgOidcIdp( req: Request, diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 9cfe2aaf..a5cfad7b 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -114,6 +114,16 @@ function getActionsCategories(root: boolean) { } }; + if (root || build === "saas" || env.flags.useOrgOnlyIdp) { + actionsByCategory["Identity Provider (IDP)"] = { + [t("actionCreateIdp")]: "createIdp", + [t("actionUpdateIdp")]: "updateIdp", + [t("actionDeleteIdp")]: "deleteIdp", + [t("actionListIdps")]: "listIdps", + [t("actionGetIdp")]: "getIdp" + }; + } + if (root) { actionsByCategory["Organization"] = { [t("actionListOrgs")]: "listOrgs", @@ -128,24 +138,21 @@ function getActionsCategories(root: boolean) { ...actionsByCategory["Organization"] }; - actionsByCategory["Identity Provider (IDP)"] = { - [t("actionCreateIdp")]: "createIdp", - [t("actionUpdateIdp")]: "updateIdp", - [t("actionDeleteIdp")]: "deleteIdp", - [t("actionListIdps")]: "listIdps", - [t("actionGetIdp")]: "getIdp", - [t("actionCreateIdpOrg")]: "createIdpOrg", - [t("actionDeleteIdpOrg")]: "deleteIdpOrg", - [t("actionListIdpOrgs")]: "listIdpOrgs", - [t("actionUpdateIdpOrg")]: "updateIdpOrg" - }; + actionsByCategory["Identity Provider (IDP)"][t("actionCreateIdpOrg")] = + "createIdpOrg"; + actionsByCategory["Identity Provider (IDP)"][t("actionDeleteIdpOrg")] = + "deleteIdpOrg"; + actionsByCategory["Identity Provider (IDP)"][t("actionListIdpOrgs")] = + "listIdpOrgs"; + actionsByCategory["Identity Provider (IDP)"][t("actionUpdateIdpOrg")] = + "updateIdpOrg"; actionsByCategory["User"] = { [t("actionUpdateUser")]: "updateUser", [t("actionGetUser")]: "getUser" }; - if (build == "saas") { + if (build === "saas") { actionsByCategory["SAAS"] = { ["Send Usage Notification Email"]: "sendUsageNotification" };