fix(proxy): reduce team-list where-builder complexity and update org-admin scope tests

This commit is contained in:
michelligabriele 2026-06-29 21:24:21 +02:00
parent b4b6f585c5
commit 919923d2bd
No known key found for this signature in database
2 changed files with 41 additions and 12 deletions

View file

@ -3839,6 +3839,28 @@ async def _get_org_admin_org_ids(
return org_ids if org_ids else None
def _apply_org_admin_membership_union(
where_conditions: dict[str, Any],
org_admin_org_ids: Optional[list[str]],
user_team_ids: list[str],
) -> None:
"""Union (teams in the admin's orgs) with (teams the caller is a member of).
Mutates ``where_conditions`` in place for a self/bare org-admin query so
teams in orgs where the caller is only a member stay visible (LIT-3723).
An org admin with no memberships still sees their org teams, so this never
collapses to an empty result.
"""
union_or: list[dict[str, Any]] = [{"organization_id": {"in": org_admin_org_ids}}]
if user_team_ids:
union_or.append({"team_id": {"in": user_team_ids}})
existing_or = where_conditions.pop("OR", None) # set above from `search`
if existing_or is not None:
where_conditions["AND"] = [{"OR": existing_or}, {"OR": union_or}]
else:
where_conditions["OR"] = union_or
async def _build_team_list_where_conditions(
prisma_client: PrismaClient,
team_id: Optional[str],
@ -3917,14 +3939,7 @@ async def _build_team_list_where_conditions(
# of), so teams in orgs where I'm only a member stay visible
# (LIT-3723). An org admin with no memberships must still see org
# teams, so do NOT early-return None here.
union_or: List[Dict[str, Any]] = [{"organization_id": {"in": org_admin_org_ids}}]
if user_team_ids:
union_or.append({"team_id": {"in": user_team_ids}})
existing_or = where_conditions.pop("OR", None) # set above from `search`
if existing_or is not None:
where_conditions["AND"] = [{"OR": existing_or}, {"OR": union_or}]
else:
where_conditions["OR"] = union_or
_apply_org_admin_membership_union(where_conditions, org_admin_org_ids, user_team_ids)
else:
# When user_id is provided, filter by that user's direct team
# memberships. For org admins the access control gate in

View file

@ -3137,6 +3137,7 @@ async def test_list_team_v2_org_admin_sees_org_teams():
organization_id=None,
team_id=None,
team_alias=None,
search=None,
user_api_key_dict=mock_user_api_key_dict,
page=1,
page_size=10,
@ -3149,9 +3150,12 @@ async def test_list_team_v2_org_admin_sees_org_teams():
assert len(result["teams"]) == 1
assert result["teams"][0].members_count == 1
# Verify org-scoped where clause
# Bare org-admin query is scoped to the admin's org via the union
# branch (LIT-3723). With no direct memberships the union collapses to
# the org-scope branch only.
where = mock_db.litellm_teamtable.find_many.call_args.kwargs["where"]
assert where["organization_id"] == {"in": ["org_A"]}
assert where["OR"] == [{"organization_id": {"in": ["org_A"]}}]
assert "organization_id" not in where
@pytest.mark.asyncio
@ -3237,6 +3241,7 @@ async def test_list_team_v2_org_admin_own_user_id_sees_all_org_teams():
organization_id=None,
team_id=None,
team_alias=None,
search=None,
user_api_key_dict=mock_user_api_key_dict,
page=1,
page_size=10,
@ -3248,9 +3253,18 @@ async def test_list_team_v2_org_admin_own_user_id_sees_all_org_teams():
assert result["total"] == 2
assert len(result["teams"]) == 2
# Verify the where clause scopes by org only — no team_id filter
# Self/bare org-admin query unions org-scoped teams with the caller's
# own memberships (LIT-3723). The org-scope branch still guarantees the
# admin sees *all* teams in their org (regression #30215); the team_id
# branch only widens the result and never clamps it. No top-level
# organization_id/team_id key that could hide an org team the admin
# isn't a direct member of.
where = mock_db.litellm_teamtable.find_many.call_args.kwargs["where"]
assert where["organization_id"] == {"in": ["org_A"]}
assert where["OR"] == [
{"organization_id": {"in": ["org_A"]}},
{"team_id": {"in": ["team_1"]}},
]
assert "organization_id" not in where
assert "team_id" not in where