mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-18 20:18:38 +00:00
Merge pull request #18677 from BerriAI/litellm_ui_sso_settings_role
[Feature] UI - SSO Settings Page Add Role Mappings
This commit is contained in:
@@ -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[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+92
@@ -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(<RoleMappings roleMappings={roleMappings} />);
|
||||
|
||||
expect(screen.getByText("Role Mappings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return null when roleMappings is undefined", () => {
|
||||
const { container } = renderWithProviders(<RoleMappings roleMappings={undefined} />);
|
||||
|
||||
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(<RoleMappings roleMappings={roleMappings} />);
|
||||
|
||||
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(<RoleMappings roleMappings={roleMappings} />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
+74
@@ -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) => <Text strong>{defaultRoleDisplayNames[text]}</Text>,
|
||||
},
|
||||
{
|
||||
title: "Mapped Groups",
|
||||
dataIndex: "groups",
|
||||
key: "groups",
|
||||
render: (groups: string[]) => (
|
||||
<>
|
||||
{groups.length > 0 ? (
|
||||
groups.map((group, index) => (
|
||||
<Tag key={index} color="blue">
|
||||
{group}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-gray-400 italic">No groups mapped</Text>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="w-6 h-6 text-gray-400 mb-2" />
|
||||
<Title level={3}>Role Mappings</Title>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Title level={5}>Group Claim</Title>
|
||||
<div>
|
||||
<Text code>{roleMappings.group_claim}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Title level={5}>Default Role</Title>
|
||||
<div>
|
||||
<Text strong>{defaultRoleDisplayNames[roleMappings.default_role]}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Table
|
||||
columns={roleMappingsColumns}
|
||||
dataSource={Object.entries(roleMappings.roles).map(([role, groups]) => ({
|
||||
role,
|
||||
groups,
|
||||
}))}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
+40
-31
@@ -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) => (
|
||||
<Text className="font-mono text-gray-600 text-sm" copyable={!!value}>
|
||||
@@ -185,39 +187,46 @@ export default function SSOSettings() {
|
||||
{isLoading ? (
|
||||
<SSOSettingsLoadingSkeleton />
|
||||
) : (
|
||||
<Card>
|
||||
<Space direction="vertical" size="large" className="w-full">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-6 h-6 text-gray-400" />
|
||||
<div>
|
||||
<Title level={3}>SSO Configuration</Title>
|
||||
<Text type="secondary">Manage Single Sign-On authentication settings</Text>
|
||||
<Space direction="vertical" size="large" className="w-full">
|
||||
<Card>
|
||||
<Space direction="vertical" size="large" className="w-full">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-6 h-6 text-gray-400" />
|
||||
<div>
|
||||
<Title level={3}>SSO Configuration</Title>
|
||||
<Text type="secondary">Manage Single Sign-On authentication settings</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isSSOConfigured && (
|
||||
<>
|
||||
<Button icon={<Edit className="w-4 h-4" />} onClick={() => setIsEditModalVisible(true)}>
|
||||
Edit SSO Settings
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
onClick={() => setIsDeleteModalVisible(true)}
|
||||
>
|
||||
Delete SSO Settings
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isSSOConfigured && (
|
||||
<>
|
||||
<Button icon={<Edit className="w-4 h-4" />} onClick={() => setIsEditModalVisible(true)}>
|
||||
Edit SSO Settings
|
||||
</Button>
|
||||
<Button danger icon={<Trash2 className="w-4 h-4" />} onClick={() => setIsDeleteModalVisible(true)}>
|
||||
Delete SSO Settings
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSSOConfigured ? (
|
||||
renderSSOSettings()
|
||||
) : (
|
||||
<SSOSettingsEmptyPlaceholder onAdd={() => setIsAddModalVisible(true)} />
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
{isSSOConfigured ? (
|
||||
renderSSOSettings()
|
||||
) : (
|
||||
<SSOSettingsEmptyPlaceholder onAdd={() => setIsAddModalVisible(true)} />
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
{isRoleMappingsEnabled && <RoleMappings roleMappings={ssoSettings?.values.role_mappings} />}
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<DeleteSSOSettingsModal
|
||||
|
||||
@@ -13,3 +13,10 @@ export const ssoProviderDisplayNames: Record<string, string> = {
|
||||
okta: "Okta / Auth0 SSO",
|
||||
generic: "Generic SSO",
|
||||
};
|
||||
|
||||
export const defaultRoleDisplayNames: Record<string, string> = {
|
||||
internal_user_viewer: "Internal Viewer",
|
||||
internal_user: "Internal User",
|
||||
proxy_admin_viewer: "Proxy Admin Viewer",
|
||||
proxy_admin: "Proxy Admin",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user