fix: order CLI SSO team choices by alias then id for a stable consent default

This commit is contained in:
72004 2026-08-21 00:01:39 +05:00
parent 487356733c
commit 77fabdf5e9
2 changed files with 63 additions and 1 deletions

View file

@ -2203,6 +2203,18 @@ def _cli_sso_team_detail(team_row: Mapping[str, object]) -> CliSsoTeamDetail:
)
def _cli_sso_team_sort_key(detail: CliSsoTeamDetail) -> tuple[str, str]:
"""Stable, predictable order for a user's team choices.
The DB returns rows in an arbitrary order, so the consent page (and the
classic login team choice, which shares this lookup) would preselect
whichever team happened to come back first -- silently binding a user who
takes the default to a team they did not mean to pick. Order by alias
case-insensitively, with team_id as a deterministic tiebreaker.
"""
return ((detail.team_alias or "").casefold(), detail.team_id or "")
async def fetch_cli_sso_team_details(
prisma_client: PrismaClient,
teams: Sequence[str],
@ -2218,7 +2230,10 @@ async def fetch_cli_sso_team_details(
except Exception as e:
verbose_proxy_logger.error("Error fetching team details for CLI SSO session: %s", e)
return None
return tuple(_cli_sso_team_detail(team_row.model_dump()) for team_row in prisma_teams)
details: Final = (
_cli_sso_team_detail(team_row.model_dump()) for team_row in prisma_teams
)
return tuple(sorted(details, key=_cli_sso_team_sort_key))
def _cli_sso_session_teams(team_details: Sequence[CliSsoTeamDetail]) -> list[str]:

View file

@ -8344,3 +8344,50 @@ class TestPersistReturnToCookieSharedHelper:
resp = Response()
_persist_return_to_cookie(resp, "https://cp.example.com/ui?page=models")
assert "litellm_cp_return_to=" in self._cookie(resp)
@pytest.mark.asyncio
async def test_fetch_cli_sso_team_details_orders_by_alias_then_id():
"""CLI SSO team choices must come back in a stable order.
The consent page preselects the first team, so an arbitrary DB order can
silently bind a user who takes the default to a team they did not mean to
pick. Teams must be ordered by alias case-insensitively, then team_id as a
deterministic tiebreaker. Regression for the CLI login consent page.
"""
from unittest.mock import AsyncMock, MagicMock, patch
from litellm.proxy.management_endpoints import ui_sso as ui_sso_mod
def _row(team_id, alias):
row = MagicMock()
row.model_dump.return_value = {
"team_id": team_id,
"team_alias": alias,
"models": [],
}
return row
# Deliberately scrambled DB order, mixed case, plus an alias-less team.
rows = [
_row("35551", "forge-team-b-35551"),
_row("10000", "Forge-Team-A-35551"),
_row("99999", None),
]
repo = MagicMock()
repo.find_many = AsyncMock(return_value=rows)
with patch.object(ui_sso_mod, "TeamRepository", return_value=MagicMock()), patch.object(
ui_sso_mod, "_team_detail_db", return_value=repo
):
result = await ui_sso_mod.fetch_cli_sso_team_details(
prisma_client=MagicMock(), teams=["35551", "10000", "99999"]
)
assert result is not None
# None alias sorts first (""), then case-insensitive "Forge-Team-A" < "forge-team-b".
assert [d.team_alias for d in result] == [
None,
"Forge-Team-A-35551",
"forge-team-b-35551",
]