11import json
22from http import HTTPStatus
3+ from typing import Annotated
34
45from fastapi import APIRouter
56from fastapi import Depends
1718from scim2_models import ResponseParameters
1819from scim2_models import Schema
1920from scim2_models import SCIMException
21+ from scim2_models import SCIMSerializer
22+ from scim2_models import ServiceProviderConfig
23+ from scim2_models import SCIMValidator
2024from scim2_models import SearchRequest
2125from scim2_models import User
2226
@@ -42,11 +46,16 @@ class SCIMResponse(Response):
4246
4347 media_type = "application/scim+json"
4448
45- def __init__ (self , content : str , ** kwargs ):
49+ def __init__ (self , content = None , ** kwargs ):
50+ if isinstance (content , (dict , list )):
51+ content = json .dumps (content , ensure_ascii = False )
4652 super ().__init__ (content = content , ** kwargs )
47- meta = json .loads (content ).get ("meta" , {})
48- if version := meta .get ("version" ):
49- self .headers ["ETag" ] = version
53+ try :
54+ meta = json .loads (content ).get ("meta" , {})
55+ if version := meta .get ("version" ):
56+ self .headers ["ETag" ] = version
57+ except (json .JSONDecodeError , AttributeError , TypeError ):
58+ pass
5059
5160
5261router = APIRouter (prefix = "/scim/v2" , default_response_class = SCIMResponse )
@@ -137,47 +146,44 @@ async def get_user(request: Request, app_record: dict = Depends(resolve_user)):
137146
138147# -- patch-user-start --
139148@router .patch ("/Users/{user_id}" )
140- async def patch_user (request : Request , app_record : dict = Depends (resolve_user )):
149+ async def patch_user (
150+ request : Request ,
151+ patch : Annotated [
152+ PatchOp [User ], SCIMValidator (Context .RESOURCE_PATCH_REQUEST )
153+ ],
154+ app_record : dict = Depends (resolve_user ),
155+ ) -> Annotated [User , SCIMSerializer (Context .RESOURCE_PATCH_RESPONSE )]:
141156 """Apply a SCIM PatchOp to an existing user."""
142157 check_etag (app_record , request )
143158 scim_user = to_scim_user (app_record , resource_location (request , app_record ))
144- patch = PatchOp [User ].model_validate (
145- await request .json (),
146- scim_ctx = Context .RESOURCE_PATCH_REQUEST ,
147- )
148159 patch .patch (scim_user )
149160
150161 updated_record = from_scim_user (scim_user )
151162 save_record (updated_record )
152163
153- return SCIMResponse (
154- scim_user .model_dump_json (scim_ctx = Context .RESOURCE_PATCH_RESPONSE ),
155- )
164+ return to_scim_user (updated_record , resource_location (request , updated_record ))
156165# -- patch-user-end --
157166
158167
159168# -- put-user-start --
160169@router .put ("/Users/{user_id}" )
161- async def replace_user (request : Request , app_record : dict = Depends (resolve_user )):
170+ async def replace_user (
171+ request : Request ,
172+ replacement : Annotated [
173+ User , SCIMValidator (Context .RESOURCE_REPLACEMENT_REQUEST )
174+ ],
175+ app_record : dict = Depends (resolve_user ),
176+ ) -> Annotated [User , SCIMSerializer (Context .RESOURCE_REPLACEMENT_RESPONSE )]:
162177 """Replace an existing user with a full SCIM resource."""
163178 check_etag (app_record , request )
164179 existing_user = to_scim_user (app_record , resource_location (request , app_record ))
165- replacement = User .model_validate (
166- await request .json (),
167- scim_ctx = Context .RESOURCE_REPLACEMENT_REQUEST ,
168- )
169180 replacement .replace (existing_user )
170181
171182 replacement .id = existing_user .id
172183 updated_record = from_scim_user (replacement )
173184 save_record (updated_record )
174185
175- response_user = to_scim_user (
176- updated_record , resource_location (request , updated_record )
177- )
178- return SCIMResponse (
179- response_user .model_dump_json (scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE ),
180- )
186+ return to_scim_user (updated_record , resource_location (request , updated_record ))
181187# -- put-user-end --
182188
183189
@@ -219,21 +225,18 @@ async def list_users(request: Request):
219225
220226
221227# -- create-user-start --
222- @router .post ("/Users" )
223- async def create_user (request : Request ):
228+ @router .post ("/Users" , status_code = HTTPStatus .CREATED )
229+ async def create_user (
230+ request : Request ,
231+ request_user : Annotated [
232+ User , SCIMValidator (Context .RESOURCE_CREATION_REQUEST )
233+ ],
234+ ) -> Annotated [User , SCIMSerializer (Context .RESOURCE_CREATION_RESPONSE )]:
224235 """Validate a SCIM creation payload and store the new user."""
225- request_user = User .model_validate (
226- await request .json (),
227- scim_ctx = Context .RESOURCE_CREATION_REQUEST ,
228- )
229236 app_record = from_scim_user (request_user )
230237 save_record (app_record )
231238
232- response_user = to_scim_user (app_record , resource_location (request , app_record ))
233- return SCIMResponse (
234- response_user .model_dump_json (scim_ctx = Context .RESOURCE_CREATION_RESPONSE ),
235- status_code = HTTPStatus .CREATED ,
236- )
239+ return to_scim_user (app_record , resource_location (request , app_record ))
237240# -- create-user-end --
238241# -- collection-end --
239242
@@ -305,13 +308,11 @@ async def get_resource_type_by_id(resource_type_id: str):
305308
306309# -- service-provider-config-start --
307310@router .get ("/ServiceProviderConfig" )
308- async def get_service_provider_config ():
311+ async def get_service_provider_config () -> Annotated [
312+ ServiceProviderConfig , SCIMSerializer (Context .RESOURCE_QUERY_RESPONSE )
313+ ]:
309314 """Return the SCIM service provider configuration."""
310- return SCIMResponse (
311- service_provider_config .model_dump_json (
312- scim_ctx = Context .RESOURCE_QUERY_RESPONSE
313- ),
314- )
315+ return service_provider_config
315316# -- service-provider-config-end --
316317# -- discovery-end --
317318
0 commit comments