diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index 42510c19..268ccd55 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -453,17 +453,6 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { scopes: text("scopes").notNull() }); -export const idpOrg = sqliteTable("idpOrg", { - idpId: integer("idpId") - .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - roleMapping: text("roleMapping"), - orgMapping: text("orgMapping") -}); - export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -500,4 +489,3 @@ export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; export type SupporterKey = InferSelectModel; export type Idp = InferSelectModel; -export type IdpOrg = InferSelectModel; diff --git a/server/routers/external.ts b/server/routers/external.ts index 4b939b4a..7cc6b490 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -517,30 +517,6 @@ authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps); authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); -authenticated.put( - "/idp/:idpId/org/:orgId", - verifyUserIsServerAdmin, - idp.createIdpOrgPolicy -); - -authenticated.post( - "/idp/:idpId/org/:orgId", - verifyUserIsServerAdmin, - idp.updateIdpOrgPolicy -); - -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 deleted file mode 100644 index 808c7ca7..00000000 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ /dev/null @@ -1,124 +0,0 @@ -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, - and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) - ) - .where(eq(idp.idpId, idpId)); - - 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/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 555895f3..2591de10 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -7,7 +7,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas"; +import { idp, idpOidcConfig } from "@server/db/schemas"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index ac84c4f7..79edd547 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; +import { idp, idpOidcConfig } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; @@ -67,11 +67,6 @@ export async function deleteIdp( .delete(idpOidcConfig) .where(eq(idpOidcConfig.idpId, idpId)); - // Delete IDP-org mappings - await trx - .delete(idpOrg) - .where(eq(idpOrg.idpId, idpId)); - // Delete the IDP itself await trx .delete(idp) diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts deleted file mode 100644 index 9a6f6e72..00000000 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ /dev/null @@ -1,90 +0,0 @@ -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/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 101a9d83..6d111451 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; +import { idp, idpOidcConfig } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index f0dcf02e..185effde 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -5,7 +5,3 @@ export * from "./listIdps"; export * from "./generateOidcUrl"; export * from "./validateOidcCallback"; export * from "./getIdp"; -export * from "./createIdpOrgPolicy"; -export * from "./deleteIdpOrgPolicy"; -export * from "./listIdpOrgPolicies"; -export * from "./updateIdpOrgPolicy"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts deleted file mode 100644 index 08ad110c..00000000 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ /dev/null @@ -1,116 +0,0 @@ -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 a723ee05..76d0be87 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, idp, orgDomains, users, idpOrg } from "@server/db/schemas"; +import { idp } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -33,10 +33,8 @@ async function query(limit: number, offset: number) { idpId: idp.idpId, name: idp.name, type: idp.type, - orgCount: sql`count(${idpOrg.orgId})` }) .from(idp) - .leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`) .groupBy(idp.idpId) .limit(limit) .offset(offset); @@ -48,7 +46,6 @@ export type ListIdpsResponse = { idpId: number; name: string; type: string; - orgCount: number; }>; pagination: { total: number; diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts deleted file mode 100644 index 2c5079e4..00000000 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ /dev/null @@ -1,126 +0,0 @@ -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 { 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 UpdateIdpOrgPolicyResponse = {}; - -registry.registerPath({ - method: "post", - path: "/idp/{idpId}/org/{orgId}", - description: "Update an IDP org policy.", - tags: [OpenAPITags.Idp], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} -}); - -export async function updateIdpOrgPolicy( - 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 parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { idpId, orgId } = parsedParams.data; - const { roleMapping, orgMapping } = parsedBody.data; - - // Check if IDP and policy exist - const [existing] = await db - .select() - .from(idp) - .leftJoin( - idpOrg, - and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) - ) - .where(eq(idp.idpId, idpId)); - - 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." - ) - ); - } - - // Update the policy - await db - .update(idpOrg) - .set({ - roleMapping, - orgMapping - }) - .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); - - return response(res, { - data: {}, - success: true, - error: false, - message: "Policy updated successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} \ No newline at end of file diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 1a13c5dd..43c4c7f5 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -1,7 +1,6 @@ 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"; @@ -9,26 +8,14 @@ import { fromError } from "zod-validation-error"; import { idp, idpOidcConfig, - idpOrg, - orgs, - Role, - roles, - userOrgs, users } from "@server/db/schemas"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import jmespath from "jmespath"; import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; -import { UserType } from "@server/types/UserTypes"; -import { - createSession, - generateId, - generateSessionToken, - serializeSessionCookie -} from "@server/auth/sessions/app"; import { decrypt } from "@server/lib/crypto"; const paramsSchema = z @@ -215,212 +202,12 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - const allOrgs = await db.select().from(orgs); - - const defaultRoleMapping = existingIdp.idp.defaultRoleMapping; - const defaultOrgMapping = existingIdp.idp.defaultOrgMapping; - - let userOrgInfo: { orgId: string; roleId: number }[] = []; - for (const org of allOrgs) { - const [idpOrgRes] = await db - .select() - .from(idpOrg) - .where( - and( - eq(idpOrg.idpId, existingIdp.idp.idpId), - eq(idpOrg.orgId, org.orgId) - ) - ); - - let roleId: number | undefined = undefined; - - const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping; - const hydratedOrgMapping = hydrateOrgMapping( - orgMapping, - org.orgId - ); - - if (hydratedOrgMapping) { - logger.debug("Hydrated Org Mapping", { - hydratedOrgMapping - }); - const orgId = jmespath.search(claims, hydratedOrgMapping); - logger.debug("Extraced Org ID", { orgId }); - if (orgId !== true && orgId !== org.orgId) { - // user not allowed to access this org - continue; - } - } - - const roleMapping = - idpOrgRes?.roleMapping || defaultRoleMapping; - if (roleMapping) { - logger.debug("Role Mapping", { roleMapping }); - const roleName = jmespath.search(claims, roleMapping); - - if (!roleName) { - logger.error("Role name not found in the ID token", { - roleName - }); - continue; - } - - const [roleRes] = await db - .select() - .from(roles) - .where( - and( - eq(roles.orgId, org.orgId), - eq(roles.name, roleName) - ) - ); - - if (!roleRes) { - logger.error("Role not found", { - orgId: org.orgId, - roleName - }); - continue; - } - - roleId = roleRes.roleId; - - userOrgInfo.push({ - orgId: org.orgId, - roleId - }); - } - } - - logger.debug("User org info", { userOrgInfo }); - - let existingUserId = existingUser?.userId; - - // sync the user with the orgs and roles - await db.transaction(async (trx) => { - let userId = existingUser?.userId; - - // create user if not exists - if (!existingUser) { - userId = generateId(15); - - await trx.insert(users).values({ - userId, - username: userIdentifier, - email: email || null, - name: name || null, - type: UserType.OIDC, - idpId: existingIdp.idp.idpId, - emailVerified: true, // OIDC users are always verified - dateCreated: new Date().toISOString() - }); - } else { - // set the name and email - await trx - .update(users) - .set({ - username: userIdentifier, - email: email || null, - name: name || null - }) - .where(eq(users.userId, userId)); - } - - existingUserId = userId; - - // get all current user orgs - const currentUserOrgs = await trx - .select() - .from(userOrgs) - .where(eq(userOrgs.userId, userId)); - - // Delete orgs that are no longer valid - const orgsToDelete = currentUserOrgs.filter( - (currentOrg) => - !userOrgInfo.some( - (newOrg) => newOrg.orgId === currentOrg.orgId - ) - ); - - if (orgsToDelete.length > 0) { - await trx.delete(userOrgs).where( - and( - eq(userOrgs.userId, userId), - inArray( - userOrgs.orgId, - orgsToDelete.map((org) => org.orgId) - ) - ) - ); - } - - // Update roles for existing orgs where the role has changed - const orgsToUpdate = currentUserOrgs.filter((currentOrg) => { - const newOrg = userOrgInfo.find( - (newOrg) => newOrg.orgId === currentOrg.orgId - ); - return newOrg && newOrg.roleId !== currentOrg.roleId; - }); - - if (orgsToUpdate.length > 0) { - for (const org of orgsToUpdate) { - const newRole = userOrgInfo.find( - (newOrg) => newOrg.orgId === org.orgId - ); - if (newRole) { - await trx - .update(userOrgs) - .set({ roleId: newRole.roleId }) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, org.orgId) - ) - ); - } - } - } - - // Add new orgs that don't exist yet - const orgsToAdd = userOrgInfo.filter( - (newOrg) => - !currentUserOrgs.some( - (currentOrg) => currentOrg.orgId === newOrg.orgId - ) - ); - - if (orgsToAdd.length > 0) { - await trx.insert(userOrgs).values( - orgsToAdd.map((org) => ({ - userId, - orgId: org.orgId, - roleId: org.roleId, - dateCreated: new Date().toISOString() - })) - ); - } - }); - - const token = generateSessionToken(); - const sess = await createSession(token, existingUserId); - const isSecure = req.protocol === "https"; - const cookie = serializeSessionCookie( - token, - isSecure, - new Date(sess.expiresAt) + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Auto provisioning is not supported" + ) ); - - res.appendHeader("Set-Cookie", cookie); - - return response(res, { - data: { - redirectUrl: postAuthRedirectUrl - }, - success: true, - error: false, - message: "OIDC callback validated successfully", - status: HttpCode.CREATED - }); } else { if (!existingUser) { return next( diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index 56d2ad05..193cbe4e 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -39,10 +39,6 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { { title: "General", href: `/admin/idp/${params.idpId}/general` - }, - { - title: "Organization Policies", - href: `/admin/idp/${params.idpId}/policies` } ]; diff --git a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx deleted file mode 100644 index b9543daa..00000000 --- a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@app/components/ui/data-table"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - onAdd: () => void; -} - -export function PolicyDataTable({ - columns, - data, - onAdd -}: DataTableProps) { - return ( - - ); -} \ No newline at end of file diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx deleted file mode 100644 index 09ba309f..00000000 --- a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx +++ /dev/null @@ -1,154 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@app/components/ui/button"; -import { - ArrowUpDown, - Trash2, - MoreHorizontal, - Pencil, - ArrowRight -} from "lucide-react"; -import { PolicyDataTable } from "./PolicyDataTable"; -import { Badge } from "@app/components/ui/badge"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; -import Link from "next/link"; -import { InfoPopup } from "@app/components/ui/info-popup"; - -export interface PolicyRow { - orgId: string; - roleMapping?: string; - orgMapping?: string; -} - -interface Props { - policies: PolicyRow[]; - onDelete: (orgId: string) => void; - onAdd: () => void; - onEdit: (policy: PolicyRow) => void; -} - -export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props) { - const columns: ColumnDef[] = [ - { - id: "dots", - cell: ({ row }) => { - const r = row.original; - - return ( - - - - - - { - onDelete(r.orgId); - }} - > - Delete - - - - ); - } - }, - { - accessorKey: "orgId", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "roleMapping", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const mapping = row.original.roleMapping; - return mapping ? ( - 50 ? `${mapping.substring(0, 50)}...` : mapping} - info={mapping} - /> - ) : ( - "--" - ); - } - }, - { - accessorKey: "orgMapping", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const mapping = row.original.orgMapping; - return mapping ? ( - 50 ? `${mapping.substring(0, 50)}...` : mapping} - info={mapping} - /> - ) : ( - "--" - ); - } - }, - { - id: "actions", - cell: ({ row }) => { - const policy = row.original; - return ( -
- -
- ); - } - } - ]; - - return ; -} diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx deleted file mode 100644 index de0f6bef..00000000 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ /dev/null @@ -1,609 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { Button } from "@app/components/ui/button"; -import { Input } from "@app/components/ui/input"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; -import PolicyTable, { PolicyRow } from "./PolicyTable"; -import { AxiosResponse } from "axios"; -import { ListOrgsResponse } from "@server/routers/org"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; -import { Textarea } from "@app/components/ui/textarea"; -import { InfoPopup } from "@app/components/ui/info-popup"; -import { GetIdpResponse } from "@server/routers/idp"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm -} from "@app/components/Settings"; - -type Organization = { - orgId: string; - name: string; -}; - -const policyFormSchema = z.object({ - orgId: z.string().min(1, { message: "Organization is required" }), - roleMapping: z.string().optional(), - orgMapping: z.string().optional() -}); - -const defaultMappingsSchema = z.object({ - defaultRoleMapping: z.string().optional(), - defaultOrgMapping: z.string().optional() -}); - -type PolicyFormValues = z.infer; -type DefaultMappingsValues = z.infer; - -export default function PoliciesPage() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const router = useRouter(); - const { idpId } = useParams(); - - const [loading, setLoading] = useState(true); - const [policies, setPolicies] = useState([]); - const [organizations, setOrganizations] = useState([]); - const [showAddDialog, setShowAddDialog] = useState(false); - const [editingPolicy, setEditingPolicy] = useState(null); - - const form = useForm({ - resolver: zodResolver(policyFormSchema), - defaultValues: { - orgId: "", - roleMapping: "", - orgMapping: "" - } - }); - - const defaultMappingsForm = useForm({ - resolver: zodResolver(defaultMappingsSchema), - defaultValues: { - defaultRoleMapping: "", - defaultOrgMapping: "" - } - }); - - const loadIdp = async () => { - try { - const res = await api.get>( - `/idp/${idpId}` - ); - if (res.status === 200) { - const data = res.data.data; - defaultMappingsForm.reset({ - defaultRoleMapping: data.idp.defaultRoleMapping || "", - defaultOrgMapping: data.idp.defaultOrgMapping || "" - }); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } - }; - - const loadPolicies = async () => { - try { - const res = await api.get(`/idp/${idpId}/org`); - if (res.status === 200) { - setPolicies(res.data.data.policies); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } - }; - - const loadOrganizations = async () => { - try { - const res = await api.get>("/orgs"); - if (res.status === 200) { - const existingOrgIds = policies.map((p) => p.orgId); - const availableOrgs = res.data.data.orgs.filter( - (org) => !existingOrgIds.includes(org.orgId) - ); - setOrganizations(availableOrgs); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } - }; - - useEffect(() => { - async function load() { - setLoading(true); - await loadPolicies(); - await loadIdp(); - setLoading(false); - } - load(); - }, [idpId]); - - const onAddPolicy = async (data: PolicyFormValues) => { - setLoading(true); - try { - const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - }); - if (res.status === 201) { - toast({ - title: "Success", - description: "Policy added successfully" - }); - loadPolicies(); - setShowAddDialog(false); - form.reset(); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setLoading(false); - } - }; - - const onEditPolicy = async (data: PolicyFormValues) => { - if (!editingPolicy) return; - - setLoading(true); - try { - const res = await api.post( - `/idp/${idpId}/org/${editingPolicy.orgId}`, - { - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - } - ); - if (res.status === 200) { - setPolicies( - policies.map((policy) => - policy.orgId === editingPolicy.orgId - ? { - ...policy, - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - } - : policy - ) - ); - toast({ - title: "Success", - description: "Policy updated successfully" - }); - setShowAddDialog(false); - setEditingPolicy(null); - form.reset(); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setLoading(false); - } - }; - - const onDeletePolicy = async (orgId: string) => { - try { - const res = await api.delete(`/idp/${idpId}/org/${orgId}`); - if (res.status === 200) { - setPolicies( - policies.filter((policy) => policy.orgId !== orgId) - ); - toast({ - title: "Success", - description: "Policy deleted successfully" - }); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } - }; - - const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { - try { - const res = await api.post(`/idp/${idpId}/oidc`, { - defaultRoleMapping: data.defaultRoleMapping, - defaultOrgMapping: data.defaultOrgMapping - }); - if (res.status === 200) { - toast({ - title: "Success", - description: "Default mappings updated successfully" - }); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } - }; - - if (loading) { - return null; - } - - return ( - <> - - - - - About Organization Policies - - - Organization policies are used to control access to - organizations based on the user's ID token. You can - specify JMESPath expressions to extract role and - organization information from the ID token. For more - information, see{" "} - - the documentation - - - - - - - - - Default Mappings (Optional) - - - The default mappings are used when when there is not - an organization policy defined for an organization. - You can specify the default role and organization - mappings to fall back to here. - - - -
- -
- ( - - - Default Role Mapping - - - - - - JMESPath to extract role - information from the ID - token. The result of this - expression must return the - role name as defined in the - organization as a string. - - - - )} - /> - - ( - - - Default Organization Mapping - - - - - - JMESPath to extract - organization information - from the ID token. This - expression must return thr - org ID or true for the user - to be allowed to access the - organization. - - - - )} - /> -
-
- - - - -
-
- - { - loadOrganizations(); - setEditingPolicy(null); - setShowAddDialog(true); - }} - onEdit={(policy) => { - setEditingPolicy(policy); - form.reset({ - orgId: policy.orgId, - roleMapping: policy.roleMapping || "", - orgMapping: policy.orgMapping || "" - }); - setShowAddDialog(true); - }} - /> -
- - { - setShowAddDialog(val); - setLoading(false); - setEditingPolicy(null); - form.reset(); - }} - > - - - - {editingPolicy - ? "Edit Organization Policy" - : "Add Organization Policy"} - - - Configure access for an organization - - - -
- - ( - - Organization - {editingPolicy ? ( - - ) : ( - - - - - - - - - - - - No org - found. - - - {organizations.map( - ( - org - ) => ( - { - form.setValue( - "orgId", - org.orgId - ); - }} - > - - { - org.name - } - - ) - )} - - - - - - )} - - - )} - /> - - ( - - - Role Mapping Path (Optional) - - - - - - JMESPath to extract role - information from the ID token. - The result of this expression - must return the role name as - defined in the organization as a - string. - - - - )} - /> - - ( - - - Organization Mapping Path - (Optional) - - - - - - JMESPath to extract organization - information from the ID token. - This expression must return the - org ID or true for the user to - be allowed to access the - organization. - - - - )} - /> - - -
- - - - - - -
-
- - ); -}