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 <songkuan-zheng@users.noreply.github.com>
This commit is contained in:
songkuan-zheng 2026-09-11 12:00:57 +00:00
parent a0756b71cf
commit 83f52d8985
2 changed files with 82 additions and 18 deletions

View file

@ -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

View file

@ -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