11from http import HTTPStatus
22
33from flask import Blueprint
4- from flask import make_response
54from flask import request
65from flask import url_for
76from pydantic import ValidationError
3938
4039
4140@bp .after_request
42- def add_scim_content_type (response ):
43- """Expose every endpoint with the SCIM media type."""
41+ def scim_after_request (response ):
42+ """Set the SCIM media type, extract ETag, and handle conditional responses ."""
4443 response .headers ["Content-Type" ] = "application/scim+json"
44+ data = response .get_json (silent = True )
45+ if meta := (data or {}).get ("meta" ):
46+ if version := meta .get ("version" ):
47+ response .headers ["ETag" ] = version
48+ response .make_conditional (request )
4549 return response
4650
4751
@@ -52,18 +56,23 @@ def resource_location(app_record):
5256
5357
5458# -- etag-start --
55- def check_etag (record ):
56- """Compare the record's ETag against the ``If-Match`` request header.
59+ @bp .before_request
60+ def check_etag ():
61+ """Verify ``If-Match`` on write operations.
5762
58- :param record: The application record.
5963 :raises ~werkzeug.exceptions.PreconditionFailed: If the header is present and does not match.
6064 """
65+ if request .method not in ("PUT" , "PATCH" , "DELETE" ):
66+ return
67+ app_record = request .view_args .get ("app_record" )
68+ if app_record is None :
69+ return
6170 if_match = request .headers .get ("If-Match" )
6271 if not if_match :
6372 return
6473 if if_match .strip () == "*" :
6574 return
66- etag = make_etag (record )
75+ etag = make_etag (app_record )
6776 tags = [t .strip () for t in if_match .split ("," )]
6877 if etag not in tags :
6978 raise PreconditionFailed ("ETag mismatch" )
@@ -96,21 +105,21 @@ def _register_converter(state):
96105def handle_validation_error (error ):
97106 """Turn Pydantic validation errors into SCIM error responses."""
98107 scim_error = Error .from_validation_error (error .errors ()[0 ])
99- return scim_error .model_dump_json (), scim_error .status
108+ return scim_error .model_dump (), scim_error .status
100109
101110
102111@bp .errorhandler (HTTPException )
103112def handle_http_error (error ):
104113 """Turn HTTP errors into SCIM error responses."""
105114 scim_error = Error (status = error .code , detail = str (error .description ))
106- return scim_error .model_dump_json (), error .code
115+ return scim_error .model_dump (), error .code
107116
108117
109118@bp .errorhandler (SCIMException )
110119def handle_scim_error (error ):
111120 """Turn SCIM exceptions into SCIM error responses."""
112121 scim_error = error .to_error ()
113- return scim_error .model_dump_json (), scim_error .status
122+ return scim_error .model_dump (), scim_error .status
114123# -- error-handlers-end --
115124# -- refinements-end --
116125
@@ -123,24 +132,18 @@ def get_user(app_record):
123132 """Return one SCIM user."""
124133 req = ResponseParameters .model_validate (request .args .to_dict ())
125134 scim_user = to_scim_user (app_record , resource_location (app_record ))
126- resp = make_response (
127- scim_user .model_dump_json (
128- scim_ctx = Context .RESOURCE_QUERY_RESPONSE ,
129- attributes = req .attributes ,
130- excluded_attributes = req .excluded_attributes ,
131- )
135+ return scim_user .model_dump (
136+ scim_ctx = Context .RESOURCE_QUERY_RESPONSE ,
137+ attributes = req .attributes ,
138+ excluded_attributes = req .excluded_attributes ,
132139 )
133- resp .headers ["ETag" ] = make_etag (app_record )
134- resp .make_conditional (request )
135- return resp
136140# -- get-user-end --
137141
138142
139143# -- patch-user-start --
140144@bp .patch ("/Users/<user:app_record>" )
141145def patch_user (app_record ):
142146 """Apply a SCIM PatchOp to an existing user."""
143- check_etag (app_record )
144147 req = ResponseParameters .model_validate (request .args .to_dict ())
145148 scim_user = to_scim_user (app_record , resource_location (app_record ))
146149 patch = PatchOp [User ].model_validate (
@@ -152,14 +155,10 @@ def patch_user(app_record):
152155 updated_record = from_scim_user (scim_user )
153156 save_record (updated_record )
154157
155- return (
156- scim_user .model_dump_json (
157- scim_ctx = Context .RESOURCE_PATCH_RESPONSE ,
158- attributes = req .attributes ,
159- excluded_attributes = req .excluded_attributes ,
160- ),
161- HTTPStatus .OK ,
162- {"ETag" : make_etag (updated_record )},
158+ return scim_user .model_dump (
159+ scim_ctx = Context .RESOURCE_PATCH_RESPONSE ,
160+ attributes = req .attributes ,
161+ excluded_attributes = req .excluded_attributes ,
163162 )
164163# -- patch-user-end --
165164
@@ -168,7 +167,6 @@ def patch_user(app_record):
168167@bp .put ("/Users/<user:app_record>" )
169168def replace_user (app_record ):
170169 """Replace an existing user with a full SCIM resource."""
171- check_etag (app_record )
172170 req = ResponseParameters .model_validate (request .args .to_dict ())
173171 existing_user = to_scim_user (app_record , resource_location (app_record ))
174172 replacement = User .model_validate (
@@ -181,14 +179,10 @@ def replace_user(app_record):
181179 save_record (updated_record )
182180
183181 response_user = to_scim_user (updated_record , resource_location (updated_record ))
184- return (
185- response_user .model_dump_json (
186- scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE ,
187- attributes = req .attributes ,
188- excluded_attributes = req .excluded_attributes ,
189- ),
190- HTTPStatus .OK ,
191- {"ETag" : make_etag (updated_record )},
182+ return response_user .model_dump (
183+ scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE ,
184+ attributes = req .attributes ,
185+ excluded_attributes = req .excluded_attributes ,
192186 )
193187# -- put-user-end --
194188
@@ -197,7 +191,6 @@ def replace_user(app_record):
197191@bp .delete ("/Users/<user:app_record>" )
198192def delete_user (app_record ):
199193 """Delete an existing user."""
200- check_etag (app_record )
201194 delete_record (app_record ["id" ])
202195 return "" , HTTPStatus .NO_CONTENT
203196# -- delete-user-end --
@@ -218,13 +211,10 @@ def list_users():
218211 items_per_page = len (resources ),
219212 resources = resources ,
220213 )
221- return (
222- response .model_dump_json (
223- scim_ctx = Context .RESOURCE_QUERY_RESPONSE ,
224- attributes = req .attributes ,
225- excluded_attributes = req .excluded_attributes ,
226- ),
227- HTTPStatus .OK ,
214+ return response .model_dump (
215+ scim_ctx = Context .RESOURCE_QUERY_RESPONSE ,
216+ attributes = req .attributes ,
217+ excluded_attributes = req .excluded_attributes ,
228218 )
229219# -- list-users-end --
230220
@@ -243,13 +233,12 @@ def create_user():
243233
244234 response_user = to_scim_user (app_record , resource_location (app_record ))
245235 return (
246- response_user .model_dump_json (
236+ response_user .model_dump (
247237 scim_ctx = Context .RESOURCE_CREATION_RESPONSE ,
248238 attributes = req .attributes ,
249239 excluded_attributes = req .excluded_attributes ,
250240 ),
251241 HTTPStatus .CREATED ,
252- {"ETag" : make_etag (app_record )},
253242 )
254243# -- create-user-end --
255244# -- collection-end --
@@ -268,10 +257,7 @@ def list_schemas():
268257 items_per_page = len (page ),
269258 resources = page ,
270259 )
271- return (
272- response .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
273- HTTPStatus .OK ,
274- )
260+ return response .model_dump (scim_ctx = Context .RESOURCE_QUERY_RESPONSE )
275261
276262
277263@bp .get ("/Schemas/<path:schema_id>" )
@@ -281,11 +267,8 @@ def get_schema_by_id(schema_id):
281267 schema = get_schema (schema_id )
282268 except KeyError :
283269 scim_error = Error (status = 404 , detail = f"Schema { schema_id !r} not found" )
284- return scim_error .model_dump_json (), HTTPStatus .NOT_FOUND
285- return (
286- schema .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
287- HTTPStatus .OK ,
288- )
270+ return scim_error .model_dump (), HTTPStatus .NOT_FOUND
271+ return schema .model_dump (scim_ctx = Context .RESOURCE_QUERY_RESPONSE )
289272# -- schemas-end --
290273
291274
@@ -301,10 +284,7 @@ def list_resource_types():
301284 items_per_page = len (page ),
302285 resources = page ,
303286 )
304- return (
305- response .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
306- HTTPStatus .OK ,
307- )
287+ return response .model_dump (scim_ctx = Context .RESOURCE_QUERY_RESPONSE )
308288
309289
310290@bp .get ("/ResourceTypes/<resource_type_id>" )
@@ -316,23 +296,17 @@ def get_resource_type_by_id(resource_type_id):
316296 scim_error = Error (
317297 status = 404 , detail = f"ResourceType { resource_type_id !r} not found"
318298 )
319- return scim_error .model_dump_json (), HTTPStatus .NOT_FOUND
320- return (
321- rt .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE ),
322- HTTPStatus .OK ,
323- )
299+ return scim_error .model_dump (), HTTPStatus .NOT_FOUND
300+ return rt .model_dump (scim_ctx = Context .RESOURCE_QUERY_RESPONSE )
324301# -- resource-types-end --
325302
326303
327304# -- service-provider-config-start --
328305@bp .get ("/ServiceProviderConfig" )
329306def get_service_provider_config ():
330307 """Return the SCIM service provider configuration."""
331- return (
332- service_provider_config .model_dump_json (
333- scim_ctx = Context .RESOURCE_QUERY_RESPONSE
334- ),
335- HTTPStatus .OK ,
308+ return service_provider_config .model_dump (
309+ scim_ctx = Context .RESOURCE_QUERY_RESPONSE
336310 )
337311# -- service-provider-config-end --
338312# -- discovery-end --
0 commit comments