From 2fa6c601249bc96dc24a5436c6e00c99aec1df37 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 29 Apr 2026 22:58:38 -0700 Subject: [PATCH] [Fix] RBAC: Unblock Guardrails / Policies / MCP-filter reads + Keys / Models page hard-blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- litellm/proxy/_types.py | 9 +++++ .../proxy/guardrails/guardrail_endpoints.py | 6 +++- .../proxy/auth/test_route_checks.py | 9 +++++ .../ModelsAndEndpointsView.tsx | 14 +++----- ui/litellm-dashboard/src/app/page.tsx | 3 -- .../src/components/user_dashboard.tsx | 34 ++++++++----------- 6 files changed, 42 insertions(+), 33 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 2ce8d82c83..a428c42d68 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -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", diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index ac487fb06d..5351391e5e 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -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) diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index f85384bcb7..a1613aad94 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -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", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 514ae673d0..8f248dacc9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -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 = ({ premiumUser, te const isLoading = isLoadingModels || isLoadingModelCostMap || isLoadingCredentials || isLoadingUISettings; - if (userRole && userRole == "Admin Viewer") { - const { Title, Paragraph } = Typography; - return ( -
- Access Denied - Ask your proxy admin for access to view all models -
- ); - } + // 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 { diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index a8553d5405..06bf3b68d0 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -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) { diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index 90eac56540..db262bc84a 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -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 = ({ setUserRole("App Owner"); } - if (userRole && userRole == "Admin Viewer") { - const { Title, Paragraph } = Typography; - return ( -
- Access Denied - Ask your proxy admin for access to create keys -
- ); - } + // 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 = ({
- + {canCreateKey && ( + + )}