fix(proxy): apply full key/team/user filter on by-id /v1/model/info

Addresses veria-ai Finding 2 on #29748. The 5638d58e bypass keyed
off `user_api_key_dict.user_id is None` to short-circuit by-id
authorization, but service-account virtual keys can carry
`user_id=None` AND still have `models` / `team_models` restrictions
— so a restricted service account could pull metadata for any
deployment via `/v1/model/info?litellm_model_id=<disallowed-id>`.

Drop the explicit master-key short-circuit and call
`get_available_models_for_user` unconditionally — same call the
listing path uses. `_apply_user_models_filter` already bypasses
the user.models step when `user_id is None` (master key / service
account), but `get_key_models` + `get_team_models` still narrow
to the key/team allowlist for service accounts. By-id authorization
now tracks the listing path exactly.

Test fixes:
- `test_model_info_endpoint_returns_defaults_for_specific_model_id`:
  add the missing patches for `get_key_models` / `get_team_models` /
  `get_complete_model_list` + stub `mock_router.get_model_names` /
  `.get_model_access_groups` so by-id authorization sees a valid
  allowlist. 18/18 pass locally.

403 message broadened to "not authorized for this key/user" since
the rejection can fire on key/team restrictions, not just user.models.
This commit is contained in:
songkuan-zheng 2026-06-16 00:43:00 +00:00
parent 3cdde571d1
commit 968835b5e4
2 changed files with 42 additions and 25 deletions

View file

@ -13445,33 +13445,37 @@ 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. Master
# key / service accounts (no user_id) bypass, matching
# `_apply_user_models_filter` semantics.
if user_api_key_dict.user_id is not None:
from litellm.proxy.utils import get_available_models_for_user
# Authorize against key/team/user models — by-id was previously
# short-circuiting before the listing-path filter, letting a
# restricted virtual key (including service accounts with no
# user_id but with key.models or team_models constraints) pull
# metadata for any deployment via /v1/model/info?litellm_model_id=.
# Reuse `get_available_models_for_user` so by-id authorization
# tracks the listing-path filter exactly: key.models, team_models,
# and user.models all narrow the allowlist; `_apply_user_models_filter`
# bypasses the user.models step when user_id is absent.
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,
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 key/user"
},
)
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)
)

View file

@ -115,6 +115,11 @@ class TestModelInfoEndpointWithRouter:
mock_router = MagicMock()
mock_router.get_deployment.return_value = deployment
# By-id now authorizes against the listing-path filter; stub
# what `get_available_models_for_user` reads from the router so
# "model1" is in the allowlist.
mock_router.get_model_names.return_value = ["model1"]
mock_router.get_model_access_groups.return_value = {}
user_api_key_dict = UserAPIKeyAuth(api_key="sk-test")
@ -122,6 +127,14 @@ class TestModelInfoEndpointWithRouter:
patch("litellm.proxy.proxy_server.llm_router", mock_router),
patch("litellm.proxy.proxy_server.llm_model_list", []),
patch("litellm.proxy.proxy_server.user_model", None),
patch("litellm.proxy.proxy_server.get_key_models", return_value=["model1"]),
patch(
"litellm.proxy.proxy_server.get_team_models", return_value=["model1"]
),
patch(
"litellm.proxy.proxy_server.get_complete_model_list",
return_value=["model1"],
),
):
response = await model_info_v1(
user_api_key_dict=user_api_key_dict,