From 3e063ee252f5ef6d7fbaec4d74f655205436fc79 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Sat, 8 Aug 2026 11:37:07 -0700 Subject: [PATCH] fix(ui): keep a view-only session read-only on the destinations table proxy_admin_viewer is mapped to the effective role Admin so it gets read parity with proxy admin, and the write restriction travels separately on isViewOnly. This page derived write access from the role alone, so after that mapping landed a read-only admin saw Add Callback and the per-row Edit scope and Delete actions on admin-owned destinations. Verified live: the viewer session now renders no Add Callback and keeps only the config-callback Test menu, while a proxy admin keeps all eleven row menus. --- .../(dashboard)/logging-and-alerts/page.tsx | 12 +++++- .../src/components/settings.test.tsx | 42 +++++++++++++++++++ .../src/components/settings.tsx | 5 ++- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx index 8232e391259..ea6f2dcdfd1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx @@ -4,6 +4,14 @@ import Settings from "@/components/settings"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export default function LoggingAndAlerts() { - const { accessToken, userRole, userId, premiumUser } = useAuthorized(); - return ; + const { accessToken, userRole, userId, premiumUser, isViewOnly } = useAuthorized(); + return ( + + ); } diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index c8b7418d45d..e4a20bedd66 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -387,3 +387,45 @@ describe("Add Callback dropdown", () => { expect(screen.getByText("Arize (scoped destination)")).toBeInTheDocument(); }); }); + +describe("Settings read-only admin", () => { + beforeEach(() => { + vi.clearAllMocks(); + credentialsFixture = { credentials: [] }; + vi.mocked(getCallbacksCall).mockResolvedValue({ callbacks: [], available_callbacks: [], alerts: [] }); + vi.mocked(getCallbackConfigsCall).mockResolvedValue([]); + vi.mocked(alertingSettingsCall).mockResolvedValue([]); + }); + + it("gives a view-only session no write affordances even though its role reads as Admin", async () => { + // Regression: proxy_admin_viewer is mapped to the effective role "Admin" so it gets + // read parity, and the write restriction travels separately on isViewOnly. Deriving + // write access from the role alone handed the viewer Add, Edit scope and Delete. + const { queryByText } = renderSettings({ + accessToken: "token", + userRole: "Admin", + userID: "viewer-1", + premiumUser: false, + isViewOnly: true, + } as never); + + await waitFor(() => { + expect(queryByText("Active Logging Callbacks")).toBeInTheDocument(); + }); + expect(queryByText("Add Callback")).not.toBeInTheDocument(); + }); + + it("keeps the write affordances for a real proxy admin", async () => { + const { queryByText } = renderSettings({ + accessToken: "token", + userRole: "Admin", + userID: "admin-1", + premiumUser: false, + }); + + await waitFor(() => { + expect(queryByText("Active Logging Callbacks")).toBeInTheDocument(); + }); + expect(queryByText("Add Callback")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 9def17ede04..dacba0524bc 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -57,6 +57,7 @@ interface SettingsPageProps { userRole: string | null; userID: string | null; premiumUser: boolean; + isViewOnly?: boolean; } const assetsLogoFolder = "/ui/assets/logos/"; @@ -215,7 +216,7 @@ const buildCallbackPayload = (formValues: Record, callbackName: str }; }; -const Settings: React.FC = ({ accessToken, userRole, userID, premiumUser }) => { +const Settings: React.FC = ({ accessToken, userRole, userID, premiumUser, isViewOnly = false }) => { const [callbacks, setCallbacks] = useState([]); const [isLoadingCallbacks, setIsLoadingCallbacks] = useState(true); const [alerts, setAlerts] = useState([]); @@ -253,7 +254,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, // credential_type=logging; they share the one Active Logging Callbacks table as // rows alongside config callbacks. Only a proxy admin (or admin-viewer, read-only) // may read them, so non-admins skip the fetch entirely. - const isProxyAdmin = userRole != null && isProxyAdminRole(userRole); + const isProxyAdmin = !isViewOnly && userRole != null && isProxyAdminRole(userRole); const { data: credentialData, refetch: refetchCredentials } = useCredentials(canReadCredentialsRole(userRole)); const { data: teamsData } = useTeams(); const { data: orgsData } = useOrganizations();