mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
Move the admin surface toward explicit, backend-owned routers instead of trimming the proxy app by path allowlist. This adds a backend/routers package whose teams router is the source of truth for /admin/teams CRUD plus membership; proxy_server imports and mounts it (guarded by the backend package being importable, since the pip wheel ships only litellm), and backend/main keeps those explicit routes regardless of the allowlist. Every route authenticates through the auth_v2 AuthSecurity stored on app.state.auth_v2 (a require_roles gate over the Principal) rather than the legacy user_api_key_auth dependency; app.state.auth_v2 is wired in the proxy startup once the DB is connected. Two resolver fixes were needed to make the DB-backed path actually work, since it was previously only exercised against an in-memory store: API-key principals now resolve their platform role from the owning user (get_key_object does not join user_role onto the token), and team group writes wrap members_with_roles in prisma Json so upsert_group persists. db_team_to_scim now carries members so team membership round-trips on read.
89 lines
2.7 KiB
Python
89 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
|
from pydantic import BaseModel, Field
|
|
from scim2_models import Group as ScimGroup
|
|
from scim2_models import GroupMember
|
|
|
|
from litellm.proxy.auth_v2 import Role
|
|
|
|
from .dependencies import require_roles, team_store
|
|
|
|
ADMIN_PREFIX = "/admin"
|
|
|
|
router = APIRouter(prefix=f"{ADMIN_PREFIX}/teams", tags=["admin"])
|
|
|
|
_team_admin = require_roles(Role.ORG_ADMIN, Role.PLATFORM_ADMIN)
|
|
_protected = [Depends(_team_admin)]
|
|
|
|
|
|
class TeamUpsert(BaseModel):
|
|
name: str
|
|
members: List[str] = Field(default_factory=list)
|
|
|
|
|
|
class TeamView(BaseModel):
|
|
id: str
|
|
name: str
|
|
members: List[str]
|
|
|
|
|
|
def _to_scim(team_id: Optional[str], body: TeamUpsert) -> ScimGroup:
|
|
group = ScimGroup(
|
|
display_name=body.name,
|
|
members=[GroupMember(value=user_id) for user_id in body.members] or None,
|
|
)
|
|
if team_id is not None:
|
|
group.id = team_id
|
|
return group
|
|
|
|
|
|
def _to_view(group: ScimGroup) -> TeamView:
|
|
return TeamView(
|
|
id=group.id,
|
|
name=group.display_name,
|
|
members=[m.value for m in (group.members or []) if m.value],
|
|
)
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED, dependencies=_protected)
|
|
async def create_team(body: TeamUpsert, request: Request) -> TeamView:
|
|
stored = await team_store(request).upsert_group(_to_scim(None, body))
|
|
return _to_view(stored)
|
|
|
|
|
|
@router.get("", dependencies=_protected)
|
|
async def list_teams(request: Request) -> List[TeamView]:
|
|
groups = await team_store(request).list_groups(None)
|
|
return [_to_view(group) for group in groups]
|
|
|
|
|
|
@router.get("/{team_id}", dependencies=_protected)
|
|
async def get_team(team_id: str, request: Request) -> TeamView:
|
|
group = await team_store(request).get_group(team_id)
|
|
if group is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=f"team {team_id} not found"
|
|
)
|
|
return _to_view(group)
|
|
|
|
|
|
@router.put("/{team_id}", dependencies=_protected)
|
|
async def upsert_team(team_id: str, body: TeamUpsert, request: Request) -> TeamView:
|
|
stored = await team_store(request).upsert_group(_to_scim(team_id, body))
|
|
return _to_view(stored)
|
|
|
|
|
|
@router.delete(
|
|
"/{team_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=_protected
|
|
)
|
|
async def delete_team(team_id: str, request: Request) -> Response:
|
|
store = team_store(request)
|
|
if await store.get_group(team_id) is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=f"team {team_id} not found"
|
|
)
|
|
await store.delete_group(team_id)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|