diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts index 3e09c3c2ca..f03f397711 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts @@ -1,7 +1,7 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { getSSOSettings } from "@/components/networking"; import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { createQueryKeys } from "../common/queryKeysFactory"; -import { getSSOSettings } from "@/components/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export interface SSOFieldSchema { description: string; @@ -27,13 +27,15 @@ export interface SSOSettingsValues { proxy_base_url: string | null; user_email: string | null; ui_access_mode: string | null; - role_mappings: { - provider: string; - group_claim: string; - default_role: "internal_user" | "internal_user_viewer" | "proxy_admin" | "proxy_admin_viewer"; - roles: { - [key: string]: string[]; - }; + role_mappings: RoleMappings; +} + +export interface RoleMappings { + provider: string; + group_claim: string; + default_role: "internal_user" | "internal_user_viewer" | "proxy_admin" | "proxy_admin_viewer"; + roles: { + [key: string]: string[]; }; } diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.test.tsx new file mode 100644 index 0000000000..f4b7b9aadd --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.test.tsx @@ -0,0 +1,92 @@ +import type { RoleMappings as RoleMappingsType } from "@/app/(dashboard)/hooks/sso/useSSOSettings"; +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { renderWithProviders } from "../../../../../tests/test-utils"; +import RoleMappings from "./RoleMappings"; + +describe("RoleMappings", () => { + it("should render successfully", () => { + const roleMappings: RoleMappingsType = { + provider: "generic", + group_claim: "groups", + default_role: "internal_user", + roles: { + proxy_admin: ["admin-group"], + proxy_admin_viewer: [], + internal_user: ["user-group"], + internal_user_viewer: [], + }, + }; + + renderWithProviders(); + + expect(screen.getByText("Role Mappings")).toBeInTheDocument(); + }); + + it("should return null when roleMappings is undefined", () => { + const { container } = renderWithProviders(); + + expect(container.firstChild).toBeNull(); + }); + + it("should display Group Claim and Default Role with correct values and display names", () => { + const testCases: Array<{ role: RoleMappingsType["default_role"]; displayName: string; groupClaim: string }> = [ + { role: "internal_user_viewer", displayName: "Internal Viewer", groupClaim: "custom-groups-1" }, + { role: "internal_user", displayName: "Internal User", groupClaim: "custom-groups-2" }, + { role: "proxy_admin_viewer", displayName: "Proxy Admin Viewer", groupClaim: "custom-groups-3" }, + { role: "proxy_admin", displayName: "Proxy Admin", groupClaim: "custom-groups-4" }, + ]; + + testCases.forEach(({ role, displayName, groupClaim }) => { + const roleMappings: RoleMappingsType = { + provider: "generic", + group_claim: groupClaim, + default_role: role, + roles: { + proxy_admin: [], + proxy_admin_viewer: [], + internal_user: [], + internal_user_viewer: [], + }, + }; + + const { unmount } = renderWithProviders(); + + expect(screen.getByText("Group Claim")).toBeInTheDocument(); + expect(screen.getByText(groupClaim)).toBeInTheDocument(); + expect(screen.getByText("Default Role")).toBeInTheDocument(); + const displayNameElements = screen.getAllByText(displayName); + expect(displayNameElements.length).toBeGreaterThan(0); + unmount(); + }); + }); + + it("should display table with roles, groups as Tags when mapped, and 'No groups mapped' when empty", () => { + const roleMappings: RoleMappingsType = { + provider: "generic", + group_claim: "groups", + default_role: "internal_user", + roles: { + proxy_admin: ["admin-group-1", "admin-group-2", "admin-group-3"], + proxy_admin_viewer: ["viewer-group"], + internal_user: ["user-group"], + internal_user_viewer: [], + }, + }; + + renderWithProviders(); + + expect(screen.getByText("Role")).toBeInTheDocument(); + expect(screen.getByText("Mapped Groups")).toBeInTheDocument(); + expect(screen.getAllByText("Proxy Admin").length).toBeGreaterThan(0); + expect(screen.getAllByText("Proxy Admin Viewer").length).toBeGreaterThan(0); + expect(screen.getAllByText("Internal User").length).toBeGreaterThan(0); + expect(screen.getAllByText("Internal Viewer").length).toBeGreaterThan(0); + expect(screen.getByText("admin-group-1")).toBeInTheDocument(); + expect(screen.getByText("admin-group-2")).toBeInTheDocument(); + expect(screen.getByText("admin-group-3")).toBeInTheDocument(); + expect(screen.getByText("viewer-group")).toBeInTheDocument(); + expect(screen.getByText("user-group")).toBeInTheDocument(); + expect(screen.getByText("No groups mapped")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx new file mode 100644 index 0000000000..3750ee8818 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx @@ -0,0 +1,74 @@ +import type { RoleMappings as RoleMappingsType } from "@/app/(dashboard)/hooks/sso/useSSOSettings"; +import { Card, Divider, Table, Tag, Typography } from "antd"; +import { Users } from "lucide-react"; +import { defaultRoleDisplayNames } from "./constants"; +const { Title, Text } = Typography; + +export default function RoleMappings({ roleMappings }: { roleMappings: RoleMappingsType | undefined }) { + if (!roleMappings) { + return null; + } + + const roleMappingsColumns = [ + { + title: "Role", + dataIndex: "role", + key: "role", + render: (text: string) => {defaultRoleDisplayNames[text]}, + }, + { + title: "Mapped Groups", + dataIndex: "groups", + key: "groups", + render: (groups: string[]) => ( + <> + {groups.length > 0 ? ( + groups.map((group, index) => ( + + {group} + + )) + ) : ( + No groups mapped + )} + + ), + }, + ]; + return ( + +
+ + Role Mappings +
+
+
+
+ Group Claim +
+ {roleMappings.group_claim} +
+
+
+ Default Role +
+ {defaultRoleDisplayNames[roleMappings.default_role]} +
+
+
+ + ({ + role, + groups, + }))} + pagination={false} + bordered + size="small" + className="w-full" + /> + + + ); +} diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx index 27ff96af05..772435e8cb 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx @@ -5,13 +5,14 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { Button, Card, Descriptions, Space, Typography } from "antd"; import { Edit, Shield, Trash2 } from "lucide-react"; import { useState } from "react"; +import { ssoProviderDisplayNames, ssoProviderLogoMap } from "./constants"; import AddSSOSettingsModal from "./Modals/AddSSOSettingsModal"; import DeleteSSOSettingsModal from "./Modals/DeleteSSOSettingsModal"; import EditSSOSettingsModal from "./Modals/EditSSOSettingsModal"; import RedactableField from "./RedactableField"; +import RoleMappings from "./RoleMappings"; import SSOSettingsEmptyPlaceholder from "./SSOSettingsEmptyPlaceholder"; import SSOSettingsLoadingSkeleton from "./SSOSettingsLoadingSkeleton"; -import { ssoProviderDisplayNames, ssoProviderLogoMap } from "./constants"; const { Title, Text } = Typography; @@ -44,6 +45,7 @@ export default function SSOSettings() { }; const selectedProvider = ssoSettings?.values ? detectSSOProvider(ssoSettings.values) : null; + const isRoleMappingsEnabled = Boolean(ssoSettings?.values.role_mappings); const renderEndpointValue = (value?: string | null) => ( @@ -185,39 +187,46 @@ export default function SSOSettings() { {isLoading ? ( ) : ( - - - {/* Header Section */} -
-
- -
- SSO Configuration - Manage Single Sign-On authentication settings + + + + {/* Header Section */} +
+
+ +
+ SSO Configuration + Manage Single Sign-On authentication settings +
+
+ +
+ {isSSOConfigured && ( + <> + + + + )}
-
- {isSSOConfigured && ( - <> - - - - )} -
-
- - {isSSOConfigured ? ( - renderSSOSettings() - ) : ( - setIsAddModalVisible(true)} /> - )} - - + {isSSOConfigured ? ( + renderSSOSettings() + ) : ( + setIsAddModalVisible(true)} /> + )} + + + {isRoleMappingsEnabled && } + )} = { okta: "Okta / Auth0 SSO", generic: "Generic SSO", }; + +export const defaultRoleDisplayNames: Record = { + internal_user_viewer: "Internal Viewer", + internal_user: "Internal User", + proxy_admin_viewer: "Proxy Admin Viewer", + proxy_admin: "Proxy Admin", +};