litellm/backend/auth/routers/scim.py
2026-06-11 16:27:52 -07:00

143 lines
5.3 KiB
Python

from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, Query, Request, Response, status
from fastapi.responses import JSONResponse
from pydantic import ValidationError
from scim2_models import Group, User
from ..services import scim
from .dependencies import scim_principal, scim_store
router = APIRouter(prefix="/scim/v2", tags=["scim"], route_class=scim.ScimErrorRoute)
_protected = [Depends(scim_principal)]
@router.post("/Users", status_code=status.HTTP_201_CREATED, dependencies=_protected)
async def create_user(request: Request) -> Response:
try:
user = scim.parse_resource(await request.json(), User)
except ValidationError as exc:
return scim.scim_error(status.HTTP_400_BAD_REQUEST, str(exc))
stored = await scim_store(request).upsert_user(user)
return JSONResponse(
status_code=status.HTTP_201_CREATED,
content=scim.creation_response(stored),
)
@router.get("/Users/{resource_id}", dependencies=_protected)
async def get_user(resource_id: str, request: Request) -> Response:
user = await scim_store(request).get_user(resource_id)
if user is None:
return scim.scim_error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
return JSONResponse(content=scim.query_response(user))
@router.patch("/Users/{resource_id}", dependencies=_protected)
async def patch_user(resource_id: str, request: Request) -> Response:
store = scim_store(request)
user = await store.get_user(resource_id)
if user is None:
return scim.scim_error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
try:
patched = scim.apply_patch(user, await request.json())
except (ValidationError, ValueError) as exc:
return scim.scim_error(status.HTTP_400_BAD_REQUEST, str(exc))
updated = await store.upsert_user(patched)
return JSONResponse(content=scim.patch_response(updated))
@router.delete(
"/Users/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=_protected,
)
async def deactivate_user(resource_id: str, request: Request) -> Response:
store = scim_store(request)
if await store.get_user(resource_id) is None:
return scim.scim_error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
await store.deactivate_user(resource_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/Users", dependencies=_protected)
async def list_users(
request: Request,
filter_expr: Optional[str] = Query(default=None, alias="filter"),
) -> Response:
users = await scim_store(request).list_users(filter_expr)
return JSONResponse(content=scim.list_response(User, users))
@router.post("/Groups", status_code=status.HTTP_201_CREATED, dependencies=_protected)
async def create_group(request: Request) -> Response:
try:
group = scim.parse_resource(await request.json(), Group)
except ValidationError as exc:
return scim.scim_error(status.HTTP_400_BAD_REQUEST, str(exc))
stored = await scim_store(request).upsert_group(group)
return JSONResponse(
status_code=status.HTTP_201_CREATED,
content=scim.creation_response(stored),
)
@router.get("/Groups/{resource_id}", dependencies=_protected)
async def get_group(resource_id: str, request: Request) -> Response:
group = await scim_store(request).get_group(resource_id)
if group is None:
return scim.scim_error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
return JSONResponse(content=scim.query_response(group))
@router.patch("/Groups/{resource_id}", dependencies=_protected)
async def patch_group(resource_id: str, request: Request) -> Response:
store = scim_store(request)
group = await store.get_group(resource_id)
if group is None:
return scim.scim_error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
try:
patched = scim.apply_patch(group, await request.json())
except (ValidationError, ValueError) as exc:
return scim.scim_error(status.HTTP_400_BAD_REQUEST, str(exc))
updated = await store.upsert_group(patched)
return JSONResponse(content=scim.patch_response(updated))
@router.delete(
"/Groups/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=_protected,
)
async def delete_group(resource_id: str, request: Request) -> Response:
store = scim_store(request)
if await store.get_group(resource_id) is None:
return scim.scim_error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
await store.delete_group(resource_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/Groups", dependencies=_protected)
async def list_groups(
request: Request,
filter_expr: Optional[str] = Query(default=None, alias="filter"),
) -> Response:
groups = await scim_store(request).list_groups(filter_expr)
return JSONResponse(content=scim.list_response(Group, groups))
@router.get("/ServiceProviderConfig")
async def service_provider_config() -> Response:
return JSONResponse(content=scim.service_provider_config())
@router.get("/ResourceTypes")
async def resource_types() -> Response:
return JSONResponse(content=scim.resource_types())
@router.get("/Schemas")
async def schemas() -> Response:
return JSONResponse(content=scim.schemas())