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)
|
||||
|
||||
|
||||
def build_scim_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))
|
||||
def _build_discovery_router() -> APIRouter:
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/ServiceProviderConfig")
|
||||
async def service_provider_config() -> Response:
|
||||
|
|
@ -219,3 +119,116 @@ def build_scim_router() -> APIRouter:
|
|||
)
|
||||
|
||||
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