Allow /global/spend/report when virtual key allowlists the route

- Replace admin-only guard with _require_global_spend_report_access: proxy
  admin roles unchanged; others pass if allowed_routes matches via RouteChecks
- Tests for internal user with path or global_spend_report_routes bundle

Made-with: Cursor
This commit is contained in:
shivam 2026-04-11 18:36:27 -07:00
parent 00e45f2cc0
commit 5baf972a96
No known key found for this signature in database
3 changed files with 77 additions and 3 deletions

View file

@ -12,6 +12,7 @@ import litellm
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import *
from litellm.proxy._types import ProviderBudgetResponse, ProviderBudgetResponseObject
from litellm.proxy.auth.route_checks import RouteChecks
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
# NOTE: Avoid module-level import from common_utils: proxy_server imports this
@ -31,12 +32,28 @@ else:
router = APIRouter()
def _require_proxy_admin_or_viewer(user_api_key_dict: UserAPIKeyAuth) -> None:
_GLOBAL_SPEND_REPORT_ROUTE = "/global/spend/report"
def _require_global_spend_report_access(user_api_key_dict: UserAPIKeyAuth) -> None:
"""
Allow proxy admin roles, or any role when the virtual key allowlists this route
(same matching rules as RouteChecks.is_virtual_key_allowed_to_call_route).
"""
if user_api_key_dict.user_role in (
LitellmUserRoles.PROXY_ADMIN,
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
):
return
if user_api_key_dict.allowed_routes:
try:
RouteChecks.is_virtual_key_allowed_to_call_route(
route=_GLOBAL_SPEND_REPORT_ROUTE,
valid_token=user_api_key_dict,
)
return
except HTTPException:
pass
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"error": CommonProxyErrors.not_allowed_access.value},
@ -1021,7 +1038,7 @@ async def get_global_spend_report(
]
}
"""
_require_proxy_admin_or_viewer(user_api_key_dict)
_require_global_spend_report_access(user_api_key_dict)
if start_date is None or end_date is None:
raise HTTPException(

View file

@ -1030,7 +1030,7 @@ def test_proxy_admin_viewer_can_access_global_spend_report():
def test_internal_user_cannot_access_global_spend_report_route():
"""Internal users must not use /global/spend/report (proxy admin roles only)."""
"""Without key allowlisting, internal users cannot use /global/spend/report."""
user_obj = LiteLLM_UserTable(
user_id="internal_user",
@ -1056,6 +1056,32 @@ def test_internal_user_cannot_access_global_spend_report_route():
assert "Only proxy admin can be used to generate" in str(exc_info.value)
def test_internal_user_can_access_global_spend_report_when_key_allowlists_route():
"""Virtual key with allowed_routes including /global/spend/report passes route check."""
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,
allowed_routes=["/global/spend/report"],
)
request = MagicMock(spec=Request)
request.query_params = {}
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={},
)
@pytest.mark.parametrize("route", ["/audit", "/audit/some-log-id"])
def test_proxy_admin_viewer_can_access_audit_logs(route):
"""

View file

@ -2761,3 +2761,34 @@ async def test_ui_view_spend_logs_team_member_no_permission_blocked(
assert response.status_code == 403
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
def test_require_global_spend_report_access_internal_with_allowed_routes():
spend_management_endpoints._require_global_spend_report_access(
UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="u1",
allowed_routes=["/global/spend/report"],
)
)
def test_require_global_spend_report_access_internal_with_route_bundle_name():
spend_management_endpoints._require_global_spend_report_access(
UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="u1",
allowed_routes=["global_spend_report_routes"],
)
)
def test_require_global_spend_report_access_internal_without_allowed_routes_fails():
with pytest.raises(HTTPException) as exc_info:
spend_management_endpoints._require_global_spend_report_access(
UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="u1",
)
)
assert exc_info.value.status_code == 403