diff --git a/tests/test_litellm/proxy/auth_v2/test_resolver.py b/tests/test_litellm/proxy/auth_v2/test_resolver.py index 84dec45ba53..f381818d3ad 100644 --- a/tests/test_litellm/proxy/auth_v2/test_resolver.py +++ b/tests/test_litellm/proxy/auth_v2/test_resolver.py @@ -3,6 +3,9 @@ from __future__ import annotations from typing import Dict, Optional import pytest +from prisma import Json +from scim2_models import Group as ScimGroup +from scim2_models import GroupMember as ScimGroupMember from litellm.proxy._types import LiteLLM_UserTable, UserAPIKeyAuth, hash_token from litellm.proxy.auth_v2.authorization import Role @@ -153,3 +156,73 @@ async def test_mtls_credential_resolves_to_service_account(): assert principal.principal_type == PrincipalType.SERVICE_ACCOUNT assert principal.user is None assert principal.subject == "CN=svc-a,O=Co" + + +class _FakeTeamTable: + """Minimal stand-in for the prisma team table. + + Mirrors the one prisma behavior the group methods depend on: a Json-wrapped + write is stored as its plain Python value and read back the same way. Storing + the raw value would let a regression that forgets the Json wrapper pass here + while failing against a real database. + """ + + def __init__(self) -> None: + self.rows: Dict[str, dict] = {} + + @staticmethod + def _norm(data: dict) -> dict: + return {k: (v.data if isinstance(v, Json) else v) for k, v in data.items()} + + async def find_unique(self, where): + return self.rows.get(where["team_id"]) + + async def create(self, data): + row = self._norm(data) + self.rows[row["team_id"]] = row + return row + + async def update(self, where, data): + self.rows[where["team_id"]].update(self._norm(data)) + return self.rows[where["team_id"]] + + async def find_many(self, **kwargs): + return list(self.rows.values()) + + async def delete(self, where): + return self.rows.pop(where["team_id"], None) + + +class _FakeTeamPrisma: + def __init__(self) -> None: + self.db = type("_Db", (), {"litellm_teamtable": _FakeTeamTable()})() + + +async def test_group_upsert_round_trips_members_through_json(): + store = DbIdentityStore(_FakeTeamPrisma(), _FakeCache()) + created = await store.upsert_group( + ScimGroup( + display_name="eng", + members=[ScimGroupMember(value="u-1"), ScimGroupMember(value="u-2")], + ) + ) + + assert created.id is not None + assert created.display_name == "eng" + assert [m.value for m in created.members] == ["u-1", "u-2"] + + fetched = await store.get_group(created.id) + assert fetched is not None + assert [m.value for m in fetched.members] == ["u-1", "u-2"] + + +async def test_group_list_and_delete(): + store = DbIdentityStore(_FakeTeamPrisma(), _FakeCache()) + a = await store.upsert_group(ScimGroup(display_name="a")) + await store.upsert_group(ScimGroup(display_name="b")) + + assert {g.display_name for g in await store.list_groups(None)} == {"a", "b"} + + await store.delete_group(a.id) + assert await store.get_group(a.id) is None + assert {g.display_name for g in await store.list_groups(None)} == {"b"} diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 8e470819557..110b87146e2 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -416,6 +416,43 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/teams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Teams */ + get: operations["list_teams_admin_teams_get"]; + put?: never; + /** Create Team */ + post: operations["create_team_admin_teams_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/teams/{team_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Team */ + get: operations["get_team_admin_teams__team_id__get"]; + /** Upsert Team */ + put: operations["upsert_team_admin_teams__team_id__put"]; + post?: never; + /** Delete Team */ + delete: operations["delete_team_admin_teams__team_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/agent/daily/activity": { parameters: { query?: never; @@ -30580,6 +30617,22 @@ export interface components { /** Team Id */ team_id: string; }; + /** TeamUpsert */ + TeamUpsert: { + /** Members */ + members?: string[]; + /** Name */ + name: string; + }; + /** TeamView */ + TeamView: { + /** Id */ + id: string; + /** Members */ + members: string[]; + /** Name */ + name: string; + }; /** * TestCustomCodeGuardrailRequest * @description Request model for testing custom code guardrails. @@ -33107,6 +33160,154 @@ export interface operations { }; }; }; + list_teams_admin_teams_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamView"][]; + }; + }; + }; + }; + create_team_admin_teams_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamUpsert"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamView"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_team_admin_teams__team_id__get: { + parameters: { + query?: never; + header?: never; + path: { + team_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamView"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + upsert_team_admin_teams__team_id__put: { + parameters: { + query?: never; + header?: never; + path: { + team_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamUpsert"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamView"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_team_admin_teams__team_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + team_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_agent_daily_activity_agent_daily_activity_get: { parameters: { query?: {