Merge pull request #36469 from BerriAI/litellm_/nifty-knuth-f7f2c6

fix(ui): gate the Old Usage page behind a proxy-admin capability
This commit is contained in:
yuneng-jiang 2026-08-10 16:46:52 -07:00 committed by GitHub
commit 022c0cce95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 158 additions and 20 deletions

View file

@ -1,6 +1,6 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen, waitFor, within } from "@testing-library/react";
import { act, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../../../tests/test-utils";
import UsagePage from "./usage";
@ -49,6 +49,14 @@ const renderUsage = (overrides: Partial<React.ComponentProps<typeof UsagePage>>
/>,
);
// Width of this window is guarded by "proves the flush window is wide enough".
const flushPendingRequests = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
beforeEach(() => {
vi.clearAllMocks();
networking.getProxyUISettings.mockResolvedValue(UNLIMITED_SETTINGS);
@ -185,18 +193,65 @@ describe("old usage page", () => {
});
});
describe("as a non-admin", () => {
it("renders only the All Up tab and skips admin-only queries", async () => {
renderUsage({ userRole: "Internal User" });
// org_admin is an organization membership role; those users reach the UI as "Internal User".
describe.each(["Internal User", "Internal Viewer", "internal_user", "internal_user_viewer", "Org Admin"])(
"as %s",
(userRole) => {
it("shows the admin-only notice instead of the usage dashboard", async () => {
renderUsage({ userRole });
expect(await screen.findByText(/Proxy-wide usage is only available to admin users/i)).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "All Up" })).not.toBeInTheDocument();
});
it("fires no /global/spend or /global/activity request", async () => {
renderUsage({ userRole });
await screen.findByText(/Proxy-wide usage is only available to admin users/i);
await flushPendingRequests();
expect(networking.getProxyUISettings).not.toHaveBeenCalled();
expect(networking.adminSpendLogsCall).not.toHaveBeenCalled();
expect(networking.adminTopKeysCall).not.toHaveBeenCalled();
expect(networking.adminTopModelsCall).not.toHaveBeenCalled();
expect(networking.adminTopEndUsersCall).not.toHaveBeenCalled();
expect(networking.teamSpendLogsCall).not.toHaveBeenCalled();
expect(networking.tagsSpendLogsCall).not.toHaveBeenCalled();
expect(networking.allTagNamesCall).not.toHaveBeenCalled();
expect(networking.adminspendByProvider).not.toHaveBeenCalled();
expect(networking.adminGlobalActivity).not.toHaveBeenCalled();
expect(networking.adminGlobalActivityPerModel).not.toHaveBeenCalled();
});
},
);
describe("the admin-only gate", () => {
it("proves the flush window is wide enough to catch a leaked request", async () => {
renderUsage({ userRole: "Admin" });
await flushPendingRequests();
expect(networking.getProxyUISettings).toHaveBeenCalled();
expect(networking.adminSpendLogsCall).toHaveBeenCalled();
expect(networking.tagsSpendLogsCall).toHaveBeenCalled();
expect(networking.adminGlobalActivity).toHaveBeenCalled();
});
it("still lets an admin through, so the notice is a real gate and not a dead branch", async () => {
renderUsage({ userRole: "Admin" });
expect(await screen.findByRole("tab", { name: "All Up" })).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Team Based Usage" })).not.toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Customer Usage" })).not.toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Tag Based Usage" })).not.toBeInTheDocument();
expect(screen.queryByText(/Proxy-wide usage is only available to admin users/i)).not.toBeInTheDocument();
await waitFor(() => expect(networking.adminSpendLogsCall).toHaveBeenCalled());
expect(networking.teamSpendLogsCall).not.toHaveBeenCalled();
expect(networking.adminTopEndUsersCall).not.toHaveBeenCalled();
});
it("does not put the session token in the provider spend query", async () => {
renderUsage({ userRole: "Admin", token: "session-jwt-value" });
await waitFor(() => expect(networking.adminspendByProvider).toHaveBeenCalled());
const callArgs = networking.adminspendByProvider.mock.calls[0];
expect(callArgs).not.toContain("session-jwt-value");
expect(callArgs[0]).toBe("sk-test");
});
});
});

View file

@ -37,6 +37,7 @@ import {
} from "@/components/networking";
import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView";
import { MoneyCell } from "@/components/shared/table_cells";
import { hasCapability } from "@/utils/capabilities";
import { formatNumberWithCommas } from "@/utils/dataUtils";
interface UsagePageProps {
@ -90,6 +91,7 @@ const TeamSpendBarList: React.FC<{ data: TeamSpendTotal[] }> = ({ data }) => {
};
const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, userID, keys, premiumUser }) => {
const canViewGlobalSpend = hasCapability(userRole, "viewGlobalSpend");
const currentDate = new Date();
const [keySpendData, setKeySpendData] = useState<any[]>([]);
const [topKeys, setTopKeys] = useState<any[]>([]);
@ -155,8 +157,11 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
};
useEffect(() => {
if (!canViewGlobalSpend) {
return;
}
updateTagSpendData(dateValue.from, dateValue.to);
}, [dateValue, selectedTags]);
}, [canViewGlobalSpend, dateValue, selectedTags]);
const updateEndUserData = async (
startTime: Date | undefined,
@ -319,10 +324,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
const fetchProviderSpend = () =>
fetchAndSetData(
() =>
accessToken && token
? adminspendByProvider(accessToken, token, startTime, endTime)
: Promise.reject("No access token or token"),
() => (accessToken ? adminspendByProvider(accessToken, startTime, endTime) : Promise.reject("No access token")),
setSpendByProvider,
"Error fetching provider spend",
);
@ -467,6 +469,9 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
useEffect(() => {
const initlizeUsageData = async () => {
if (!canViewGlobalSpend) {
return;
}
if (accessToken && token && userRole && userID) {
const proxy_settings: ProxySettings | undefined = await fetchProxySettings();
if (proxy_settings) {
@ -493,7 +498,24 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
};
initlizeUsageData();
}, [accessToken, token, userRole, userID, startTime, endTime]);
}, [canViewGlobalSpend, accessToken, token, userRole, userID, startTime, endTime]);
if (!canViewGlobalSpend) {
return (
<div className="w-full p-8">
<Card>
<CardHeader>
<CardTitle>Usage</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Proxy-wide usage is only available to admin users. Your own usage is on the Usage page.
</p>
</CardContent>
</Card>
</div>
);
}
if (proxySettings?.DISABLE_EXPENSIVE_DB_QUERIES) {
return (

View file

@ -6,6 +6,7 @@ import Sidebar, { menuGroups, getBreadcrumb } from "./leftnav";
vi.mock("../utils/roles", () => {
return {
all_admin_roles: ["admin", "admin_viewer"],
old_admin_roles: ["admin", "admin_viewer"],
internalUserRoles: ["internal"],
rolesWithWriteAccess: ["admin", "internal"],
rolesAllowedToViewWriteScopedPages: ["admin", "internal", "admin_viewer"],
@ -266,6 +267,30 @@ describe("Sidebar (leftnav)", () => {
});
expect(screen.queryByText("Prompts")).not.toBeInTheDocument();
});
it("should hide Old Usage from internal users while keeping other Experimental children", async () => {
mockUseAuthorized.mockReturnValue(internalAuth);
renderWithProviders(<Sidebar {...defaultProps} />);
act(() => {
fireEvent.click(screen.getByText("Experimental"));
});
await waitFor(() => {
expect(screen.getByText("API Playground")).toBeInTheDocument();
});
expect(screen.queryByText("Old Usage")).not.toBeInTheDocument();
});
it("should show Old Usage to admins", async () => {
renderWithProviders(<Sidebar {...defaultProps} />);
act(() => {
fireEvent.click(screen.getByText("Experimental"));
});
await waitFor(() => {
expect(screen.getByText("Old Usage")).toBeInTheDocument();
});
});
});
it("should show Organizations tab for organization admins", () => {

View file

@ -289,7 +289,13 @@ const menuGroups: MenuGroup[] = [
icon: <Tags {...ICON} />,
roles: all_admin_roles,
},
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChart3 {...ICON} /> },
{
key: "4",
page: "usage",
label: "Old Usage",
icon: <BarChart3 {...ICON} />,
roles: rolesWithCapability("viewGlobalSpend"),
},
],
},
],

View file

@ -2099,7 +2099,6 @@ export const adminTopEndUsersCall = async (
export const adminspendByProvider = async (
accessToken: string,
keyToken: string | null,
startTime: string | undefined,
endTime: string | undefined,
) => {
@ -2108,7 +2107,6 @@ export const adminspendByProvider = async (
accessToken,
query: {
...(startTime && endTime ? { start_date: startTime, end_date: endTime } : {}),
...(keyToken ? { api_key: keyToken } : {}),
},
});
return data;

View file

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { hasCapability, rolesWithCapability, type Capability } from "./capabilities";
import { effectiveSessionRole } from "./roles";
const ADMIN_ROLES = ["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer"];
const NON_ADMIN_ROLES = [
@ -37,6 +38,34 @@ describe("hasCapability", () => {
});
});
describe("hasCapability - viewGlobalSpend", () => {
it.each(ADMIN_ROLES)("should grant it to %s", (role) => {
expect(hasCapability(role, "viewGlobalSpend")).toBe(true);
});
it.each([...NON_ADMIN_ROLES, "internal_user_viewer", "org_admin"])("should deny it to %s", (role) => {
expect(hasCapability(role, "viewGlobalSpend")).toBe(false);
});
it("should deny it to every role an org admin or team admin can present at runtime", () => {
const orgAdminSessionRole = effectiveSessionRole("internal_user");
const teamAdminSessionRole = effectiveSessionRole("internal_user");
expect(orgAdminSessionRole).toBe("Internal User");
expect(hasCapability(orgAdminSessionRole, "viewGlobalSpend")).toBe(false);
expect(hasCapability(teamAdminSessionRole, "viewGlobalSpend")).toBe(false);
});
it.each([
["proxy_admin", true],
["proxy_admin_viewer", true],
["internal_user", false],
["internal_user_viewer", false],
] as const)("should match the backend for a %s session", (rawRole, expected) => {
expect(hasCapability(effectiveSessionRole(rawRole), "viewGlobalSpend")).toBe(expected);
});
});
describe("rolesWithCapability", () => {
it("should return a copy so callers cannot mutate the capability map", () => {
const roles = rolesWithCapability("viewToolPolicies");

View file

@ -1,4 +1,6 @@
import { all_admin_roles } from "./roles";
import { all_admin_roles, old_admin_roles } from "./roles";
const proxyAdminOnlyRoles = [...old_admin_roles, "proxy_admin", "proxy_admin_viewer"];
const CAPABILITY_ROLES = {
viewToolPolicies: all_admin_roles,
@ -8,6 +10,7 @@ const CAPABILITY_ROLES = {
viewPrompts: all_admin_roles,
viewOrganizationUsage: all_admin_roles,
viewAgentUsage: all_admin_roles,
viewGlobalSpend: proxyAdminOnlyRoles,
} as const satisfies Record<string, readonly string[]>;
export type Capability = keyof typeof CAPABILITY_ROLES;