mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
fix(proxy): close by-id authorization bypass on /v1/model/info
The `litellm_model_id` query branch in `model_info_v1` returned deployment metadata before the listing-path user-filter ever ran, so a user restricted by `LiteLLM_UserTable.models` could pull info for any deployment via `/v1/model/info?litellm_model_id=<id>`. Reported by @veria-ai on this PR. Fix in-place rather than as a follow-up since closing the discovery-vs-inference gap is the PR's whole point. - After `llm_router.get_deployment(...)` resolves, call `get_available_models_for_user` (same signature as the listing path) and intersect against `deployment_info.model_name`. - If not in the authorized list, raise `HTTPException(403, ...)` with a "not authorized" message — 403, not 404, since the user IS authenticated, matching inference-time `can_user_call_model`. - Admin / master-key unaffected: `_apply_user_models_filter` returns `all_models` unchanged when `user_api_key_dict.user_id` is None. Tests (`test_v1_model_info_user_filter.py`): - `test_v1_model_info_by_id_denied_when_model_not_in_user_models` - `test_v1_model_info_by_id_allowed_when_model_in_user_models` - `test_v1_model_info_by_id_master_key_bypass` 11/11 pass locally.
This commit is contained in:
parent
6137e2f467
commit
c4ee909407
2 changed files with 90 additions and 0 deletions
|
|
@ -13445,6 +13445,30 @@ async def model_info_v1(
|
|||
"error": f"Model id = {litellm_model_id} not found on litellm proxy"
|
||||
},
|
||||
)
|
||||
# Authorize against user.models — by-id was previously
|
||||
# short-circuiting before the listing-path filter.
|
||||
from litellm.proxy.utils import get_available_models_for_user
|
||||
|
||||
authorized_models = await get_available_models_for_user(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
llm_router=llm_router,
|
||||
general_settings=general_settings,
|
||||
user_model=user_model,
|
||||
prisma_client=prisma_client,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
team_id=None,
|
||||
include_model_access_groups=False,
|
||||
only_model_access_groups=False,
|
||||
return_wildcard_routes=False,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
)
|
||||
if deployment_info.model_name not in authorized_models:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": f"Model id = {litellm_model_id} not authorized for this user"
|
||||
},
|
||||
)
|
||||
_deployment_info_dict = _get_proxy_model_info(
|
||||
model=deployment_info.model_dump(exclude_none=True)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -236,3 +236,69 @@ def test_v1_model_info_alias_route_filters_identically(
|
|||
resp = client.get("/model/info", headers={"Authorization": "Bearer sk-test"})
|
||||
assert resp.status_code == 200
|
||||
assert _model_names(resp.json()) == ["claude-3-opus"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /v1/model/info Path A (?litellm_model_id=...) — by-id authorization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _deployment_id(router, model_name):
|
||||
for d in router.model_list:
|
||||
if d["model_name"] == model_name:
|
||||
return d["model_info"]["id"]
|
||||
raise AssertionError(f"model {model_name!r} not in router")
|
||||
|
||||
|
||||
def test_v1_model_info_by_id_denied_when_model_not_in_user_models(
|
||||
client, configure_router, monkeypatch
|
||||
):
|
||||
"""Regression: by-id lookup must respect user.models (veria-ai #29748)."""
|
||||
_override_auth(user_id="u-test")
|
||||
_patch_user(monkeypatch, models=["gpt-4"])
|
||||
disallowed_id = _deployment_id(configure_router, "claude-3-opus")
|
||||
|
||||
resp = client.get(
|
||||
f"/v1/model/info?litellm_model_id={disallowed_id}",
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
assert "not authorized" in resp.text.lower()
|
||||
|
||||
|
||||
def test_v1_model_info_by_id_allowed_when_model_in_user_models(
|
||||
client, configure_router, monkeypatch
|
||||
):
|
||||
"""By-id lookup for a deployment whose model_name is in user.models -> 200."""
|
||||
_override_auth(user_id="u-test")
|
||||
_patch_user(monkeypatch, models=["claude-3-opus"])
|
||||
allowed_id = _deployment_id(configure_router, "claude-3-opus")
|
||||
|
||||
resp = client.get(
|
||||
f"/v1/model/info?litellm_model_id={allowed_id}",
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert _model_names(resp.json()) == ["claude-3-opus"]
|
||||
|
||||
|
||||
def test_v1_model_info_by_id_master_key_bypass(client, configure_router, monkeypatch):
|
||||
"""Master key / service account (user_id=None) -> by-id lookup unrestricted."""
|
||||
|
||||
async def _should_not_be_called(*args, **kwargs):
|
||||
raise AssertionError("get_user_object must not be called when user_id is None")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.auth.auth_checks.get_user_object",
|
||||
_should_not_be_called,
|
||||
)
|
||||
|
||||
_override_auth(user_id=None)
|
||||
any_id = _deployment_id(configure_router, "claude-3-opus")
|
||||
|
||||
resp = client.get(
|
||||
f"/v1/model/info?litellm_model_id={any_id}",
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert _model_names(resp.json()) == ["claude-3-opus"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue