[Fix] RBAC: Unblock Guardrails / Policies / MCP-filter reads + Keys / Models page hard-blocks

User reported six more 403s and "still restricts access to keys + models" after
the first round. Root causes:

1. Six read endpoints were missing from admin_viewer_routes:
   - /guardrails/list, /v2/guardrails/list (Guardrails page)
   - /guardrails/submissions, /guardrails/submissions/{guardrail_id}
   - /guardrails/usage/overview (Guardrails Monitor page)
   - /policies/attachments/list (Policies page)
   - /get/mcp_semantic_filter_settings (Settings page)

2. /guardrails/submissions handler treated admin viewer as non-admin, filtering
   them to only their team submissions. Switch to _user_has_admin_view() so
   admin viewer sees all submissions (read parity with Proxy Admin).

3. UI Keys page (user_dashboard.tsx) and Models page (ModelsAndEndpointsView.tsx)
   each had a hard "Access Denied" block specifically for "Admin Viewer" — a
   leftover from the pre-parity era. Remove the blocks; gate the "Create Key"
   button on the Keys page so admin viewer can read keys but not mint them.
   Also drop the post-login redirect that forced admin viewers to /usage on
   sign-in (page.tsx).

Tests:
- Extend ADMIN_VIEWER_SETTINGS_ROUTES parametrize list to cover all 7 new
  routes (route-checks layer is now the layer production traffic actually
  hits, vs. the dependency-override-bypass that was masking the gap).
This commit is contained in:
Yuneng Jiang
2026-04-29 22:58:38 -07:00
parent f81fbdabe6
commit 2fa6c60124
6 changed files with 42 additions and 33 deletions
+9
View File
@@ -758,6 +758,15 @@ class LiteLLMRoutes(enum.Enum):
"/budget/settings",
# Invitation viewing (admin viewer cannot create/delete; can read).
"/invitation/info",
# Guardrails / Policies pages (read-only views).
"/guardrails/list",
"/v2/guardrails/list",
"/guardrails/submissions",
"/guardrails/submissions/{guardrail_id}",
"/guardrails/usage/overview",
"/policies/attachments/list",
# MCP semantic filter settings (read).
"/get/mcp_semantic_filter_settings",
# Model cost map maintenance views (read-only status / source).
"/schedule/model_cost_map_reload/status",
"/model/cost_map/source",
@@ -21,6 +21,7 @@ from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
from litellm.proxy.guardrails.guardrail_hooks.custom_code.sandbox import (
build_sandbox_globals,
compile_sandboxed,
@@ -842,7 +843,10 @@ async def list_guardrail_submissions(
if prisma_client is None:
raise HTTPException(status_code=500, detail="Prisma client not initialized")
is_admin = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
# Admin Viewer follows the read-parity rule: see all submissions like a
# Proxy Admin would (no writes — registration / approval still gated
# elsewhere by their own per-action checks).
is_admin = _user_has_admin_view(user_api_key_dict)
visible_team_ids: Optional[List[str]] = None
if not is_admin:
visible_team_ids = await _get_user_team_ids(user_api_key_dict)
@@ -1325,6 +1325,15 @@ ADMIN_VIEWER_SETTINGS_ROUTES = [
"/budget/settings",
# Invitation viewing (admin viewer cannot create/delete; can read)
"/invitation/info",
# Guardrails / Policies pages (read-only views)
"/guardrails/list",
"/v2/guardrails/list",
"/guardrails/submissions",
"/guardrails/submissions/some-guardrail-id",
"/guardrails/usage/overview",
"/policies/attachments/list",
# MCP semantic filter settings (read)
"/get/mcp_semantic_filter_settings",
# Model cost map (read-only status / source)
"/schedule/model_cost_map_reload/status",
"/model/cost_map/source",
@@ -17,7 +17,7 @@ import { RefreshIcon } from "@heroicons/react/outline";
import { useQueryClient } from "@tanstack/react-query";
import { Col, Grid, Icon, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import type { UploadProps } from "antd";
import { Form, Typography } from "antd";
import { Form } from "antd";
import { PlusCircleOutlined } from "@ant-design/icons";
import React, { useEffect, useMemo, useState } from "react";
import AddModelTab from "../../../components/add_model/add_model_tab";
@@ -229,15 +229,9 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
const isLoading = isLoadingModels || isLoadingModelCostMap || isLoadingCredentials || isLoadingUISettings;
if (userRole && userRole == "Admin Viewer") {
const { Title, Paragraph } = Typography;
return (
<div>
<Title level={1}>Access Denied</Title>
<Paragraph>Ask your proxy admin for access to view all models</Paragraph>
</div>
);
}
// Admin Viewer can view all models read-only — page render proceeds; the
// individual write-action tabs (Add Model, LLM Credentials, etc.) are
// gated separately below.
const handleOk = async () => {
try {
-3
View File
@@ -325,9 +325,6 @@ function CreateKeyPageContent() {
if (decoded.user_role) {
const formattedUserRole = formatUserRole(decoded.user_role);
setUserRole(formattedUserRole);
if (formattedUserRole == "Admin Viewer") {
setPage("usage");
}
}
if (decoded.user_email) {
@@ -1,7 +1,6 @@
"use client";
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
import { Col, Grid } from "@tremor/react";
import { Typography } from "antd";
import { jwtDecode } from "jwt-decode";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
@@ -317,15 +316,10 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
setUserRole("App Owner");
}
if (userRole && userRole == "Admin Viewer") {
const { Title, Paragraph } = Typography;
return (
<div>
<Title level={1}>Access Denied</Title>
<Paragraph>Ask your proxy admin for access to create keys</Paragraph>
</div>
);
}
// Admin Viewer can view keys read-only — gate "Create Key" but render the
// virtual-keys table the same as for Proxy Admin (read parity). Every
// other role keeps its existing ability to create keys.
const canCreateKey = userRole !== "Admin Viewer" && userRole !== "proxy_admin_viewer";
console.log("inside user dashboard, selected team", selectedTeam);
console.log("All cookies after redirect:", document.cookie);
@@ -333,15 +327,17 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
<div className="w-full mx-4 h-[75vh]">
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
<Col numColSpan={1} className="flex flex-col gap-2">
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
team={selectedTeam as Team | null}
teams={teams as Team[]}
data={keys}
addKey={addKey}
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
{canCreateKey && (
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
team={selectedTeam as Team | null}
teams={teams as Team[]}
data={keys}
addKey={addKey}
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
)}
<VirtualKeysTable teams={teams} organizations={organizations} />
</Col>
</Grid>