Merge branch 'org-only-idp' into dev

This commit is contained in:
miloschwartz
2026-01-11 10:47:57 -08:00
9 changed files with 216 additions and 69 deletions

View File

@@ -13,3 +13,4 @@ export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyApiKeyAccess";
export * from "./verifyApiKeyClientAccess"; export * from "./verifyApiKeyClientAccess";
export * from "./verifyApiKeySiteResourceAccess"; export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess";

View File

@@ -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"
)
);
}
}

View File

@@ -18,7 +18,8 @@ import * as logs from "#private/routers/auditLogs";
import { import {
verifyApiKeyHasAction, verifyApiKeyHasAction,
verifyApiKeyIsRoot, verifyApiKeyIsRoot,
verifyApiKeyOrgAccess verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess
} from "@server/middlewares"; } from "@server/middlewares";
import { import {
verifyValidSubscription, verifyValidSubscription,
@@ -31,6 +32,8 @@ import {
authenticated as a authenticated as a
} from "@server/routers/integration"; } from "@server/routers/integration";
import { logActionAudit } from "#private/middlewares"; import { logActionAudit } from "#private/middlewares";
import config from "#private/lib/config";
import { build } from "@server/build";
export const unauthenticated = ua; export const unauthenticated = ua;
export const authenticated = a; export const authenticated = a;
@@ -88,3 +91,49 @@ authenticated.get(
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
logs.exportAccessAuditLogs 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
);

View File

@@ -46,22 +46,23 @@ const bodySchema = z.strictObject({
roleMapping: z.string().optional() roleMapping: z.string().optional()
}); });
// registry.registerPath({ registry.registerPath({
// method: "put", method: "put",
// path: "/idp/oidc", path: "/org/{orgId}/idp/oidc",
// description: "Create an OIDC IdP.", description: "Create an OIDC IdP for a specific organization.",
// tags: [OpenAPITags.Idp], tags: [OpenAPITags.Idp, OpenAPITags.Org],
// request: { request: {
// body: { params: paramsSchema,
// content: { body: {
// "application/json": { content: {
// schema: bodySchema "application/json": {
// } schema: bodySchema
// } }
// } }
// }, }
// responses: {} },
// }); responses: {}
});
export async function createOrgOidcIdp( export async function createOrgOidcIdp(
req: Request, req: Request,

View File

@@ -32,9 +32,9 @@ const paramsSchema = z
registry.registerPath({ registry.registerPath({
method: "delete", method: "delete",
path: "/idp/{idpId}", path: "/org/{orgId}/idp/{idpId}",
description: "Delete IDP.", description: "Delete IDP for a specific organization.",
tags: [OpenAPITags.Idp], tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: { request: {
params: paramsSchema params: paramsSchema
}, },

View File

@@ -48,16 +48,16 @@ async function query(idpId: number, orgId: string) {
return res; return res;
} }
// registry.registerPath({ registry.registerPath({
// method: "get", method: "get",
// path: "/idp/{idpId}", path: "/org/:orgId/idp/:idpId",
// description: "Get an IDP by its IDP ID.", description: "Get an IDP by its IDP ID for a specific organization.",
// tags: [OpenAPITags.Idp], tags: [OpenAPITags.Idp, OpenAPITags.Org],
// request: { request: {
// params: paramsSchema params: paramsSchema
// }, },
// responses: {} responses: {}
// }); });
export async function getOrgIdp( export async function getOrgIdp(
req: Request, req: Request,

View File

@@ -62,16 +62,17 @@ async function query(orgId: string, limit: number, offset: number) {
return res; return res;
} }
// registry.registerPath({ registry.registerPath({
// method: "get", method: "get",
// path: "/idp", path: "/org/{orgId}/idp",
// description: "List all IDP in the system.", description: "List all IDP for a specific organization.",
// tags: [OpenAPITags.Idp], tags: [OpenAPITags.Idp, OpenAPITags.Org],
// request: { request: {
// query: querySchema query: querySchema,
// }, params: paramsSchema
// responses: {} },
// }); responses: {}
});
export async function listOrgIdps( export async function listOrgIdps(
req: Request, req: Request,

View File

@@ -53,23 +53,23 @@ export type UpdateOrgIdpResponse = {
idpId: number; idpId: number;
}; };
// registry.registerPath({ registry.registerPath({
// method: "post", method: "post",
// path: "/idp/{idpId}/oidc", path: "/org/{orgId}/idp/{idpId}/oidc",
// description: "Update an OIDC IdP.", description: "Update an OIDC IdP for a specific organization.",
// tags: [OpenAPITags.Idp], tags: [OpenAPITags.Idp, OpenAPITags.Org],
// request: { request: {
// params: paramsSchema, params: paramsSchema,
// body: { body: {
// content: { content: {
// "application/json": { "application/json": {
// schema: bodySchema schema: bodySchema
// } }
// } }
// } }
// }, },
// responses: {} responses: {}
// }); });
export async function updateOrgOidcIdp( export async function updateOrgOidcIdp(
req: Request, req: Request,

View File

@@ -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) { if (root) {
actionsByCategory["Organization"] = { actionsByCategory["Organization"] = {
[t("actionListOrgs")]: "listOrgs", [t("actionListOrgs")]: "listOrgs",
@@ -128,24 +138,21 @@ function getActionsCategories(root: boolean) {
...actionsByCategory["Organization"] ...actionsByCategory["Organization"]
}; };
actionsByCategory["Identity Provider (IDP)"] = { actionsByCategory["Identity Provider (IDP)"][t("actionCreateIdpOrg")] =
[t("actionCreateIdp")]: "createIdp", "createIdpOrg";
[t("actionUpdateIdp")]: "updateIdp", actionsByCategory["Identity Provider (IDP)"][t("actionDeleteIdpOrg")] =
[t("actionDeleteIdp")]: "deleteIdp", "deleteIdpOrg";
[t("actionListIdps")]: "listIdps", actionsByCategory["Identity Provider (IDP)"][t("actionListIdpOrgs")] =
[t("actionGetIdp")]: "getIdp", "listIdpOrgs";
[t("actionCreateIdpOrg")]: "createIdpOrg", actionsByCategory["Identity Provider (IDP)"][t("actionUpdateIdpOrg")] =
[t("actionDeleteIdpOrg")]: "deleteIdpOrg", "updateIdpOrg";
[t("actionListIdpOrgs")]: "listIdpOrgs",
[t("actionUpdateIdpOrg")]: "updateIdpOrg"
};
actionsByCategory["User"] = { actionsByCategory["User"] = {
[t("actionUpdateUser")]: "updateUser", [t("actionUpdateUser")]: "updateUser",
[t("actionGetUser")]: "getUser" [t("actionGetUser")]: "getUser"
}; };
if (build == "saas") { if (build === "saas") {
actionsByCategory["SAAS"] = { actionsByCategory["SAAS"] = {
["Send Usage Notification Email"]: "sendUsageNotification" ["Send Usage Notification Email"]: "sendUsageNotification"
}; };