From daf7c0c3a85947aaadb8cc6ea2679fda70ee3183 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 7 Mar 2026 16:23:12 -0800 Subject: [PATCH] fix: scoping virtual keys in the teams view to be applying the team filter globally instead of an or branch --- .../key_management_endpoints.py | 6 +-- .../test_key_management_endpoints.py | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 3b54a79a73..68f997e29c 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -4345,8 +4345,6 @@ def _build_key_filter_conditions( user_condition: Dict[str, Any] = {} if user_id and isinstance(user_id, str): user_condition["user_id"] = user_id - if team_id and isinstance(team_id, str): - user_condition["team_id"] = team_id if key_alias and isinstance(key_alias, str): user_condition["key_alias"] = key_alias if exclude_team_id and isinstance(exclude_team_id, str): @@ -4414,8 +4412,10 @@ def _build_key_filter_conditions( elif len(or_conditions) == 1: where.update(or_conditions[0]) - # Apply project_id and access_group_id as global AND filters so they + # Apply team_id, project_id and access_group_id as global AND filters so they # narrow results across all visibility conditions (own keys, team keys, etc.) + if team_id and isinstance(team_id, str): + where = {"AND": [where, {"team_id": team_id}]} if project_id: where = {"AND": [where, {"project_id": project_id}]} if access_group_id: diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index 6195f34f28..b3e564dc73 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -6344,6 +6344,45 @@ async def test_build_key_filter_project_id_and_access_group_id(): assert {"project_id": project_id} in inner_and +@pytest.mark.asyncio +async def test_build_key_filter_team_id_scoped(): + """ + When team_id is provided, it should act as a global AND filter so keys + from other teams are excluded — even when the user is admin of multiple teams. + """ + from litellm.proxy.management_endpoints.key_management_endpoints import ( + _build_key_filter_conditions, + ) + + where = _build_key_filter_conditions( + user_id="multi-team-user", + team_id="team-A", + organization_id=None, + key_alias=None, + key_hash=None, + exclude_team_id=None, + admin_team_ids=["team-A", "team-B"], + member_team_ids=["team-A", "team-B"], + include_created_by_keys=True, + ) + + def _collect_team_id_filters(d): + results = [] + if isinstance(d, dict): + for k, v in d.items(): + if k == "team_id": + results.append(v) + else: + results.extend(_collect_team_id_filters(v)) + elif isinstance(d, list): + for item in d: + results.extend(_collect_team_id_filters(item)) + return results + + team_id_filters = _collect_team_id_filters(where) + assert "team-A" in team_id_filters, f"Expected global team_id='team-A' AND filter, got: {where}" + + @pytest.mark.asyncio async def test_get_member_team_ids(): """