fix(ui): hide the Add Provider wizard from view-only admins

effectiveSessionRole deliberately reports a proxy_admin_viewer session as "Admin",
so gating the tab on all_admin_roles showed a read-only admin a wizard whose every
step is a write the proxy then refuses. The first step solicits a provider API key,
so a viewer types a real secret into a form that 403s on submit.

Only the raw-role isViewOnly separates the two, which is the same mechanism the
Playground already uses for this carve-out. Both conjuncts of the gate are covered:
dropping either one fails a test.

The five older admin panels on this page have the same gap, but they predate this
PR and fixing them is a page-wide change, so they are left for a follow-up.
This commit is contained in:
derhornspieler 2026-08-23 20:13:54 -04:00
parent bacf9c9ad0
commit 2b1e56b877
2 changed files with 23 additions and 3 deletions

View file

@ -40,6 +40,15 @@ vi.mock("./useModelDashboardData", () => ({
const ADMIN = { accessToken: "at", token: "t", userRole: "Admin", userId: "u1", premiumUser: false };
const NON_ADMIN = { accessToken: "at", token: "t", userRole: "Internal User", userId: "u1", premiumUser: false };
// effectiveSessionRole reports a proxy_admin_viewer session as "Admin"; only isViewOnly tells them apart.
const ADMIN_VIEWER = {
accessToken: "at",
token: "t",
userRole: "Admin",
userId: "u1",
premiumUser: false,
isViewOnly: true,
};
const renderPage = () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
@ -68,9 +77,17 @@ describe("ModelsAndEndpointsPage", () => {
expect(getByRole("tab", { name: "All Models" })).toBeInTheDocument();
expect(getByRole("tab", { name: "LLM Credentials" })).toBeInTheDocument();
expect(getByRole("tab", { name: "Health Status" })).toBeInTheDocument();
expect(getByRole("tab", { name: "Add Provider" })).toBeInTheDocument();
expect(getByTestId("panel-all-models")).toBeInTheDocument();
});
it("hides the write-only Add Provider tab from a view-only admin", () => {
mockUseAuthorized.mockReturnValue(ADMIN_VIEWER);
const { getByRole, queryByRole } = renderPage();
expect(getByRole("tab", { name: "All Models" })).toBeInTheDocument();
expect(queryByRole("tab", { name: "Add Provider" })).not.toBeInTheDocument();
});
it("switches tabs in-memory, mounting only the active panel", async () => {
const user = userEvent.setup();
const { getByRole, getByTestId, queryByTestId } = renderPage();
@ -97,6 +114,7 @@ describe("ModelsAndEndpointsPage", () => {
const { queryByRole } = renderPage();
expect(queryByRole("tab", { name: "LLM Credentials" })).not.toBeInTheDocument();
expect(queryByRole("tab", { name: "Health Status" })).not.toBeInTheDocument();
expect(queryByRole("tab", { name: "Add Provider" })).not.toBeInTheDocument();
});
// Auto-routers are excluded from the All Models table, so this tab is their home: the only

View file

@ -80,7 +80,7 @@ const renderPanel = (key: string) => {
};
export default function ModelsAndEndpointsPage() {
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
const { accessToken, userRole, userId: userID, premiumUser, isViewOnly } = useAuthorized();
const { data: teams } = useTeams();
const { data: uiSettings } = useUISettings();
const queryClient = useQueryClient();
@ -106,12 +106,14 @@ export default function ModelsAndEndpointsPage() {
"",
...(canCreate ? (["add"] as const) : []),
...(isAdmin || canCreate ? (["auto-routers"] as const) : []),
...(isAdmin ? (["add-provider"] as const) : []),
// effectiveSessionRole reports proxy_admin_viewer as "Admin", so isAdmin alone would show
// a viewer this write-only wizard; only the raw-role isViewOnly separates them.
...(isAdmin && !isViewOnly ? (["add-provider"] as const) : []),
...(isAdmin
? (["llm-credentials", "pass-through", "health", "retry-settings", "model-group-alias", "price-data"] as const)
: []),
],
[canCreate, isAdmin],
[canCreate, isAdmin, isViewOnly],
);
const allModelsLabel = isAdmin ? "All Models" : "Your Models";