fix(team): 404 a role update whose target left the roster before the locked read

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-19 23:57:38 +00:00
parent 431ddbdd22
commit b7db48c7c1
2 changed files with 42 additions and 0 deletions

View file

@ -3069,6 +3069,9 @@ async def _update_team_member_role(
raise HTTPException(status_code=404, detail={"error": f"Team id={team_id} does not exist in db"})
before: Final = tuple(locked_members)
if all(member.user_id != user_id for member in before):
raise HTTPException(status_code=404, detail={"error": f"User {user_id} is not a member of team {team_id}"})
after: Final = tuple(
Member(user_id=member.user_id, role=role, user_email=user_email or member.user_email)
if member.user_id == user_id

View file

@ -13675,6 +13675,45 @@ async def test_team_member_update_role_change_404s_when_the_team_is_gone_under_t
mock_prisma_client.db.litellm_teamtable.update.assert_not_awaited()
@pytest.mark.asyncio
async def test_team_member_update_role_change_404s_when_the_member_left_before_the_locked_read(monkeypatch):
"""Regression: a member removed between the pre-lock read and the locked read was reported as updated."""
audit_logger = _wire_audit_log_callback(monkeypatch)
snapshot = LiteLLM_TeamTable(
team_id="team-member-gone-race",
team_alias="member-gone-race",
metadata={},
members_with_roles=[Member(user_id="alice", role="admin"), Member(user_id="bob", role="user")],
)
locked_row = LiteLLM_TeamTable(
team_id="team-member-gone-race",
team_alias="member-gone-race",
metadata={},
members_with_roles=[Member(user_id="alice", role="admin")],
)
mock_prisma_client = MagicMock()
mock_prisma_client.db.litellm_teamtable.find_unique = AsyncMock(side_effect=[snapshot, locked_row])
mock_prisma_client.db.litellm_teamtable.update = AsyncMock()
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
_wire_member_delete_tx(mock_prisma_client)
team_info_patch, upsert_patch = _member_update_patches(snapshot)
with team_info_patch, upsert_patch, pytest.raises(HTTPException) as exc_info:
await team_member_update(
data=TeamMemberUpdateRequest(team_id="team-member-gone-race", user_id="bob", role="admin"),
http_request=MagicMock(),
user_api_key_dict=UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-admin", user_id="admin-user"
),
)
assert exc_info.value.status_code == 404
assert "bob" in str(exc_info.value.detail)
mock_prisma_client.db.litellm_teamtable.update.assert_not_awaited()
await _settle_audit_log_tasks()
assert audit_logger.payloads == []
@pytest.mark.asyncio
async def test_team_member_delete_response_does_not_wait_for_the_audit_insert(
monkeypatch, mock_db_client, mock_admin_auth