mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix: enforce access_group_ids during API request authorization (#23850)
When valid_token.models=[] and access_group_ids is set, _can_object_call_model treated the empty list as "allow all" before reaching the access_group_ids fallback, letting a restricted key call any model. Fix: resolve effective_models from access_group_ids before calling _can_object_call_model when native models is empty. The existing except-ProxyException fallback is preserved for the non-empty native models case, but skipped when we already resolved from access groups to avoid a redundant DB round-trip. Adds test asserting that a key with models=[] + access_group_ids=["ag-limited"] is allowed only the models in the group and denied everything else. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
82d01db3f4
commit
d40826e85f
2 changed files with 78 additions and 7 deletions
|
|
@ -2742,8 +2742,12 @@ async def can_key_call_model(
|
|||
"""
|
||||
Checks if token can call a given model
|
||||
|
||||
1. First checks native key-level model permissions (current implementation)
|
||||
2. If not allowed natively, falls back to access_group_ids on the key
|
||||
1. If key has no native models but has access_group_ids, resolve the allowed
|
||||
model set from those groups first — prevents models=[] from being treated
|
||||
as "allow all" when access groups are configured (fix for issue #23850).
|
||||
2. Check native key-level model permissions with the resolved model list.
|
||||
3. If not allowed natively (non-empty models case), fall back to
|
||||
access_group_ids on the key.
|
||||
|
||||
Returns:
|
||||
- True: if token allowed to call model
|
||||
|
|
@ -2751,21 +2755,35 @@ async def can_key_call_model(
|
|||
Raises:
|
||||
- Exception: If token not allowed to call model
|
||||
"""
|
||||
effective_models = valid_token.models
|
||||
if not effective_models and valid_token.access_group_ids:
|
||||
# Key has no native model list but is scoped to access groups.
|
||||
# Resolve the allowed models now so _can_object_call_model enforces them
|
||||
# instead of treating models=[] as "allow all" (#23850).
|
||||
effective_models = (
|
||||
await _get_models_from_access_groups(
|
||||
access_group_ids=valid_token.access_group_ids,
|
||||
)
|
||||
or effective_models # keep [] only if the group truly has no models
|
||||
)
|
||||
|
||||
try:
|
||||
return _can_object_call_model(
|
||||
model=model,
|
||||
llm_router=llm_router,
|
||||
models=valid_token.models,
|
||||
models=effective_models,
|
||||
team_model_aliases=valid_token.team_model_aliases,
|
||||
team_id=valid_token.team_id,
|
||||
object_type="key",
|
||||
)
|
||||
except ProxyException:
|
||||
# Fallback: check key's access_group_ids
|
||||
key_access_group_ids = valid_token.access_group_ids or []
|
||||
if key_access_group_ids:
|
||||
# Fallback: if native models were non-empty and denied access, check
|
||||
# whether access_group_ids grant additional permissions.
|
||||
# Skip when we already resolved from access_group_ids above to avoid a
|
||||
# redundant DB round-trip and an infinite retry on the same denied model.
|
||||
if valid_token.models and valid_token.access_group_ids:
|
||||
models_from_groups = await _get_models_from_access_groups(
|
||||
access_group_ids=key_access_group_ids,
|
||||
access_group_ids=valid_token.access_group_ids,
|
||||
)
|
||||
if models_from_groups:
|
||||
return _can_object_call_model(
|
||||
|
|
|
|||
|
|
@ -1102,3 +1102,56 @@ async def test_can_key_call_model_via_access_group_ids():
|
|||
valid_token=user_api_key_object,
|
||||
llm_router=router,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_can_key_call_model_access_group_ids_denies_when_models_empty():
|
||||
"""
|
||||
Issue #23850: a key with models=[] and access_group_ids must NOT be able to
|
||||
call a model that is outside the access group, even though models=[] would
|
||||
normally grant all-model access.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from litellm.proxy.auth.auth_checks import can_key_call_model
|
||||
|
||||
user_api_key_object = UserAPIKeyAuth(
|
||||
token="test-token",
|
||||
models=[],
|
||||
access_group_ids=["ag-limited"],
|
||||
)
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "gpt-4",
|
||||
"litellm_params": {"model": "openai/gpt-4", "api_key": "test"},
|
||||
},
|
||||
{
|
||||
"model_name": "claude-3",
|
||||
"litellm_params": {"model": "anthropic/claude-3", "api_key": "test"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Access group only grants gpt-4 — claude-3 must be denied
|
||||
with patch(
|
||||
"litellm.proxy.auth.auth_checks._get_models_from_access_groups",
|
||||
new_callable=AsyncMock,
|
||||
return_value=["gpt-4"],
|
||||
):
|
||||
# Allowed model — must pass
|
||||
await can_key_call_model(
|
||||
model="gpt-4",
|
||||
llm_model_list=[],
|
||||
valid_token=user_api_key_object,
|
||||
llm_router=router,
|
||||
)
|
||||
|
||||
# Denied model — must raise
|
||||
with pytest.raises(Exception):
|
||||
await can_key_call_model(
|
||||
model="claude-3",
|
||||
llm_model_list=[],
|
||||
valid_token=user_api_key_object,
|
||||
llm_router=router,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue