mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(auth): block proxy_admin_viewer from LLM inference routes
non_proxy_admin_allowed_routes_check early-returned on is_llm_api_route before the PROXY_ADMIN_VIEW_ONLY branch, so a view-only admin key could call /v1/chat/completions and spend provider budget. Route viewers through _check_proxy_admin_viewer_access first (Fixes #43478).
This commit is contained in:
parent
b79fc9f1b0
commit
1f71dcee2f
2 changed files with 55 additions and 1 deletions
|
|
@ -284,7 +284,16 @@ class RouteChecks:
|
|||
jwt_team_allowed_routes=RouteChecks._jwt_team_allowed_routes(valid_token=valid_token),
|
||||
)
|
||||
elif RouteChecks.is_llm_api_route(route=route):
|
||||
pass
|
||||
# View-only admins must not spend provider budget on inference.
|
||||
# _check_proxy_admin_viewer_access already 403s LLM routes, but that
|
||||
# branch sits below this early pass — so call it here first (#43478).
|
||||
if _user_role == LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value:
|
||||
RouteChecks._check_proxy_admin_viewer_access(
|
||||
route=route,
|
||||
_user_role=_user_role,
|
||||
request_data=request_data,
|
||||
request=request,
|
||||
)
|
||||
elif RouteChecks.is_info_route(route=route):
|
||||
# check if user allowed to call an info route
|
||||
if route == "/key/info":
|
||||
|
|
|
|||
|
|
@ -206,6 +206,51 @@ def test_proxy_admin_viewer_config_update_route_rejected():
|
|||
assert "role= proxy_admin_viewer" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"inference_route",
|
||||
[
|
||||
"/v1/chat/completions",
|
||||
"/chat/completions",
|
||||
"/v1/embeddings",
|
||||
"/v1/responses",
|
||||
"/v1/images/generations",
|
||||
],
|
||||
)
|
||||
def test_proxy_admin_viewer_inference_routes_rejected(inference_route):
|
||||
"""proxy_admin_viewer must not call cost-incurring LLM routes (#43478).
|
||||
|
||||
`non_proxy_admin_allowed_routes_check` used to early-return on
|
||||
`is_llm_api_route`, so `_check_proxy_admin_viewer_access` never ran for
|
||||
/v1/chat/completions and friends — a view-only key could spend budget.
|
||||
"""
|
||||
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 = {}
|
||||
request.method = "POST"
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value,
|
||||
route=inference_route,
|
||||
request=request,
|
||||
valid_token=valid_token,
|
||||
request_data={},
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "OpenAI routes" in str(exc_info.value.detail) or "not allowed" in str(exc_info.value.detail)
|
||||
assert "proxy_admin_viewer" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"blocked_route",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue