mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 08:23:17 +00:00
fix: scope key access_group_ids override by team's assigned groups
A team member could set any access_group_ids on their key (e.g. a group assigned only to a different team) and override the team's model restriction. Intersect the key's access_group_ids with team_object.access_group_ids in _key_access_group_grants_model so foreign groups are dropped before model expansion. Adds a regression test that asserts expansion is never called for foreign groups.
This commit is contained in:
@@ -512,6 +512,7 @@ async def common_checks( # noqa: PLR0915
|
||||
if not await _key_access_group_grants_model(
|
||||
model=_model,
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
llm_router=llm_router,
|
||||
):
|
||||
raise
|
||||
@@ -2870,20 +2871,28 @@ async def can_team_access_model(
|
||||
async def _key_access_group_grants_model(
|
||||
model: Union[str, List[str]],
|
||||
valid_token: Optional[UserAPIKeyAuth],
|
||||
team_object: Optional[LiteLLM_TeamTable],
|
||||
llm_router: Optional[Router],
|
||||
) -> bool:
|
||||
"""
|
||||
Returns True if the key's `access_group_ids` expand to models that grant
|
||||
access to `model`. Used to let a key's access group override a team's
|
||||
model restriction in `common_checks`.
|
||||
|
||||
A key's access group only counts if it is also assigned to the key's team
|
||||
(i.e., present in `team_object.access_group_ids`). This preserves the
|
||||
team-as-owner boundary: a team member cannot escalate by naming an access
|
||||
group that belongs to a different team.
|
||||
"""
|
||||
if valid_token is None:
|
||||
if valid_token is None or team_object is None:
|
||||
return False
|
||||
key_access_group_ids = valid_token.access_group_ids or []
|
||||
if not key_access_group_ids:
|
||||
key_access_group_ids = set(valid_token.access_group_ids or [])
|
||||
team_access_group_ids = set(team_object.access_group_ids or [])
|
||||
allowed_group_ids = key_access_group_ids & team_access_group_ids
|
||||
if not allowed_group_ids:
|
||||
return False
|
||||
models_from_groups = await _get_models_from_access_groups(
|
||||
access_group_ids=key_access_group_ids,
|
||||
access_group_ids=list(allowed_group_ids),
|
||||
)
|
||||
if not models_from_groups:
|
||||
return False
|
||||
|
||||
@@ -1164,6 +1164,12 @@ async def test_key_access_group_grants_model_when_group_covers_model():
|
||||
token="test-token",
|
||||
models=[],
|
||||
access_group_ids=["ryan-access-group"],
|
||||
team_id="team-a",
|
||||
)
|
||||
team_object = LiteLLM_TeamTable(
|
||||
team_id="team-a",
|
||||
models=["mock-success"],
|
||||
access_group_ids=["ryan-access-group"],
|
||||
)
|
||||
|
||||
with patch(
|
||||
@@ -1175,6 +1181,7 @@ async def test_key_access_group_grants_model_when_group_covers_model():
|
||||
await _key_access_group_grants_model(
|
||||
model="claude-haiku-4-5",
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
llm_router=None,
|
||||
)
|
||||
is True
|
||||
@@ -1190,11 +1197,18 @@ async def test_key_access_group_grants_model_when_key_has_no_groups():
|
||||
token="test-token",
|
||||
models=[],
|
||||
access_group_ids=[],
|
||||
team_id="team-a",
|
||||
)
|
||||
team_object = LiteLLM_TeamTable(
|
||||
team_id="team-a",
|
||||
models=["mock-success"],
|
||||
access_group_ids=["ryan-access-group"],
|
||||
)
|
||||
assert (
|
||||
await _key_access_group_grants_model(
|
||||
model="claude-haiku-4-5",
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
llm_router=None,
|
||||
)
|
||||
is False
|
||||
@@ -1212,6 +1226,12 @@ async def test_key_access_group_grants_model_when_group_does_not_cover_model():
|
||||
token="test-token",
|
||||
models=[],
|
||||
access_group_ids=["other-group"],
|
||||
team_id="team-a",
|
||||
)
|
||||
team_object = LiteLLM_TeamTable(
|
||||
team_id="team-a",
|
||||
models=["mock-success"],
|
||||
access_group_ids=["other-group"],
|
||||
)
|
||||
|
||||
with patch(
|
||||
@@ -1223,7 +1243,76 @@ async def test_key_access_group_grants_model_when_group_does_not_cover_model():
|
||||
await _key_access_group_grants_model(
|
||||
model="claude-haiku-4-5",
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
llm_router=None,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_access_group_grants_model_when_group_not_assigned_to_team():
|
||||
"""
|
||||
Regression test: a team member naming a foreign access group on their key
|
||||
must NOT escalate to that group's models. The group expands to the requested
|
||||
model, but it isn't assigned to the key's team — so the override is denied.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from litellm.proxy.auth.auth_checks import _key_access_group_grants_model
|
||||
|
||||
valid_token = UserAPIKeyAuth(
|
||||
token="test-token",
|
||||
models=[],
|
||||
access_group_ids=["team-b-premium"],
|
||||
team_id="team-a",
|
||||
)
|
||||
team_object = LiteLLM_TeamTable(
|
||||
team_id="team-a",
|
||||
models=["mock-success"],
|
||||
access_group_ids=["team-a-basic"],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.auth.auth_checks._get_models_from_access_groups",
|
||||
new_callable=AsyncMock,
|
||||
return_value=["claude-opus-4-5"],
|
||||
) as mocked_expand:
|
||||
assert (
|
||||
await _key_access_group_grants_model(
|
||||
model="claude-opus-4-5",
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
llm_router=None,
|
||||
)
|
||||
is False
|
||||
)
|
||||
# Foreign group must be filtered out before expansion ever runs.
|
||||
mocked_expand.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_access_group_grants_model_when_team_has_no_groups():
|
||||
"""Team with no access_group_ids leaves the intersection empty → denied."""
|
||||
from litellm.proxy.auth.auth_checks import _key_access_group_grants_model
|
||||
|
||||
valid_token = UserAPIKeyAuth(
|
||||
token="test-token",
|
||||
models=[],
|
||||
access_group_ids=["ryan-access-group"],
|
||||
team_id="team-a",
|
||||
)
|
||||
team_object = LiteLLM_TeamTable(
|
||||
team_id="team-a",
|
||||
models=["mock-success"],
|
||||
access_group_ids=[],
|
||||
)
|
||||
assert (
|
||||
await _key_access_group_grants_model(
|
||||
model="claude-haiku-4-5",
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
llm_router=None,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user