[Fix] RBAC: Open Handler-Level Admin Reads + Hide UI Write Buttons for Admin Viewer
The default-allow-GET fix in route_checks unblocked the route layer, but a
second class of bug remained: handlers that gate on `user_role !=
PROXY_ADMIN` (or a private `_require_proxy_admin` helper) reject admin
viewer at the handler before the route's HTTP method even matters.
Backend: relax handler role checks on read endpoints to allow
PROXY_ADMIN_VIEW_ONLY (same `_user_has_admin_view` helper used elsewhere).
- /v1/access_group GET (list) + /v1/access_group/{id} GET — split
`_require_proxy_admin` into a parallel `_require_admin_view` for the
two read handlers; writes (POST / PUT / DELETE) keep the strict gate.
- /cloudzero/settings GET, /vantage/settings GET — read-only views.
- /config_overrides/hashicorp_vault GET — read-only config view.
- /team/permissions_list GET — let admin viewer see permissions like
a Proxy Admin would.
- /jwt/key/mapping/list, /jwt/key/mapping/info — JWT mapping reads.
- /v1/mcp/discover, /v1/mcp/openapi-registry — MCP picker views.
- /schedule/anthropic_beta_headers_reload/status — read-only status.
- /adaptive_router/state — read-only live snapshot.
UI: hide write buttons that admin viewer should not see (button click
would fail the backend write gate, but the UX expectation is no button).
- Internal Users: hide "Invite User" button.
- Access Groups: hide "Create Access Group" + Delete row action.
- Budgets: hide "+ Create Budget" + Edit/Delete row actions.
- Prompts: hide "+ Add New Prompt" / "Upload .prompt File"; gate the
prompt-table Edit/Delete actions on `isProxyAdminRole` (was
`isAdminRole` which incorrectly included admin viewer).
- Router Settings → Fallbacks: hide AddFallbacks panel + per-row Test
+ Delete actions.
- AI Hub: hide "Select Models / Agents / MCP Servers / Skills to Make
Public" + "Useful Links Management" (writes).
These pages remain VISIBLE for admin viewer (read parity); only the
write entry points are hidden.
This commit is contained in:
parent
00145f91a8
commit
0423f51e2a
@ -38,6 +38,17 @@ def _require_proxy_admin(user_api_key_dict: UserAPIKeyAuth) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _require_admin_view(user_api_key_dict: UserAPIKeyAuth) -> None:
|
||||
"""Admin Viewer parity: PROXY_ADMIN or PROXY_ADMIN_VIEW_ONLY may read."""
|
||||
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
||||
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={"error": CommonProxyErrors.not_allowed_access.value},
|
||||
)
|
||||
|
||||
|
||||
def _record_to_response(record) -> AccessGroupResponse:
|
||||
return AccessGroupResponse(
|
||||
access_group_id=record.access_group_id,
|
||||
@ -372,7 +383,7 @@ async def create_access_group(
|
||||
async def list_access_groups(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> List[AccessGroupResponse]:
|
||||
_require_proxy_admin(user_api_key_dict)
|
||||
_require_admin_view(user_api_key_dict)
|
||||
prisma_client = get_prisma_client_or_throw(
|
||||
CommonProxyErrors.db_not_connected_error.value
|
||||
)
|
||||
@ -391,7 +402,7 @@ async def get_access_group(
|
||||
access_group_id: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> AccessGroupResponse:
|
||||
_require_proxy_admin(user_api_key_dict)
|
||||
_require_admin_view(user_api_key_dict)
|
||||
prisma_client = get_prisma_client_or_throw(
|
||||
CommonProxyErrors.db_not_connected_error.value
|
||||
)
|
||||
|
||||
@ -267,9 +267,11 @@ async def get_hashicorp_vault_config(
|
||||
Get current Hashicorp Vault configuration.
|
||||
Returns decrypted values from DB, or falls back to current env vars.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
||||
from litellm.proxy.proxy_server import prisma_client, proxy_config
|
||||
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Only admin users can view config overrides",
|
||||
|
||||
@ -10,6 +10,7 @@ from litellm.proxy._types import (
|
||||
hash_token,
|
||||
)
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -194,7 +195,8 @@ async def list_jwt_key_mappings(
|
||||
):
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Only proxy admins can list JWT key mappings"
|
||||
)
|
||||
@ -233,7 +235,8 @@ async def info_jwt_key_mapping(
|
||||
):
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Only proxy admins can get JWT key mapping info"
|
||||
)
|
||||
|
||||
@ -2119,7 +2119,8 @@ if MCP_AVAILABLE:
|
||||
|
||||
Used by the UI to show a discovery grid when adding new MCP servers.
|
||||
"""
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
@ -2176,7 +2177,8 @@ if MCP_AVAILABLE:
|
||||
async def get_openapi_registry(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
|
||||
@ -4623,9 +4623,11 @@ async def team_member_permissions(
|
||||
|
||||
complete_team_data = LiteLLM_TeamTable(**existing_team_row.model_dump())
|
||||
|
||||
# Admin Viewer follows the read-parity rule: see team permissions like
|
||||
# a Proxy Admin would. Team / org admins keep their existing scope.
|
||||
if (
|
||||
hasattr(user_api_key_dict, "user_role")
|
||||
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
|
||||
and not _user_has_admin_view(user_api_key_dict)
|
||||
and not _is_user_team_admin(
|
||||
user_api_key_dict=user_api_key_dict, team_obj=complete_team_data
|
||||
)
|
||||
|
||||
@ -14058,8 +14058,8 @@ async def get_anthropic_beta_headers_reload_status(
|
||||
|
||||
Get the status of the scheduled Anthropic beta headers reload job.
|
||||
"""
|
||||
# Check if user is admin
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Read-only status — admin viewers can read.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Access denied. Admin role required. Current role: {user_api_key_dict.user_role}",
|
||||
@ -14165,7 +14165,8 @@ async def get_adaptive_router_state(
|
||||
adaptive-router deployment. Each snapshot's `router_name` field identifies
|
||||
which deployment it came from.
|
||||
"""
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Read-only state — admin viewers can read.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={"error": CommonProxyErrors.not_allowed_access.value},
|
||||
|
||||
@ -6,6 +6,7 @@ from litellm._logging import verbose_proxy_logger
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
|
||||
from litellm.proxy._types import CommonProxyErrors, 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.common_utils.encrypt_decrypt_utils import (
|
||||
decrypt_value_helper,
|
||||
encrypt_value_helper,
|
||||
@ -127,10 +128,10 @@ async def get_cloudzero_settings(
|
||||
Only the first 4 and last 4 characters of the API key are shown.
|
||||
Returns null/empty values when settings are not configured (consistent with other settings endpoints).
|
||||
|
||||
Only admin users can view CloudZero settings.
|
||||
Only admin users (Proxy Admin or Admin Viewer) can view CloudZero settings.
|
||||
"""
|
||||
# Validation
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Validation — Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={"error": CommonProxyErrors.not_allowed_access.value},
|
||||
|
||||
@ -7,6 +7,7 @@ from litellm._logging import verbose_proxy_logger
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
|
||||
from litellm.proxy._types import CommonProxyErrors, 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.common_utils.encrypt_decrypt_utils import (
|
||||
decrypt_value_helper,
|
||||
encrypt_value_helper,
|
||||
@ -140,9 +141,10 @@ async def get_vantage_settings(
|
||||
View current Vantage settings.
|
||||
|
||||
Returns the current Vantage configuration with the API key masked for security.
|
||||
Only admin users can view Vantage settings.
|
||||
Only admin users (Proxy Admin or Admin Viewer) can view Vantage settings.
|
||||
"""
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
# Admin Viewer follows the read-parity rule.
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={"error": CommonProxyErrors.not_allowed_access.value},
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
modelHubPublicModelsCall,
|
||||
} from "@/components/networking";
|
||||
import PublicModelHub from "@/components/public_model_hub";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import { Badge, Button, Card, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
|
||||
import { Modal } from "antd";
|
||||
@ -61,6 +61,10 @@ interface ModelGroupInfo {
|
||||
}
|
||||
|
||||
const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage, premiumUser, userRole }) => {
|
||||
// Admin Viewer follows the read-parity rule: see the AI Hub catalog, but
|
||||
// cannot toggle public visibility (write).
|
||||
const canModify = isProxyAdminRole(userRole || "");
|
||||
|
||||
const [publicPageAllowed, setPublicPageAllowed] = useState<boolean>(false);
|
||||
const [modelHubData, setModelHubData] = useState<ModelGroupInfo[] | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
@ -420,7 +424,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
||||
</div>
|
||||
|
||||
{/* Useful Links Management Section for Admins */}
|
||||
{isAdminRole(userRole || "") && (
|
||||
{canModify && (
|
||||
<div className="mt-8 mb-2">
|
||||
<UsefulLinksManagement accessToken={accessToken} userRole={userRole} />
|
||||
</div>
|
||||
@ -441,7 +445,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
||||
{/* Model Filters and Table */}
|
||||
<Card>
|
||||
{/* Header with Make Public Button */}
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
{publicPage == false && canModify && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={() => handleMakePublicPage()}>Select Models to Make Public</Button>
|
||||
</div>
|
||||
@ -470,7 +474,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
||||
<TabPanel>
|
||||
<Card>
|
||||
{/* Header with Make Public Button */}
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
{publicPage == false && canModify && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={() => handleMakeAgentPublicPage()}>Select Agents to Make Public</Button>
|
||||
</div>
|
||||
@ -496,7 +500,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
||||
<TabPanel>
|
||||
<Card>
|
||||
{/* Header with Make Public Button */}
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
{publicPage == false && canModify && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={() => handleMakeMcpPublicPage()}>Select MCP Servers to Make Public</Button>
|
||||
</div>
|
||||
@ -520,7 +524,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
||||
|
||||
{/* Skill Hub Tab */}
|
||||
<TabPanel>
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
{publicPage == false && canModify && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={() => setIsMakeSkillPublicModalVisible(true)}>
|
||||
Select Skills to Make Public
|
||||
@ -530,7 +534,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
||||
<SkillHubDashboard
|
||||
skills={skillHubData}
|
||||
isLoading={skillLoading}
|
||||
isAdmin={isAdminRole(userRole || "")}
|
||||
isAdmin={canModify}
|
||||
accessToken={accessToken}
|
||||
publicPage={publicPage}
|
||||
onPublishSuccess={async () => {
|
||||
|
||||
@ -43,6 +43,8 @@ import {
|
||||
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
|
||||
import { AccessGroupCreateModal } from "./AccessGroupsModal/AccessGroupCreateModal";
|
||||
import { AccessGroup } from "./types";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -130,6 +132,9 @@ function buildAntdColumns(
|
||||
|
||||
export function AccessGroupsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const { userRole } = useAuthorized();
|
||||
// Admin Viewer follows the read-parity rule: see access groups, no writes.
|
||||
const canModify = isProxyAdminRole(userRole ?? "");
|
||||
const { data: groupsData, isLoading } = useAccessGroups();
|
||||
const groups = useMemo(
|
||||
() => (groupsData ?? []).map(mapResponseToAccessGroup),
|
||||
@ -251,24 +256,28 @@ export function AccessGroupsPage() {
|
||||
new Date(getValue() as string).toLocaleDateString(),
|
||||
meta: { responsive: ["xl"] },
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span>Actions</span>,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<Space>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete access group"
|
||||
onClick={() => setGroupToDelete(row.original)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
...(canModify
|
||||
? [
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span>Actions</span>,
|
||||
enableSorting: false,
|
||||
cell: ({ row }: { row: Row<AccessGroup> }) => (
|
||||
<Space>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete access group"
|
||||
onClick={() => setGroupToDelete(row.original)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
// setSelectedGroup is stable (useState setter)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[canModify],
|
||||
);
|
||||
|
||||
// ---------- TanStack table instance ----------
|
||||
@ -329,13 +338,15 @@ export function AccessGroupsPage() {
|
||||
Manage resource permissions for your organization
|
||||
</Text>
|
||||
</Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsCreateModalVisible(true)}
|
||||
>
|
||||
Create Access Group
|
||||
</Button>
|
||||
{canModify && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsCreateModalVisible(true)}
|
||||
>
|
||||
Create Access Group
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Card styles={{ body: { padding: 0 } }}>
|
||||
|
||||
@ -8,6 +8,7 @@ import DeleteResourceModal from "../../../common_components/DeleteResourceModal"
|
||||
import { ProviderLogo } from "../../../molecules/models/ProviderLogo";
|
||||
import NotificationsManager from "../../../molecules/notifications_manager";
|
||||
import { getCallbacksCall, setCallbacksCall } from "../../../networking";
|
||||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
import AddFallbacks from "./AddFallbacks";
|
||||
|
||||
type FallbackEntry = { [modelName: string]: string[] };
|
||||
@ -243,15 +244,19 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
||||
};
|
||||
|
||||
const hasFallbacks = Array.isArray(routerSettings.fallbacks) && routerSettings.fallbacks.length > 0;
|
||||
// Admin Viewer follows the read-parity rule: see fallbacks, no writes.
|
||||
const canModify = isProxyAdminRole(userRole ?? "");
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddFallbacks
|
||||
models={modelData?.data ? modelData.data.map((data: any) => data.model_name) : []}
|
||||
accessToken={accessToken || ""}
|
||||
value={routerSettings.fallbacks || []}
|
||||
onChange={handleFallbacksChange}
|
||||
/>
|
||||
{canModify && (
|
||||
<AddFallbacks
|
||||
models={modelData?.data ? modelData.data.map((data: any) => data.model_name) : []}
|
||||
accessToken={accessToken || ""}
|
||||
value={routerSettings.fallbacks || []}
|
||||
onChange={handleFallbacksChange}
|
||||
/>
|
||||
)}
|
||||
{!hasFallbacks ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-6 text-center">
|
||||
<Typography.Text type="secondary">
|
||||
@ -280,30 +285,34 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
||||
{renderFallbacksChain(key, Array.isArray(value) ? value : [], getProviderFromModel)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<Tooltip title="Test fallback">
|
||||
<Icon
|
||||
icon={PlayIcon}
|
||||
size="sm"
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
|
||||
className="cursor-pointer hover:text-blue-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete fallback">
|
||||
<span
|
||||
data-testid="delete-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleDeleteClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
|
||||
className="cursor-pointer inline-flex"
|
||||
>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
className="hover:text-red-600"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{canModify && (
|
||||
<>
|
||||
<Tooltip title="Test fallback">
|
||||
<Icon
|
||||
icon={PlayIcon}
|
||||
size="sm"
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
|
||||
className="cursor-pointer hover:text-blue-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete fallback">
|
||||
<span
|
||||
data-testid="delete-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleDeleteClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
|
||||
className="cursor-pointer inline-flex"
|
||||
>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
className="hover:text-red-600"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
|
||||
@ -28,6 +28,8 @@ import { useBudgets, useDeleteBudget } from "@/app/(dashboard)/hooks/budgets/use
|
||||
import BudgetModal from "./budget_modal";
|
||||
import EditBudgetModal from "./edit_budget_modal";
|
||||
import { CREATE_END_USER_CURL_COMMAND, CHAT_COMPLETIONS_CURL_COMMAND, OPENAI_SDK_PYTHON_CODE } from "./constants";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
|
||||
interface BudgetSettingsPageProps {
|
||||
accessToken: string | null;
|
||||
@ -47,6 +49,10 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||
const [selectedBudget, setSelectedBudget] = useState<budgetItem | null>(null);
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
|
||||
const { userRole } = useAuthorized();
|
||||
// Admin Viewer follows the read-parity rule: see budgets, no writes.
|
||||
const canModify = isProxyAdminRole(userRole ?? "");
|
||||
|
||||
const { data: budgetList = [] } = useBudgets();
|
||||
const deleteBudget = useDeleteBudget();
|
||||
|
||||
@ -89,9 +95,11 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<Button size="sm" variant="primary" className="mb-2" onClick={() => setIsCreateModelVisible(true)}>
|
||||
+ Create Budget
|
||||
</Button>
|
||||
{canModify && (
|
||||
<Button size="sm" variant="primary" className="mb-2" onClick={() => setIsCreateModelVisible(true)}>
|
||||
+ Create Budget
|
||||
</Button>
|
||||
)}
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>Budgets</Tab>
|
||||
@ -133,18 +141,22 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||
<TableCell>{value.max_budget ? value.max_budget : "n/a"}</TableCell>
|
||||
<TableCell>{value.tpm_limit ? value.tpm_limit : "n/a"}</TableCell>
|
||||
<TableCell>{value.rpm_limit ? value.rpm_limit : "n/a"}</TableCell>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit budget"
|
||||
onClick={() => handleEditCall(value)}
|
||||
dataTestId="edit-budget-button"
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete budget"
|
||||
onClick={() => handleDeleteClick(value)}
|
||||
dataTestId="delete-budget-button"
|
||||
/>
|
||||
{canModify && (
|
||||
<>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit budget"
|
||||
onClick={() => handleEditCall(value)}
|
||||
dataTestId="edit-budget-button"
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete budget"
|
||||
onClick={() => handleDeleteClick(value)}
|
||||
dataTestId="delete-budget-button"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@ -8,7 +8,7 @@ import PromptInfoView from "./prompts/prompt_info";
|
||||
import AddPromptForm from "./prompts/add_prompt_form";
|
||||
import PromptEditorView from "./prompts/prompt_editor_view";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
|
||||
|
||||
interface PromptsProps {
|
||||
accessToken: string | null;
|
||||
@ -27,6 +27,8 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
const isAdmin = userRole ? isAdminRole(userRole) : false;
|
||||
// Admin Viewer follows the read-parity rule: see prompts, no writes.
|
||||
const canModify = userRole ? isProxyAdminRole(userRole) : false;
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
if (!accessToken) {
|
||||
@ -128,7 +130,7 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
promptId={selectedPromptId}
|
||||
onClose={() => setSelectedPromptId(null)}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
isAdmin={canModify}
|
||||
onDelete={fetchPrompts}
|
||||
onEdit={handleEditPrompt}
|
||||
/>
|
||||
@ -136,12 +138,16 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleAddPrompt} disabled={!accessToken}>
|
||||
+ Add New Prompt
|
||||
</Button>
|
||||
<Button onClick={handleAddPromptFromFile} disabled={!accessToken} variant="secondary">
|
||||
Upload .prompt File
|
||||
</Button>
|
||||
{canModify && (
|
||||
<>
|
||||
<Button onClick={handleAddPrompt} disabled={!accessToken}>
|
||||
+ Add New Prompt
|
||||
</Button>
|
||||
<Button onClick={handleAddPromptFromFile} disabled={!accessToken} variant="secondary">
|
||||
Upload .prompt File
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
placeholder="All Environments"
|
||||
@ -163,7 +169,7 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
onPromptClick={handlePromptClick}
|
||||
onDeleteClick={handleDeleteClick}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
isAdmin={canModify}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -304,7 +304,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
||||
</>
|
||||
) : userID && accessToken ? (
|
||||
<>
|
||||
<CreateUserButton userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
|
||||
{isProxyAdmin && (
|
||||
<CreateUserButton userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
|
||||
)}
|
||||
|
||||
{isProxyAdmin && (
|
||||
<Button
|
||||
|
||||
Loading…
Reference in New Issue
Block a user