From 548d3d1b0a7fcdbf3eb03d5345c489eef709e98a Mon Sep 17 00:00:00 2001 From: jesus Date: Tue, 8 Sep 2026 15:34:28 +0000 Subject: [PATCH] fix(proxy): recognize team admins from members_with_roles in project create/update permission check Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../management_endpoints/project_endpoints.py | 18 ++++++----- .../test_project_endpoints_prisma.py | 30 +++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py index b2eda76f9ae..d652670c220 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py @@ -22,7 +22,7 @@ from litellm._uuid import uuid from litellm.proxy._types import * from litellm.proxy.auth.auth_checks import delete_cached_project_object from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.management_endpoints.common_utils import _set_object_metadata_field +from litellm.proxy.management_endpoints.common_utils import _is_user_team_admin, _set_object_metadata_field from litellm.proxy.management_helpers.utils import ( management_endpoint_wrapper, ) @@ -105,14 +105,16 @@ async def _check_user_permission_for_project( if not team_id or not user_api_key_dict.user_id: return False - team = team_object - if team is None: - team = await _team_table(prisma_client).find_unique(where={"team_id": team_id}) + team_row: Final = ( + team_object + if team_object is not None + else await _team_table(prisma_client).find_unique(where={"team_id": team_id}) + ) + if team_row is None: + return False - if team and team.admins: - return user_api_key_dict.user_id in team.admins - - return False + team: Final = LiteLLM_TeamTable.model_validate(team_row.model_dump()) + return _is_user_team_admin(user_api_key_dict, team) or user_api_key_dict.user_id in (team.admins or []) async def _validate_team_exists( diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index c23b203feba..d1f39fa8df6 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -1368,3 +1368,33 @@ async def test_new_project_flag_on_access_group_model_returns_400(monkeypatch): assert "prod-models" in str(exc_info.value) assert "expand to multiple models at request time" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_check_user_permission_for_project_uses_members_with_roles(): + """Team admins added via /team/member_add live in members_with_roles, not the legacy admins list.""" + from litellm.proxy._types import LiteLLM_TeamTable, Member + from litellm_enterprise.proxy.management_endpoints.project_endpoints import ( + _check_user_permission_for_project, + ) + + team = LiteLLM_TeamTable( + team_id="team-1", + admins=[], + members_with_roles=[ + Member(user_id="team-admin", role="admin"), + Member(user_id="plain-member", role="user"), + ], + ) + + async def check(user_id: str) -> bool: + return await _check_user_permission_for_project( + user_api_key_dict=UserAPIKeyAuth(user_id=user_id, user_role=LitellmUserRoles.INTERNAL_USER), + team_id="team-1", + prisma_client=mock.MagicMock(), + team_object=team, + ) + + assert await check("team-admin") is True + assert await check("plain-member") is False + assert await check("stranger") is False