mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(auth_v2): make SCIM discovery public and return 404 on missing DELETE
RFC 7644 requires /ServiceProviderConfig, /ResourceTypes and /Schemas to be publicly readable; split them onto an unguarded router while Users and Groups stay behind scim:write. DELETE on a missing User or Group now returns a 404 SCIM Error instead of a misleading 204.
This commit is contained in:
parent
71a189bf65
commit
ca896ac073
1 changed files with 115 additions and 102 deletions
|
|
@ -63,108 +63,8 @@ def _dump(resource: Resource, ctx: Context) -> Dict[str, Any]:
|
||||||
return resource.model_dump(scim_ctx=ctx)
|
return resource.model_dump(scim_ctx=ctx)
|
||||||
|
|
||||||
|
|
||||||
def build_scim_router() -> APIRouter:
|
def _build_discovery_router() -> APIRouter:
|
||||||
router = APIRouter(
|
router = APIRouter()
|
||||||
prefix="/scim/v2",
|
|
||||||
tags=["scim"],
|
|
||||||
dependencies=[Security(get_current_principal, scopes=["scim:write"])],
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/Users", status_code=status.HTTP_201_CREATED)
|
|
||||||
async def create_user(request: Request) -> Response:
|
|
||||||
try:
|
|
||||||
user = await _parse(request, User)
|
|
||||||
except ValidationError as exc:
|
|
||||||
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
|
||||||
stored = await _store(request).upsert_user(user)
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_201_CREATED,
|
|
||||||
content=_dump(stored, Context.RESOURCE_CREATION_RESPONSE),
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/Users/{resource_id}")
|
|
||||||
async def get_user(resource_id: str, request: Request) -> Response:
|
|
||||||
user = await _store(request).get_user(resource_id)
|
|
||||||
if user is None:
|
|
||||||
return _error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
|
|
||||||
return JSONResponse(content=_dump(user, Context.RESOURCE_QUERY_RESPONSE))
|
|
||||||
|
|
||||||
@router.patch("/Users/{resource_id}")
|
|
||||||
async def patch_user(resource_id: str, request: Request) -> Response:
|
|
||||||
store = _store(request)
|
|
||||||
user = await store.get_user(resource_id)
|
|
||||||
if user is None:
|
|
||||||
return _error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
|
|
||||||
try:
|
|
||||||
patch = PatchOp[User].model_validate(await request.json())
|
|
||||||
except ValidationError as exc:
|
|
||||||
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
|
||||||
updated = await store.upsert_user(_apply_patch(user, patch))
|
|
||||||
return JSONResponse(content=_dump(updated, Context.RESOURCE_PATCH_RESPONSE))
|
|
||||||
|
|
||||||
@router.delete("/Users/{resource_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
async def deactivate_user(resource_id: str, request: Request) -> Response:
|
|
||||||
await _store(request).deactivate_user(resource_id)
|
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
@router.get("/Users")
|
|
||||||
async def list_users(request: Request, filter: Optional[str] = None) -> Response:
|
|
||||||
users = await _store(request).list_users(filter)
|
|
||||||
listing: ListResponse[User] = ListResponse[User](
|
|
||||||
total_results=len(users),
|
|
||||||
start_index=1,
|
|
||||||
items_per_page=len(users),
|
|
||||||
resources=users or None,
|
|
||||||
)
|
|
||||||
return JSONResponse(content=_dump(listing, Context.RESOURCE_QUERY_RESPONSE))
|
|
||||||
|
|
||||||
@router.post("/Groups", status_code=status.HTTP_201_CREATED)
|
|
||||||
async def create_group(request: Request) -> Response:
|
|
||||||
try:
|
|
||||||
group = await _parse(request, Group)
|
|
||||||
except ValidationError as exc:
|
|
||||||
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
|
||||||
stored = await _store(request).upsert_group(group)
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_201_CREATED,
|
|
||||||
content=_dump(stored, Context.RESOURCE_CREATION_RESPONSE),
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/Groups/{resource_id}")
|
|
||||||
async def get_group(resource_id: str, request: Request) -> Response:
|
|
||||||
group = await _store(request).get_group(resource_id)
|
|
||||||
if group is None:
|
|
||||||
return _error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
|
|
||||||
return JSONResponse(content=_dump(group, Context.RESOURCE_QUERY_RESPONSE))
|
|
||||||
|
|
||||||
@router.patch("/Groups/{resource_id}")
|
|
||||||
async def patch_group(resource_id: str, request: Request) -> Response:
|
|
||||||
store = _store(request)
|
|
||||||
group = await store.get_group(resource_id)
|
|
||||||
if group is None:
|
|
||||||
return _error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
|
|
||||||
try:
|
|
||||||
patch = PatchOp[Group].model_validate(await request.json())
|
|
||||||
except ValidationError as exc:
|
|
||||||
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
|
||||||
updated = await store.upsert_group(_apply_patch(group, patch))
|
|
||||||
return JSONResponse(content=_dump(updated, Context.RESOURCE_PATCH_RESPONSE))
|
|
||||||
|
|
||||||
@router.delete("/Groups/{resource_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
async def delete_group(resource_id: str, request: Request) -> Response:
|
|
||||||
await _store(request).delete_group(resource_id)
|
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
@router.get("/Groups")
|
|
||||||
async def list_groups(request: Request, filter: Optional[str] = None) -> Response:
|
|
||||||
groups = await _store(request).list_groups(filter)
|
|
||||||
listing: ListResponse[Group] = ListResponse[Group](
|
|
||||||
total_results=len(groups),
|
|
||||||
start_index=1,
|
|
||||||
items_per_page=len(groups),
|
|
||||||
resources=groups or None,
|
|
||||||
)
|
|
||||||
return JSONResponse(content=_dump(listing, Context.RESOURCE_QUERY_RESPONSE))
|
|
||||||
|
|
||||||
@router.get("/ServiceProviderConfig")
|
@router.get("/ServiceProviderConfig")
|
||||||
async def service_provider_config() -> Response:
|
async def service_provider_config() -> Response:
|
||||||
|
|
@ -219,3 +119,116 @@ def build_scim_router() -> APIRouter:
|
||||||
)
|
)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
def build_scim_router() -> APIRouter:
|
||||||
|
router = APIRouter(prefix="/scim/v2", tags=["scim"])
|
||||||
|
protected = APIRouter(
|
||||||
|
dependencies=[Security(get_current_principal, scopes=["scim:write"])],
|
||||||
|
)
|
||||||
|
|
||||||
|
@protected.post("/Users", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_user(request: Request) -> Response:
|
||||||
|
try:
|
||||||
|
user = await _parse(request, User)
|
||||||
|
except ValidationError as exc:
|
||||||
|
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
||||||
|
stored = await _store(request).upsert_user(user)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
content=_dump(stored, Context.RESOURCE_CREATION_RESPONSE),
|
||||||
|
)
|
||||||
|
|
||||||
|
@protected.get("/Users/{resource_id}")
|
||||||
|
async def get_user(resource_id: str, request: Request) -> Response:
|
||||||
|
user = await _store(request).get_user(resource_id)
|
||||||
|
if user is None:
|
||||||
|
return _error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
|
||||||
|
return JSONResponse(content=_dump(user, Context.RESOURCE_QUERY_RESPONSE))
|
||||||
|
|
||||||
|
@protected.patch("/Users/{resource_id}")
|
||||||
|
async def patch_user(resource_id: str, request: Request) -> Response:
|
||||||
|
store = _store(request)
|
||||||
|
user = await store.get_user(resource_id)
|
||||||
|
if user is None:
|
||||||
|
return _error(status.HTTP_404_NOT_FOUND, f"User {resource_id} not found")
|
||||||
|
try:
|
||||||
|
patch = PatchOp[User].model_validate(await request.json())
|
||||||
|
except ValidationError as exc:
|
||||||
|
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
||||||
|
updated = await store.upsert_user(_apply_patch(user, patch))
|
||||||
|
return JSONResponse(content=_dump(updated, Context.RESOURCE_PATCH_RESPONSE))
|
||||||
|
|
||||||
|
@protected.delete("/Users/{resource_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def deactivate_user(resource_id: str, request: Request) -> Response:
|
||||||
|
store = _store(request)
|
||||||
|
if await store.get_user(resource_id) is None:
|
||||||
|
return _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)
|
||||||
|
|
||||||
|
@protected.get("/Users")
|
||||||
|
async def list_users(request: Request, filter: Optional[str] = None) -> Response:
|
||||||
|
users = await _store(request).list_users(filter)
|
||||||
|
listing: ListResponse[User] = ListResponse[User](
|
||||||
|
total_results=len(users),
|
||||||
|
start_index=1,
|
||||||
|
items_per_page=len(users),
|
||||||
|
resources=users or None,
|
||||||
|
)
|
||||||
|
return JSONResponse(content=_dump(listing, Context.RESOURCE_QUERY_RESPONSE))
|
||||||
|
|
||||||
|
@protected.post("/Groups", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_group(request: Request) -> Response:
|
||||||
|
try:
|
||||||
|
group = await _parse(request, Group)
|
||||||
|
except ValidationError as exc:
|
||||||
|
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
||||||
|
stored = await _store(request).upsert_group(group)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
content=_dump(stored, Context.RESOURCE_CREATION_RESPONSE),
|
||||||
|
)
|
||||||
|
|
||||||
|
@protected.get("/Groups/{resource_id}")
|
||||||
|
async def get_group(resource_id: str, request: Request) -> Response:
|
||||||
|
group = await _store(request).get_group(resource_id)
|
||||||
|
if group is None:
|
||||||
|
return _error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
|
||||||
|
return JSONResponse(content=_dump(group, Context.RESOURCE_QUERY_RESPONSE))
|
||||||
|
|
||||||
|
@protected.patch("/Groups/{resource_id}")
|
||||||
|
async def patch_group(resource_id: str, request: Request) -> Response:
|
||||||
|
store = _store(request)
|
||||||
|
group = await store.get_group(resource_id)
|
||||||
|
if group is None:
|
||||||
|
return _error(status.HTTP_404_NOT_FOUND, f"Group {resource_id} not found")
|
||||||
|
try:
|
||||||
|
patch = PatchOp[Group].model_validate(await request.json())
|
||||||
|
except ValidationError as exc:
|
||||||
|
return _error(status.HTTP_400_BAD_REQUEST, str(exc))
|
||||||
|
updated = await store.upsert_group(_apply_patch(group, patch))
|
||||||
|
return JSONResponse(content=_dump(updated, Context.RESOURCE_PATCH_RESPONSE))
|
||||||
|
|
||||||
|
@protected.delete("/Groups/{resource_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_group(resource_id: str, request: Request) -> Response:
|
||||||
|
store = _store(request)
|
||||||
|
if await store.get_group(resource_id) is None:
|
||||||
|
return _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)
|
||||||
|
|
||||||
|
@protected.get("/Groups")
|
||||||
|
async def list_groups(request: Request, filter: Optional[str] = None) -> Response:
|
||||||
|
groups = await _store(request).list_groups(filter)
|
||||||
|
listing: ListResponse[Group] = ListResponse[Group](
|
||||||
|
total_results=len(groups),
|
||||||
|
start_index=1,
|
||||||
|
items_per_page=len(groups),
|
||||||
|
resources=groups or None,
|
||||||
|
)
|
||||||
|
return JSONResponse(content=_dump(listing, Context.RESOURCE_QUERY_RESPONSE))
|
||||||
|
|
||||||
|
router.include_router(protected)
|
||||||
|
router.include_router(_build_discovery_router())
|
||||||
|
return router
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue