diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index b02f5b18..f5a9cdc1 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -14,4 +14,5 @@ export * from "./verifyAdmin"; export * from "./verifySetResourceUsers"; export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; -export * from "./verifyUserIsServerAdmin"; \ No newline at end of file +export * from "./verifyUserIsServerAdmin"; +export * from "./verifyIsLoggedInUser"; diff --git a/server/middlewares/verifyIsLoggedInUser.ts b/server/middlewares/verifyIsLoggedInUser.ts new file mode 100644 index 00000000..bee066b7 --- /dev/null +++ b/server/middlewares/verifyIsLoggedInUser.ts @@ -0,0 +1,44 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyIsLoggedInUser( + req: Request, + res: Response, + next: NextFunction +) { + try { + const userId = req.user!.userId; + const reqUserId = + req.params.userId || req.body.userId || req.query.userId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + // allow server admins to access any user + if (req.user?.serverAdmin) { + return next(); + } + + if (reqUserId !== userId) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User only has access to their own account" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error checking if user has access to this user" + ) + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index 903885b9..4b939b4a 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -25,7 +25,8 @@ import { verifySetResourceUsers, verifyUserAccess, getUserOrgs, - verifyUserIsServerAdmin + verifyUserIsServerAdmin, + verifyIsLoggedInUser } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -47,7 +48,10 @@ authenticated.use(verifySessionUserMiddleware); authenticated.get("/org/checkId", org.checkId); authenticated.put("/org", getUserOrgs, org.createOrg); -authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here + +authenticated.get("/orgs", verifyUserIsServerAdmin, org.listOrgs); +authenticated.get("/user/:userId/orgs", verifyIsLoggedInUser, org.listUserOrgs); + authenticated.get( "/org/:orgId", verifyOrgAccess, @@ -507,23 +511,11 @@ authenticated.post( idp.updateOidcIdp ); -authenticated.delete( - "/idp/:idpId", - verifyUserIsServerAdmin, - idp.deleteIdp -); +authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); -authenticated.get( - "/idp", - verifyUserIsServerAdmin, - idp.listIdps -); +authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps); -authenticated.get( - "/idp/:idpId", - verifyUserIsServerAdmin, - idp.getIdp -); +authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); authenticated.put( "/idp/:idpId/org/:orgId", @@ -531,6 +523,12 @@ authenticated.put( idp.createIdpOrgPolicy ); +authenticated.post( + "/idp/:idpId/org/:orgId", + verifyUserIsServerAdmin, + idp.updateIdpOrgPolicy +); + authenticated.delete( "/idp/:idpId/org/:orgId", verifyUserIsServerAdmin, @@ -631,17 +629,8 @@ authRouter.post( resource.authWithAccessToken ); -authRouter.post( - "/access-token", - resource.authWithAccessToken -); +authRouter.post("/access-token", resource.authWithAccessToken); -authRouter.post( - "/idp/:idpId/oidc/generate-url", - idp.generateOidcUrl -); +authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl); -authRouter.post( - "/idp/:idpId/oidc/validate-callback", - idp.validateOidcCallback -); +authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index 13c5c1b8..808c7ca7 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -77,10 +77,13 @@ export async function createIdpOrgPolicy( const [existing] = await db .select() .from(idp) - .leftJoin(idpOrg, eq(idpOrg.orgId, orgId)) - .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))); + .leftJoin( + idpOrg, + and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) + ) + .where(eq(idp.idpId, idpId)); - if (!existing.idp) { + if (!existing?.idp) { return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index 731d39e8..f0dcf02e 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -8,3 +8,4 @@ export * from "./getIdp"; export * from "./createIdpOrgPolicy"; export * from "./deleteIdpOrgPolicy"; export * from "./listIdpOrgPolicies"; +export * from "./updateIdpOrgPolicy"; diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts new file mode 100644 index 00000000..2c5079e4 --- /dev/null +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -0,0 +1,126 @@ +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/org/index.ts b/server/routers/org/index.ts index 04ff1362..5623823d 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -2,6 +2,7 @@ export * from "./getOrg"; export * from "./createOrg"; export * from "./deleteOrg"; export * from "./updateOrg"; -export * from "./listOrgs"; +export * from "./listUserOrgs"; export * from "./checkId"; export * from "./getOrgOverview"; +export * from "./listOrgs"; diff --git a/server/routers/org/listOrgs.ts b/server/routers/org/listOrgs.ts index f299e3f2..611b928e 100644 --- a/server/routers/org/listOrgs.ts +++ b/server/routers/org/listOrgs.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { Org, orgs } from "@server/db/schemas"; +import { Org, orgs, userOrgs } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { sql, inArray } from "drizzle-orm"; +import { sql, inArray, eq } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -27,8 +27,8 @@ const listOrgsSchema = z.object({ registry.registerPath({ method: "get", - path: "/orgs", - description: "List all organizations in the system", + path: "/user/:userId/orgs", + description: "List all organizations in the system.", tags: [OpenAPITags.Org], request: { query: listOrgsSchema @@ -59,37 +59,15 @@ export async function listOrgs( const { limit, offset } = parsedQuery.data; - // Use the userOrgs passed from the middleware - const userOrgIds = req.userOrgIds; - - if (!userOrgIds || userOrgIds.length === 0) { - return response(res, { - data: { - orgs: [], - pagination: { - total: 0, - limit, - offset - } - }, - success: true, - error: false, - message: "No organizations found for the user", - status: HttpCode.OK - }); - } - const organizations = await db .select() .from(orgs) - .where(inArray(orgs.orgId, userOrgIds)) .limit(limit) .offset(offset); const totalCountResult = await db .select({ count: sql`cast(count(*) as integer)` }) - .from(orgs) - .where(inArray(orgs.orgId, userOrgIds)); + .from(orgs); const totalCount = totalCountResult[0].count; return response(res, { diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts new file mode 100644 index 00000000..64d0871f --- /dev/null +++ b/server/routers/org/listUserOrgs.ts @@ -0,0 +1,141 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { Org, orgs, userOrgs } from "@server/db/schemas"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { sql, inArray, eq } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromZodError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listOrgsParamsSchema = z.object({ + userId: z.string() +}); + +const listOrgsSchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +registry.registerPath({ + method: "get", + path: "/user/:userId/orgs", + description: "List all organizations for a user.", + tags: [OpenAPITags.Org, OpenAPITags.User], + request: { + query: listOrgsSchema + }, + responses: {} +}); + +export type ListUserOrgsResponse = { + orgs: Org[]; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listUserOrgs( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listOrgsSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedQuery.error) + ) + ); + } + + const { limit, offset } = parsedQuery.data; + + const parsedParams = listOrgsParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedParams.error) + ) + ); + } + + const { userId } = parsedParams.data; + + const userOrganizations = await db + .select({ + orgId: userOrgs.orgId, + roleId: userOrgs.roleId + }) + .from(userOrgs) + .where(eq(userOrgs.userId, userId)); + + const userOrgIds = userOrganizations.map((org) => org.orgId); + + if (!userOrgIds || userOrgIds.length === 0) { + return response(res, { + data: { + orgs: [], + pagination: { + total: 0, + limit, + offset + } + }, + success: true, + error: false, + message: "No organizations found for the user", + status: HttpCode.OK + }); + } + + const organizations = await db + .select() + .from(orgs) + .where(inArray(orgs.orgId, userOrgIds)) + .limit(limit) + .offset(offset); + + const totalCountResult = await db + .select({ count: sql`cast(count(*) as integer)` }) + .from(orgs) + .where(inArray(orgs.orgId, userOrgIds)); + const totalCount = totalCountResult[0].count; + + return response(res, { + data: { + orgs: organizations, + pagination: { + total: totalCount, + limit, + offset + } + }, + success: true, + error: false, + message: "Organizations retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred..." + ) + ); + } +} diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 959a32a6..49f3d834 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -31,7 +31,7 @@ import { CardTitle } from "@/components/ui/card"; import { AxiosResponse } from "axios"; -import { DeleteOrgResponse, ListOrgsResponse } from "@server/routers/org"; +import { DeleteOrgResponse, ListUserOrgsResponse } from "@server/routers/org"; import { redirect, useRouter } from "next/navigation"; import { SettingsContainer, @@ -43,6 +43,7 @@ import { SettingsSectionForm, SettingsSectionFooter } from "@app/components/Settings"; +import { useUserContext } from "@app/hooks/useUserContext"; const GeneralFormSchema = z.object({ name: z.string() @@ -57,6 +58,7 @@ export default function GeneralPage() { const router = useRouter(); const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); + const { user } = useUserContext(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); @@ -101,7 +103,9 @@ export default function GeneralPage() { async function pickNewOrgAndNavigate() { try { - const res = await api.get>(`/orgs`); + const res = await api.get>( + `/user/${user.userId}/orgs` + ); if (res.status === 200) { if (res.data.data.orgs.length > 0) { @@ -237,9 +241,7 @@ export default function GeneralPage() { - - Danger Zone - + Danger Zone Once you delete this org, there is no going back. Please be certain. diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 206f52f9..ac5e552b 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -11,7 +11,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; -import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org"; +import { GetOrgResponse, ListUserOrgsResponse } from "@server/routers/org"; import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; import { GetOrgUserResponse } from "@server/routers/user"; @@ -62,10 +62,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}`); } - let orgs: ListOrgsResponse["orgs"] = []; + let orgs: ListUserOrgsResponse["orgs"] = []; try { const getOrgs = cache(() => - internal.get>(`/orgs`, cookie) + internal.get>( + `/user/${user.userId}/orgs`, + cookie + ) ); const res = await getOrgs(); if (res && res.data.data.orgs) { diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index 193cbe4e..56d2ad05 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -39,6 +39,10 @@ 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 new file mode 100644 index 00000000..b9543daa --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx @@ -0,0 +1,28 @@ +"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 new file mode 100644 index 00000000..09ba309f --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx @@ -0,0 +1,154 @@ +"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 new file mode 100644 index 00000000..121f6a85 --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -0,0 +1,453 @@ +"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 SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +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"; + +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() +}); + +type PolicyFormValues = 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 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(); + 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" + }); + } + }; + + 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 + + + + + + { + 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) + + +