mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-22 22:24:10 +00:00
[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:
@@ -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",
|
||||
|
||||
+4
-10
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user