revert: undo teamless all-team-models denial from #32022 and #29746 (#32032)

* Revert "fix(auth): deny model access for teamless keys with all-team-models (#32022)"

This reverts commit dfbbda4f19.

* revert: undo teamless all-team-models denial from PR #29746

Reverts the team_id guard in _resolve_key_models_for_auth_check and
get_key_models so teamless keys with all-team-models resolve to []
(unrestricted = all proxy models) rather than being denied.

Adds hardened regression tests across listing (get_key_models), inference
(_enforce_key_and_fallback_model_access, can_key_call_model,
can_key_call_resolved_model), and batch (_enforce_batch_file_model_access)
paths that enforce teamless all-team-models == all-proxy-models and will
fail if anyone re-introduces a team_id guard

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: retrigger checks

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-07-03 18:56:07 -07:00 • committed by GitHub
parent 718e9cfa11
commit 5ece78fb5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 126 additions and 54 deletions

View file

@ -2941,18 +2941,16 @@ def _resolve_key_models_for_auth_check(valid_token: UserAPIKeyAuth) -> List[str]
"""
Expand key model sentinels before auth checks.
``all-team-models`` means inherit the parent team's allowlist — same
``all-team-models`` means inherit the parent team's allowlist -- same
semantics as ``get_key_models`` in ``model_checks.py``.
If the key has no team_id the sentinel cannot be resolved, so the original
model list (still containing the sentinel string) is returned unchanged.
That string won't match any real model, so access is denied rather than
silently falling through to unrestricted access.
If the key has no team_id, it inherits the full proxy model list
(equivalent to an empty models field, i.e. unrestricted access).
"""
models = list(valid_token.models or [])
if SpecialModelNames.all_team_models.value in models:
if valid_token.team_id is None:
return models
return []
return list(valid_token.team_models or [])
return models
@ -3017,9 +3015,7 @@ async def can_key_call_resolved_model(
)
skip_key_model_check = valid_token.config or (
isinstance(valid_token.models, list)
and SpecialModelNames.all_team_models.value in valid_token.models
and valid_token.team_id is not None
isinstance(valid_token.models, list) and SpecialModelNames.all_team_models.value in valid_token.models
)
if not skip_key_model_check:
await can_key_call_model(

View file

@ -111,7 +111,7 @@ def get_key_models(
all_models: List[str] = []
if len(user_api_key_dict.models) > 0:
all_models = list(user_api_key_dict.models) # copy to avoid mutating cached objects
if SpecialModelNames.all_team_models.value in all_models and user_api_key_dict.team_id is not None:
if SpecialModelNames.all_team_models.value in all_models:
all_models = list(user_api_key_dict.team_models)
if SpecialModelNames.all_team_models.value in all_models:
all_models = [model for model in all_models if model != SpecialModelNames.all_team_models.value]

View file

@ -2776,11 +2776,7 @@ async def _enforce_key_and_fallback_model_access(
model_list = config.get("model_list", [])
new_model_list = model_list
verbose_proxy_logger.debug(f"\n new llm router model list {new_model_list}")
elif (
isinstance(valid_token.models, list)
and "all-team-models" in valid_token.models
and valid_token.team_id is not None
):
elif isinstance(valid_token.models, list) and "all-team-models" in valid_token.models:
pass
else:
model = _get_model_from_request_context(

View file

@ -333,8 +333,10 @@ async def test_can_key_call_model_all_team_models_empty_team_models_is_unrestric
@pytest.mark.asyncio
async def test_can_key_call_model_all_team_models_no_team_id_is_denied():
"""Key with all-team-models but no team_id cannot resolve the sentinel; access must be denied."""
async def test_can_key_call_model_all_team_models_no_team_id_is_unrestricted():
"""A teamless key with all-team-models inherits the full proxy model list
(empty resolved list = unrestricted access), the same as leaving the models
field empty. This test will fail if someone re-introduces a teamless denial."""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.auth.auth_checks import can_key_call_model
@ -344,67 +346,86 @@ async def test_can_key_call_model_all_team_models_no_team_id_is_denied():
team_models=[],
)
with pytest.raises(ProxyException) as exc_info:
assert (
await can_key_call_model(
model="gpt-4o",
llm_model_list=None,
valid_token=valid_token,
llm_router=None,
)
assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied
@pytest.mark.asyncio
async def test_enforce_key_access_teamless_all_team_models_denied():
"""_enforce_key_and_fallback_model_access must not skip key-level
model checks for a teamless key that carries all-team-models."""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.auth.user_api_key_auth import (
_enforce_key_and_fallback_model_access,
is True
)
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
access). Fails if someone returns the sentinel list for teamless keys."""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.auth.auth_checks import _resolve_key_models_for_auth_check
valid_token = UserAPIKeyAuth(
api_key="sk-orphan-key",
api_key="sk-orphan",
models=[SpecialModelNames.all_team_models.value],
team_models=[],
)
with pytest.raises(ProxyException) as exc_info:
await _enforce_key_and_fallback_model_access(
valid_token=valid_token,
request_data={"model": "gpt-4o"},
route="/v1/chat/completions",
request=None,
llm_model_list=None,
llm_router=None,
)
assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied
result = _resolve_key_models_for_auth_check(valid_token)
assert result == [], "teamless all-team-models must resolve to [] (unrestricted)"
@pytest.mark.asyncio
async def test_can_key_call_resolved_model_teamless_all_team_models_denied():
"""can_key_call_resolved_model must not skip key-level model checks
for a teamless key that carries all-team-models."""
async def test_enforce_key_access_teamless_all_team_models_passes():
"""_enforce_key_and_fallback_model_access must not deny a teamless key with
all-team-models. The inference path skips the key-level model check when
the sentinel is present, regardless of team_id. Fails if someone adds a
team_id guard to the pass branch."""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.auth.user_api_key_auth import _enforce_key_and_fallback_model_access
valid_token = UserAPIKeyAuth(
api_key="sk-orphan",
models=[SpecialModelNames.all_team_models.value],
team_models=[],
)
await _enforce_key_and_fallback_model_access(
valid_token=valid_token,
request_data={"model": "gpt-4o"},
route="/chat/completions",
request=None,
llm_model_list=None,
llm_router=None,
)
@pytest.mark.asyncio
async def test_can_key_call_resolved_model_teamless_all_team_models_passes():
"""can_key_call_resolved_model must skip the key model check for a teamless
key with all-team-models. Fails if someone adds a team_id guard to the
skip_key_model_check condition."""
from unittest.mock import AsyncMock, patch
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.auth.auth_checks import can_key_call_resolved_model
valid_token = UserAPIKeyAuth(
api_key="sk-orphan-key",
api_key="sk-orphan",
models=[SpecialModelNames.all_team_models.value],
team_models=[],
)
with pytest.raises(ProxyException) as exc_info:
await can_key_call_resolved_model(
model="gpt-4o",
llm_model_list=None,
valid_token=valid_token,
llm_router=None,
)
assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied
with patch("litellm.proxy.auth.auth_checks.can_key_call_model", new_callable=AsyncMock) as mock_call:
with patch("litellm.proxy.proxy_server.prisma_client", None):
with patch("litellm.proxy.proxy_server.proxy_logging_obj", None):
with patch("litellm.proxy.proxy_server.user_api_key_cache", None):
await can_key_call_resolved_model(
model="gpt-4o",
llm_model_list=None,
valid_token=valid_token,
llm_router=None,
)
mock_call.assert_not_awaited()
@pytest.mark.asyncio

View file

@ -620,6 +620,29 @@ def test_get_team_models_all_team_models_expands_with_access_groups():
assert "group-2" in result
def test_get_key_models_teamless_all_team_models_returns_unrestricted():
"""Teamless key with all-team-models must resolve the same as leaving the
models field empty ([] = unrestricted). The sentinel must not leak into
the returned list. Fails if someone adds a team_id guard to the sentinel
expansion in get_key_models."""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.auth.model_checks import get_key_models
user_api_key_dict = type(
"obj",
(object,),
{
"models": [SpecialModelNames.all_team_models.value],
"team_id": None,
"team_models": [],
},
)()
proxy_model_list = ["gpt-4o", "claude-sonnet-4-20250514"]
result = get_key_models(user_api_key_dict, proxy_model_list, {})
assert SpecialModelNames.all_team_models.value not in result
assert result == [], "should return [] (unrestricted), same as an unscoped key"
def test_expand_wildcard_deployments_non_wildcard_passthrough():
"""Non-wildcard deployments must be returned unchanged."""
from litellm.proxy.auth.model_checks import (

View file

@ -463,6 +463,42 @@ async def test_pre_call_fails_closed_when_current_team_fetch_fails_for_all_team_
mock_can_key_call_model.assert_not_awaited()
@pytest.mark.asyncio
async def test_pre_call_allows_teamless_all_team_models_key():
"""A teamless key with all-team-models must be allowed to submit batch jobs
for any model (same as leaving models empty = unrestricted). Fails if
someone re-introduces a teamless denial in _resolve_key_models_for_auth_check
or adds a team_id guard that blocks the batch path."""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.hooks.batch_rate_limiter import _PROXY_BatchRateLimiter
rate_limiter = _PROXY_BatchRateLimiter(
internal_usage_cache=MagicMock(),
parallel_request_limiter=MagicMock(),
)
file_dict = [
{
"body": {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "x"}],
}
}
]
user = UserAPIKeyAuth(
api_key="sk-orphan",
user_id="alice",
models=[SpecialModelNames.all_team_models.value],
team_models=[],
user_role=LitellmUserRoles.INTERNAL_USER.value,
)
with patch("litellm.proxy.proxy_server.llm_router", None):
await rate_limiter._enforce_batch_file_model_access(
user_api_key_dict=user,
models=_models(file_dict),
)
@pytest.mark.asyncio
async def test_pre_call_allows_authorized_model_in_batch_file():
"""If every model in the JSONL is on the caller's allowlist, the hook