mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Restrict /global/spend/report to proxy admin and proxy admin viewer
- Move route out of global_spend_tracking_routes; add global_spend_report_routes - Allow proxy_admin_viewer via route_checks; extend default JWT admin_allowed_routes - Enforce roles in get_global_spend_report handler for defense in depth - Add route_checks tests for viewer access and internal user denial Made-with: Cursor
This commit is contained in:
parent
2c786ca2e6
commit
00e45f2cc0
4 changed files with 77 additions and 2 deletions
|
|
@ -586,12 +586,15 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/global/spend/end_users",
|
||||
"/global/spend/models",
|
||||
"/global/predict/spend/logs",
|
||||
"/global/spend/report",
|
||||
"/global/spend/provider",
|
||||
"/global/spend/tags",
|
||||
"/global/spend/all_tag_names",
|
||||
]
|
||||
|
||||
global_spend_report_routes = [
|
||||
"/global/spend/report",
|
||||
]
|
||||
|
||||
public_routes = set(
|
||||
[
|
||||
"/routes",
|
||||
|
|
@ -4191,6 +4194,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase):
|
|||
"management_routes",
|
||||
"spend_tracking_routes",
|
||||
"global_spend_tracking_routes",
|
||||
"global_spend_report_routes",
|
||||
"info_routes",
|
||||
]
|
||||
team_id_jwt_field: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -665,6 +665,11 @@ class RouteChecks:
|
|||
# Allow access to global spend tracking routes (read-only spend endpoints)
|
||||
# proxy_admin_viewer role description: "view all keys, view all spend"
|
||||
return
|
||||
elif RouteChecks.check_route_access(
|
||||
route=route, allowed_routes=LiteLLMRoutes.global_spend_report_routes.value
|
||||
):
|
||||
# Global spend report: proxy_admin + proxy_admin_viewer only (not org admin)
|
||||
return
|
||||
else:
|
||||
# For other routes, block access
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,18 @@ else:
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
def _require_proxy_admin_or_viewer(user_api_key_dict: UserAPIKeyAuth) -> None:
|
||||
if user_api_key_dict.user_role in (
|
||||
LitellmUserRoles.PROXY_ADMIN,
|
||||
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
|
||||
):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={"error": CommonProxyErrors.not_allowed_access.value},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/spend/keys",
|
||||
tags=["Budget & Spend Tracking"],
|
||||
|
|
@ -945,12 +957,12 @@ async def get_global_spend_provider(
|
|||
@router.get(
|
||||
"/global/spend/report",
|
||||
tags=["Budget & Spend Tracking"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
responses={
|
||||
200: {"model": List[LiteLLM_SpendLogs]},
|
||||
},
|
||||
)
|
||||
async def get_global_spend_report(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
start_date: Optional[str] = fastapi.Query(
|
||||
default=None,
|
||||
description="Time from which to start viewing spend",
|
||||
|
|
@ -1009,6 +1021,8 @@ async def get_global_spend_report(
|
|||
]
|
||||
}
|
||||
"""
|
||||
_require_proxy_admin_or_viewer(user_api_key_dict)
|
||||
|
||||
if start_date is None or end_date is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
|
|||
|
|
@ -1004,6 +1004,58 @@ def test_proxy_admin_viewer_can_access_global_spend_tags():
|
|||
)
|
||||
|
||||
|
||||
def test_proxy_admin_viewer_can_access_global_spend_report():
|
||||
"""proxy_admin_viewer should pass route checks for /global/spend/report."""
|
||||
|
||||
user_obj = LiteLLM_UserTable(
|
||||
user_id="viewer_user",
|
||||
user_email="viewer@example.com",
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value,
|
||||
)
|
||||
valid_token = UserAPIKeyAuth(
|
||||
user_id="viewer_user",
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value,
|
||||
)
|
||||
request = MagicMock(spec=Request)
|
||||
request.query_params = {"start_date": "2025-05-12", "end_date": "2025-10-09"}
|
||||
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value,
|
||||
route="/global/spend/report",
|
||||
request=request,
|
||||
valid_token=valid_token,
|
||||
request_data={},
|
||||
)
|
||||
|
||||
|
||||
def test_internal_user_cannot_access_global_spend_report_route():
|
||||
"""Internal users must not use /global/spend/report (proxy admin roles only)."""
|
||||
|
||||
user_obj = LiteLLM_UserTable(
|
||||
user_id="internal_user",
|
||||
user_email="user@example.com",
|
||||
user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
)
|
||||
valid_token = UserAPIKeyAuth(
|
||||
user_id="internal_user",
|
||||
user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
)
|
||||
request = MagicMock(spec=Request)
|
||||
request.query_params = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
route="/global/spend/report",
|
||||
request=request,
|
||||
valid_token=valid_token,
|
||||
request_data={},
|
||||
)
|
||||
assert "Only proxy admin can be used to generate" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("route", ["/audit", "/audit/some-log-id"])
|
||||
def test_proxy_admin_viewer_can_access_audit_logs(route):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue