1+ import json
12from http import HTTPStatus
23
34from fastapi import APIRouter
3435
3536# -- setup-start --
3637app = FastAPI ()
37- router = APIRouter (prefix = "/scim/v2" )
3838
3939
40- @app .middleware ("http" )
41- async def add_scim_content_type (request : Request , call_next ):
42- """Set the SCIM media type on every response."""
43- response = await call_next (request )
44- response .headers ["Content-Type" ] = "application/scim+json"
45- return response
40+ class SCIMResponse (Response ):
41+ """SCIM JSON response that auto-extracts the ``ETag`` from ``meta.version``."""
42+
43+ media_type = "application/scim+json"
44+
45+ def __init__ (self , content : str , ** kwargs ):
46+ super ().__init__ (content = content , ** kwargs )
47+ meta = json .loads (content ).get ("meta" , {})
48+ if version := meta .get ("version" ):
49+ self .headers ["ETag" ] = version
50+
51+
52+ router = APIRouter (prefix = "/scim/v2" , default_response_class = SCIMResponse )
4653
4754
4855def resource_location (request , app_record ):
@@ -87,21 +94,21 @@ def resolve_user(user_id: str):
8794async def handle_validation_error (request , error ):
8895 """Turn Pydantic validation errors into SCIM error responses."""
8996 scim_error = Error .from_validation_error (error .errors ()[0 ])
90- return Response (scim_error .model_dump_json (), status_code = scim_error .status )
97+ return SCIMResponse (scim_error .model_dump_json (), status_code = scim_error .status )
9198
9299
93100@app .exception_handler (HTTPException )
94101async def handle_http_exception (request , error ):
95102 """Turn HTTP exceptions into SCIM error responses."""
96103 scim_error = Error (status = error .status_code , detail = error .detail or "" )
97- return Response (scim_error .model_dump_json (), status_code = error .status_code )
104+ return SCIMResponse (scim_error .model_dump_json (), status_code = error .status_code )
98105
99106
100107@app .exception_handler (SCIMException )
101108async def handle_scim_error (request , error ):
102109 """Turn SCIM exceptions into SCIM error responses."""
103110 scim_error = error .to_error ()
104- return Response (scim_error .model_dump_json (), status_code = scim_error .status )
111+ return SCIMResponse (scim_error .model_dump_json (), status_code = scim_error .status )
105112# -- error-handlers-end --
106113# -- refinements-end --
107114
@@ -118,13 +125,12 @@ async def get_user(request: Request, app_record: dict = Depends(resolve_user)):
118125 if_none_match = request .headers .get ("If-None-Match" )
119126 if if_none_match and etag in [t .strip () for t in if_none_match .split ("," )]:
120127 return Response (status_code = HTTPStatus .NOT_MODIFIED )
121- return Response (
128+ return SCIMResponse (
122129 scim_user .model_dump_json (
123130 scim_ctx = Context .RESOURCE_QUERY_RESPONSE ,
124131 attributes = req .attributes ,
125132 excluded_attributes = req .excluded_attributes ,
126133 ),
127- headers = {"ETag" : etag },
128134 )
129135# -- get-user-end --
130136
@@ -144,9 +150,8 @@ async def patch_user(request: Request, app_record: dict = Depends(resolve_user))
144150 updated_record = from_scim_user (scim_user )
145151 save_record (updated_record )
146152
147- return Response (
153+ return SCIMResponse (
148154 scim_user .model_dump_json (scim_ctx = Context .RESOURCE_PATCH_RESPONSE ),
149- headers = {"ETag" : make_etag (updated_record )},
150155 )
151156# -- patch-user-end --
152157
@@ -170,9 +175,8 @@ async def replace_user(request: Request, app_record: dict = Depends(resolve_user
170175 response_user = to_scim_user (
171176 updated_record , resource_location (request , updated_record )
172177 )
173- return Response (
178+ return SCIMResponse (
174179 response_user .model_dump_json (scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE ),
175- headers = {"ETag" : make_etag (updated_record )},
176180 )
177181# -- put-user-end --
178182
@@ -204,7 +208,7 @@ async def list_users(request: Request):
204208 items_per_page = len (resources ),
205209 resources = resources ,
206210 )
207- return Response (
211+ return SCIMResponse (
208212 response .model_dump_json (
209213 scim_ctx = Context .RESOURCE_QUERY_RESPONSE ,
210214 attributes = req .attributes ,
@@ -226,10 +230,9 @@ async def create_user(request: Request):
226230 save_record (app_record )
227231
228232 response_user = to_scim_user (app_record , resource_location (request , app_record ))
229- return Response (
233+ return SCIMResponse (
230234 response_user .model_dump_json (scim_ctx = Context .RESOURCE_CREATION_RESPONSE ),
231235 status_code = HTTPStatus .CREATED ,
232- headers = {"ETag" : make_etag (app_record )},
233236 )
234237# -- create-user-end --
235238# -- collection-end --
@@ -248,7 +251,7 @@ async def list_schemas(request: Request):
248251 items_per_page = len (page ),
249252 resources = page ,
250253 )
251- return Response (
254+ return SCIMResponse (
252255 response .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
253256 )
254257
@@ -260,8 +263,8 @@ async def get_schema_by_id(schema_id: str):
260263 schema = get_schema (schema_id )
261264 except KeyError :
262265 scim_error = Error (status = 404 , detail = f"Schema { schema_id !r} not found" )
263- return Response (scim_error .model_dump_json (), status_code = HTTPStatus .NOT_FOUND )
264- return Response (
266+ return SCIMResponse (scim_error .model_dump_json (), status_code = HTTPStatus .NOT_FOUND )
267+ return SCIMResponse (
265268 schema .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
266269 )
267270# -- schemas-end --
@@ -279,7 +282,7 @@ async def list_resource_types(request: Request):
279282 items_per_page = len (page ),
280283 resources = page ,
281284 )
282- return Response (
285+ return SCIMResponse (
283286 response .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
284287 )
285288
@@ -293,8 +296,8 @@ async def get_resource_type_by_id(resource_type_id: str):
293296 scim_error = Error (
294297 status = 404 , detail = f"ResourceType { resource_type_id !r} not found"
295298 )
296- return Response (scim_error .model_dump_json (), status_code = HTTPStatus .NOT_FOUND )
297- return Response (
299+ return SCIMResponse (scim_error .model_dump_json (), status_code = HTTPStatus .NOT_FOUND )
300+ return SCIMResponse (
298301 rt .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
299302 )
300303# -- resource-types-end --
@@ -304,7 +307,7 @@ async def get_resource_type_by_id(resource_type_id: str):
304307@router .get ("/ServiceProviderConfig" )
305308async def get_service_provider_config ():
306309 """Return the SCIM service provider configuration."""
307- return Response (
310+ return SCIMResponse (
308311 service_provider_config .model_dump_json (
309312 scim_ctx = Context .RESOURCE_QUERY_RESPONSE
310313 ),
0 commit comments