From 83f52d8985a419862e5df0edc9626479a1a487f3 Mon Sep 17 00:00:00 2001 From: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:00:57 +0000 Subject: [PATCH] fix(auth): decide team alias access on the target alone A team alias is resolved to its target before any allowlist matching, and the alias name itself is no longer matched against the key or team model list, so an allowlist entry or wildcard that happens to match the alias name cannot grant a target that is not on the list. The resolution is a plain variable swap rather than a recursive call, which keeps the repository's recursion detector happy _can_object_call_model resolves the team alias first and expands router and global aliases from the resolved target, so a team alias that points at a router model-group alias is allowed when the key lists the underlying model. When a name is both a team alias and a router or global alias, only the team target is considered, matching the precedence request routing already applies Co-authored-by: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com> Co-authored-by: songkuan-zheng --- litellm/proxy/auth/auth_checks.py | 31 ++++----- .../proxy/auth/test_auth_checks.py | 69 +++++++++++++++++++ 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 0bead9fb2f4..9453140f472 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -3940,10 +3940,13 @@ def _check_model_access_helper( ## check if model in allowed model names from collections import defaultdict + team_alias_target: Final = team_model_aliases.get(model) if team_model_aliases else None + effective_model: Final = team_alias_target if team_alias_target is not None else model + access_groups: dict[str, list[str]] = defaultdict(list) if llm_router: - access_groups = llm_router.get_model_access_groups(model_name=model, team_id=team_id) + access_groups = llm_router.get_model_access_groups(model_name=effective_model, team_id=team_id) models = _resolve_all_team_model_sentinel_for_auth_check( models=models, @@ -3959,16 +3962,7 @@ def _check_model_access_helper( # Filter out models that are access_groups filtered_models: Final = [m for m in models if m not in access_groups] - team_alias_target: Final = team_model_aliases.get(model) if team_model_aliases else None - if team_alias_target is not None and _check_model_access_helper( - model=team_alias_target, - llm_router=llm_router, - models=models, - team_id=team_id, - ): - return True - - if _model_matches_any_wildcard_pattern_in_list(model=model, allowed_model_list=filtered_models): + if _model_matches_any_wildcard_pattern_in_list(model=effective_model, allowed_model_list=filtered_models): return True all_model_access: bool = False @@ -3979,7 +3973,7 @@ def _check_model_access_helper( if SpecialModelNames.all_proxy_models.value in filtered_models: all_model_access = True - if model is not None and model not in filtered_models and all_model_access is False: + if effective_model is not None and effective_model not in filtered_models and all_model_access is False: return False return True @@ -4024,11 +4018,13 @@ def _can_object_call_model( ) return True - potential_models: Final = [model] - if model in litellm.model_alias_map: - potential_models.append(litellm.model_alias_map[model]) - elif llm_router and model in llm_router.model_group_alias: - _model: Final = llm_router._get_model_from_alias(model) + team_alias_target: Final = team_model_aliases.get(model) if team_model_aliases else None + resolved_model: Final = team_alias_target if team_alias_target is not None else model + potential_models: Final = [resolved_model] + if resolved_model in litellm.model_alias_map: + potential_models.append(litellm.model_alias_map[resolved_model]) + elif llm_router and resolved_model in llm_router.model_group_alias: + _model: Final = llm_router._get_model_from_alias(resolved_model) if _model: potential_models.append(_model) @@ -4038,7 +4034,6 @@ def _can_object_call_model( model=m, llm_router=llm_router, models=models, - team_model_aliases=team_model_aliases, team_id=team_id, ): return True diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index c2a66eece87..94c952839d1 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -519,6 +519,75 @@ def test_check_model_access_helper_self_referential_alias_terminates(): ) +def _router_with_group_alias(alias: str, target: str) -> "Router": + from litellm import Router + + return Router( + model_list=[{"model_name": target, "litellm_params": {"model": "gpt-4o", "api_key": "test-api-key"}}], + model_group_alias={alias: {"model": target, "hidden": False}}, + ) + + +@pytest.mark.asyncio +async def test_can_key_call_model_team_alias_name_in_key_models_is_not_enough(): + """A key that lists the alias name but not its target is denied.""" + from litellm.proxy.auth.auth_checks import can_key_call_model + + for models in (["smart"], ["sm*"]): + valid_token: Final = _aliased_key(models=models) + with pytest.raises(ProxyException) as exc_info: + await can_key_call_model(model="smart", llm_model_list=None, valid_token=valid_token, llm_router=None) + assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied + assert "Tried to access smart" in exc_info.value.message + + +def test_can_object_call_model_team_alias_target_expands_router_alias(): + """A team alias whose target is a router alias is allowed when the key lists the underlying model.""" + llm_router: Final = _router_with_group_alias(alias="router-fast", target="gpt-4o-group") + + assert _can_object_call_model( + model="team-fast", + llm_router=llm_router, + models=["gpt-4o-group"], + team_model_aliases={"team-fast": "router-fast"}, + object_type="key", + ) + with pytest.raises(ProxyException) as exc_info: + _can_object_call_model( + model="team-fast", + llm_router=llm_router, + models=["other"], + team_model_aliases={"team-fast": "router-fast"}, + object_type="key", + ) + assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied + + +def test_can_object_call_model_team_alias_takes_precedence_over_router_alias(): + """A name that is both a team alias and a router alias is checked against the team alias target only.""" + llm_router: Final = _router_with_group_alias(alias="fast", target="gpt-4o-group") + + with pytest.raises(ProxyException) as exc_info: + _can_object_call_model( + model="fast", + llm_router=llm_router, + models=["gpt-4o-group"], + team_model_aliases={"fast": "openai/gpt-4.1-mini"}, + object_type="key", + ) + assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied + assert _can_object_call_model( + model="fast", + llm_router=llm_router, + models=["openai/gpt-4.1-mini"], + team_model_aliases={"fast": "openai/gpt-4.1-mini"}, + object_type="key", + ) + assert _can_object_call_model( + model="fast", llm_router=llm_router, models=["gpt-4o-group"], team_model_aliases=None, object_type="key" + ) + + def test_resolve_key_models_teamless_all_team_models_returns_empty(): """_resolve_key_models_for_auth_check must return [] for a teamless key with all-team-models, making it equivalent to an unscoped key (unrestricted