fix(proxy): return persisted team memberships from /user/new so first CLI login gets the default team (#39545)

* fix(proxy): return persisted team memberships from /user/new

new_user attached default teams after building its response from the
pre-membership snapshot, so NewUserResponse.teams was always empty for
users created with default_internal_user_params.teams. The CLI SSO flow
reads that response on a user's first login and minted a teamless JWT,
which skipped the default team's model allowlist.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): return team ids as a tuple to satisfy LIT001

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-03 11:55:59 -07:00 committed by GitHub
parent 4b1e24eae9
commit c2265b0ef3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 19 additions and 0 deletions

View file

@ -426,6 +426,11 @@ async def add_new_user_to_default_team(
await asyncio.gather(*tasks, return_exceptions=True)
async def _fetch_user_team_ids(user_id: str, prisma_client: "PrismaClient") -> tuple[str, ...]:
user_row: Final = await _user_table(prisma_client).find_unique(where={"user_id": user_id})
return tuple(user_row.teams) if user_row is not None else ()
@router.post(
"/user/new",
tags=["Internal User management"],
@ -580,6 +585,11 @@ async def new_user(
)
user_id: Final = cast(str | None, response.get("user_id", None))
attached_team_ids: Final = (
await _fetch_user_team_ids(user_id=user_id, prisma_client=prisma_client)
if user_id is not None and (_team_id is not None or teams is not None)
else None
)
if organization_ids is not None and user_id is not None:
await _add_user_to_organizations(
@ -596,6 +606,8 @@ async def new_user(
response_dict[key] = value
response_dict["key"] = response.get("token", "")
if attached_team_ids is not None:
response_dict["teams"] = list(attached_team_ids)
new_user_response: Final = NewUserResponse.model_validate(response_dict)

View file

@ -1449,6 +1449,11 @@ async def test_new_user_default_teams_flow(mocker):
return 5 # Low user count, under limit
mock_prisma_client.db.litellm_usertable.count = mock_count
persisted_user_row = mocker.MagicMock()
persisted_user_row.teams = ["96fed65b-0182-4ff4-8429-2721cd7d42af"]
mock_prisma_client.db.litellm_usertable.find_unique = mocker.AsyncMock(
return_value=persisted_user_row
)
# Mock duplicate checks to pass
async def mock_check_duplicate_user_email(*args, **kwargs):
@ -1477,6 +1482,7 @@ async def test_new_user_default_teams_flow(mocker):
"token": "sk-test-token-123",
"expires": None,
"max_budget": 100,
"teams": [],
}
# Mock _add_user_to_team
@ -1551,6 +1557,7 @@ async def test_new_user_default_teams_flow(mocker):
# Verify response structure
assert response.user_id == "test-user-123"
assert response.key == "sk-test-token-123"
assert response.teams == ["96fed65b-0182-4ff4-8429-2721cd7d42af"]
finally:
# Restore original default params (always assign, never delattr — the attribute