Merge pull request #36333 from BerriAI/litellm_/elated-bhaskara-739752

fix(ui): hide admin-only Logs tabs from roles that cannot call their endpoints
This commit is contained in:
yuneng-jiang 2026-08-10 13:39:51 -07:00 • committed by GitHub
commit 3e680a4ffc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 254 additions and 35 deletions

View file

@ -0,0 +1,104 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import SpendLogsTable from "./index";
import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: useAuthorizedMock,
}));
vi.mock("./RequestLogsPanel", () => ({
default: function RequestLogsPanelMock() {
return <div data-testid="request-logs-panel" />;
},
}));
const fetchMock = vi.fn();
const jsonResponse = (body: unknown) => ({
ok: true,
status: 200,
statusText: "OK",
json: async () => body,
});
const requestedUrls = () => fetchMock.mock.calls.map(([url]) => String(url));
const emptyAuditLogs = { audit_logs: [], total: 0, page: 1, page_size: 50, total_pages: 0 };
const defaultProps = {
accessToken: "sk-test",
token: "jwt-test",
userRole: "Admin",
userID: "user-1",
premiumUser: true,
};
const renderAs = (sessionRole: string) => {
useAuthorizedMock.mockReturnValue({ accessToken: "sk-test", userRole: sessionRole, premiumUser: true });
return renderWithProviders(<SpendLogsTable {...defaultProps} userRole={sessionRole} />);
};
describe("SpendLogsTable network access by role", () => {
beforeEach(() => {
testQueryClient.clear();
vi.clearAllMocks();
fetchMock.mockImplementation(async (url: string) => {
if (String(url).includes("/audit")) {
return jsonResponse(emptyAuditLogs);
}
if (String(url).includes("/v2/team/list")) {
return jsonResponse({ teams: [] });
}
return jsonResponse({ keys: [], total_count: 0 });
});
vi.stubGlobal("fetch", fetchMock);
});
it("fires neither the audit nor the deleted-teams request for an internal user", async () => {
const user = userEvent.setup();
renderAs("Internal User");
// Liveness gate: the sibling Deleted Keys panel does reach the network, so a
// silent absence below means the gate worked, not that nothing rendered.
await waitFor(() => expect(requestedUrls().some((url) => url.includes("/key/list"))).toBe(true));
await user.click(screen.getByRole("tab", { name: "Deleted Keys" }));
await user.click(screen.getByRole("tab", { name: "Request Logs" }));
expect(requestedUrls().filter((url) => url.includes("/audit"))).toEqual([]);
expect(requestedUrls().filter((url) => url.includes("/v2/team/list"))).toEqual([]);
});
it("fetches deleted teams and audit logs for an admin", async () => {
const user = userEvent.setup();
renderAs("Admin");
await waitFor(() =>
expect(requestedUrls().some((url) => url.includes("/v2/team/list") && url.includes("status=deleted"))).toBe(true),
);
expect(requestedUrls().filter((url) => url.includes("/audit"))).toEqual([]);
await user.click(screen.getByRole("tab", { name: "Audit Logs" }));
await waitFor(() => expect(requestedUrls().some((url) => url.includes("/audit"))).toBe(true));
});
it("leaves the audit request unsent when an admin selects a tab after Audit Logs", async () => {
const user = userEvent.setup();
renderAs("Admin");
await user.click(screen.getByRole("tab", { name: "Deleted Teams" }));
expect(screen.getByRole("tab", { name: "Deleted Teams" })).toHaveAttribute("aria-selected", "true");
expect(requestedUrls().filter((url) => url.includes("/audit"))).toEqual([]);
await user.click(screen.getByRole("tab", { name: "Audit Logs" }));
await waitFor(() => expect(requestedUrls().some((url) => url.includes("/audit"))).toBe(true));
});
});

View file

@ -1,9 +1,15 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import SpendLogsTable from "./index";
import { renderWithProviders } from "../../../tests/test-utils";
const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: useAuthorizedMock,
}));
vi.mock("./RequestLogsPanel", () => ({
default: function RequestLogsPanelMock({ isActive }: { isActive: boolean }) {
return <div data-testid="request-logs-panel">{isActive ? "active" : "inactive"}</div>;
@ -36,9 +42,18 @@ const defaultProps = {
premiumUser: false,
};
const renderAs = (sessionRole: string) => {
useAuthorizedMock.mockReturnValue({ userRole: sessionRole });
return renderWithProviders(<SpendLogsTable {...defaultProps} userRole={sessionRole} />);
};
describe("SpendLogsTable", () => {
beforeEach(() => {
useAuthorizedMock.mockReturnValue({ userRole: "Admin" });
});
it("renders the four log tabs", () => {
renderWithProviders(<SpendLogsTable {...defaultProps} />);
renderAs("Admin");
for (const label of ["Request Logs", "Audit Logs", "Deleted Keys", "Deleted Teams"]) {
expect(screen.getByRole("tab", { name: label })).toBeInTheDocument();
@ -47,7 +62,7 @@ describe("SpendLogsTable", () => {
it("marks only the visible tab's panel active so background tabs do not query", async () => {
const user = userEvent.setup();
renderWithProviders(<SpendLogsTable {...defaultProps} />);
renderAs("Admin");
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("active");
@ -57,8 +72,64 @@ describe("SpendLogsTable", () => {
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
});
describe("admin-only tabs", () => {
it.each(["Internal User", "Internal Viewer"])("hides Audit Logs and Deleted Teams from %s", (role) => {
renderAs(role);
expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Deleted Keys" })).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Audit Logs" })).not.toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Deleted Teams" })).not.toBeInTheDocument();
});
it("never mounts the panels that call the admin-only endpoints for an internal user", () => {
renderAs("Internal User");
expect(screen.queryByTestId("audit-logs-panel")).not.toBeInTheDocument();
expect(screen.queryByTestId("deleted-teams-page")).not.toBeInTheDocument();
expect(screen.getByTestId("deleted-keys-page")).toBeInTheDocument();
});
});
describe("tab index mapping", () => {
it("activates the panel the admin selected, not the one at the old hardcoded index", async () => {
const user = userEvent.setup();
renderAs("Admin");
await user.click(screen.getByRole("tab", { name: "Deleted Keys" }));
expect(screen.getByTestId("audit-logs-panel")).toHaveTextContent("inactive");
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
});
it("keeps the audit panel inert when an admin selects the last tab", async () => {
const user = userEvent.setup();
renderAs("Admin");
await user.click(screen.getByRole("tab", { name: "Deleted Teams" }));
expect(screen.getByTestId("audit-logs-panel")).toHaveTextContent("inactive");
expect(screen.getByTestId("deleted-teams-page")).toBeInTheDocument();
});
it("selects the last visible tab for an internal user and returns to Request Logs", async () => {
const user = userEvent.setup();
renderAs("Internal User");
await user.click(screen.getByRole("tab", { name: "Deleted Keys" }));
expect(screen.getByTestId("deleted-keys-page")).toBeInTheDocument();
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
await user.click(screen.getByRole("tab", { name: "Request Logs" }));
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("active");
});
});
describe("auth-not-ready guard", () => {
it("shows a loading spinner when credentials are not yet resolved", () => {
useAuthorizedMock.mockReturnValue({ userRole: "Admin" });
renderWithProviders(<SpendLogsTable {...defaultProps} accessToken={null} />);
expect(document.querySelector(".ant-spin")).toBeInTheDocument();
@ -66,7 +137,7 @@ describe("SpendLogsTable", () => {
});
it("renders the tabs (no spinner) once all credentials are present", () => {
renderWithProviders(<SpendLogsTable {...defaultProps} />);
renderAs("Admin");
expect(document.querySelector(".ant-spin")).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument();

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import useCan from "@/app/(dashboard)/hooks/useCan";
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
import AuditLogsPanel from "./AuditLogsPanel";
@ -14,8 +15,22 @@ interface SpendLogsTableProps {
premiumUser: boolean;
}
type LogsTabId = "request logs" | "audit logs" | "deleted keys" | "deleted teams";
interface LogsTab {
id: LogsTabId;
label: string;
}
const REQUEST_LOGS_TAB: LogsTab = { id: "request logs", label: "Request Logs" };
const AUDIT_LOGS_TAB: LogsTab = { id: "audit logs", label: "Audit Logs" };
const DELETED_KEYS_TAB: LogsTab = { id: "deleted keys", label: "Deleted Keys" };
const DELETED_TEAMS_TAB: LogsTab = { id: "deleted teams", label: "Deleted Teams" };
export default function SpendLogsTable({ accessToken, token, userRole, userID, premiumUser }: SpendLogsTableProps) {
const [activeTab, setActiveTab] = useState("request logs");
const [activeTab, setActiveTab] = useState<LogsTabId>(REQUEST_LOGS_TAB.id);
const canViewAuditLogs = useCan("viewAuditLogs");
const canViewDeletedTeams = useCan("viewDeletedTeams");
if (!accessToken || !token || !userRole || !userID) {
return (
@ -25,41 +40,55 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
);
}
const tabs: LogsTab[] = [
REQUEST_LOGS_TAB,
...(canViewAuditLogs ? [AUDIT_LOGS_TAB] : []),
DELETED_KEYS_TAB,
...(canViewDeletedTeams ? [DELETED_TEAMS_TAB] : []),
];
const renderPanel = (tabId: LogsTabId) => {
switch (tabId) {
case "request logs":
return (
<RequestLogsPanel
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userID}
isActive={activeTab === "request logs"}
/>
);
case "audit logs":
return (
<AuditLogsPanel
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
isActive={activeTab === "audit logs"}
premiumUser={premiumUser}
/>
);
case "deleted keys":
return <DeletedKeysPage />;
case "deleted teams":
return <DeletedTeamsPage />;
}
};
return (
<div className="w-full p-6 overflow-x-hidden box-border">
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "request logs" : "audit logs")}>
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(tabs[index].id)}>
<TabList>
<Tab>Request Logs</Tab>
<Tab>Audit Logs</Tab>
<Tab>Deleted Keys</Tab>
<Tab>Deleted Teams</Tab>
{tabs.map((tab) => (
<Tab key={tab.id}>{tab.label}</Tab>
))}
</TabList>
<TabPanels>
<TabPanel>
<RequestLogsPanel
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userID}
isActive={activeTab === "request logs"}
/>
</TabPanel>
<TabPanel>
<AuditLogsPanel
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
isActive={activeTab === "audit logs"}
premiumUser={premiumUser}
/>
</TabPanel>
<TabPanel>
<DeletedKeysPage />
</TabPanel>
<TabPanel>
<DeletedTeamsPage />
</TabPanel>
{tabs.map((tab) => (
<TabPanel key={tab.id}>{renderPanel(tab.id)}</TabPanel>
))}
</TabPanels>
</TabGroup>
</div>

View file

@ -18,6 +18,19 @@ describe("hasCapability", () => {
);
});
describe.each(["viewAuditLogs", "viewDeletedTeams"] as const)("hasCapability - %s", (capability) => {
it.each(["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer"])("should grant it to %s", (role) => {
expect(hasCapability(role, capability)).toBe(true);
});
it.each(["Internal User", "Internal Viewer", "App User", "Org Admin", "Unknown Role", "", null, undefined])(
"should deny it to %s",
(role) => {
expect(hasCapability(role, capability)).toBe(false);
},
);
});
describe("rolesWithCapability", () => {
it("should return a copy so callers cannot mutate the capability map", () => {
const roles = rolesWithCapability("viewToolPolicies");

View file

@ -2,6 +2,8 @@ import { all_admin_roles } from "./roles";
const CAPABILITY_ROLES = {
viewToolPolicies: all_admin_roles,
viewAuditLogs: all_admin_roles,
viewDeletedTeams: all_admin_roles,
} as const satisfies Record<string, readonly string[]>;
export type Capability = keyof typeof CAPABILITY_ROLES;