fix(auth): return 403 for admin-only route denials

Denying a non-admin an admin-only management route raised a bare Exception,
which the auth error handler maps to 401 and the Prometheus failure metric
records as exception_status="None". The key is valid and re-authenticating
does not help, so the correct status is 403.

Raising HTTPException with 403 fixes both symptoms at once, because the
failure hook reads status_code off the original exception. Every comparable
admin denial in the codebase already returns 403.
This commit is contained in:
Chinmayrawat15 2026-08-16 21:30:13 -05:00
parent dad4c1a0fb
commit 0344151877
2 changed files with 43 additions and 9 deletions

View file

@ -226,17 +226,14 @@ class RouteChecks:
route (str): The route being accessed
Raises:
Exception: With user role and masked user_id information
HTTPException: 403, with user role and masked user_id information
"""
user_role = "unknown"
user_id = "unknown"
if user_obj is not None:
user_role = user_obj.user_role or "unknown"
user_id = user_obj.user_id or "unknown"
user_role: Final = (user_obj.user_role if user_obj is not None else None) or "unknown"
user_id: Final = (user_obj.user_id if user_obj is not None else None) or "unknown"
masked_user_id: Final = RouteChecks._mask_user_id(user_id)
raise Exception(
f"Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route={route}. Your role={user_role}. Your user_id={masked_user_id}"
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route={route}. Your role={user_role}. Your user_id={masked_user_id}",
)
@staticmethod

View file

@ -61,6 +61,43 @@ def test_non_admin_config_update_route_rejected():
assert "Your role=internal_user" in str(exc_info.value)
@pytest.mark.parametrize(
"route",
["/policies/list", "/prompts/list", "/in_product_nudges", "/config/update"],
)
def test_admin_only_route_denial_is_forbidden_and_carries_status(route):
"""A valid non-admin key denied an admin-only route is an authorization
failure, so it must surface as 403 and carry the status code the metrics
layer reads, not an unlabelled 401.
Regression test for https://github.com/BerriAI/litellm/issues/37108
"""
user_obj = LiteLLM_UserTable(
user_id="test_user",
user_email="test@example.com",
user_role=LitellmUserRoles.INTERNAL_USER.value,
)
valid_token = UserAPIKeyAuth(
user_id="test_user",
user_role=LitellmUserRoles.INTERNAL_USER.value,
)
request = MagicMock(spec=Request)
request.query_params = {}
with pytest.raises(HTTPException) as exc_info:
RouteChecks.non_proxy_admin_allowed_routes_check(
user_obj=user_obj,
_user_role=LitellmUserRoles.INTERNAL_USER.value,
route=route,
request=request,
valid_token=valid_token,
request_data={},
)
assert exc_info.value.status_code == 403
assert f"Route={route}" in str(exc_info.value.detail)
@pytest.mark.parametrize(
"role",
[