1616from scim2_models import PatchOp
1717from scim2_models import ResourceType
1818from scim2_models import ResponseParameters
19- from scim2_models import SCIMException
2019from scim2_models import Schema
20+ from scim2_models import SCIMException
2121from scim2_models import SearchRequest
2222from scim2_models import User
2323
24- from .integrations import check_etag
2524from .integrations import delete_record
2625from .integrations import from_scim_user
2726from .integrations import get_record
3130from .integrations import get_schemas
3231from .integrations import list_records
3332from .integrations import make_etag
34- from .integrations import PreconditionFailed
3533from .integrations import save_record
3634from .integrations import service_provider_config
3735from .integrations import to_scim_user
3836
37+
3938# -- setup-start --
4039def scim_response (payload , status = HTTPStatus .OK ):
4140 """Build a Django response with the SCIM media type."""
@@ -54,6 +53,30 @@ def resource_location(request, app_record):
5453# -- setup-end --
5554
5655
56+ # -- etag-start --
57+ def check_etag (record , request ):
58+ """Compare the record's ETag against the ``If-Match`` request header.
59+
60+ :param record: The application record.
61+ :param request: The Django request.
62+ :return: A 412 SCIM error response if the ETag does not match, or :data:`None`.
63+ """
64+ if_match = request .META .get ("HTTP_IF_MATCH" )
65+ if not if_match :
66+ return None
67+ if if_match .strip () == "*" :
68+ return None
69+ etag = make_etag (record )
70+ tags = [t .strip () for t in if_match .split ("," )]
71+ if etag not in tags :
72+ scim_error = Error (status = 412 , detail = "ETag mismatch" )
73+ return scim_response (
74+ scim_error .model_dump_json (), HTTPStatus .PRECONDITION_FAILED
75+ )
76+ return None
77+ # -- etag-end --
78+
79+
5780# -- refinements-start --
5881# -- converters-start --
5982class UserConverter :
@@ -86,22 +109,18 @@ def scim_exception_error(error):
86109 """Turn SCIM exceptions into a SCIM error response."""
87110 scim_error = error .to_error ()
88111 return scim_response (scim_error .model_dump_json (), scim_error .status )
89- # -- scim-exception-helper-end --
90112
91113
92- # -- precondition-helper-start --
93- def scim_precondition_error ():
94- """Turn ETag mismatches into a SCIM 412 response."""
95- scim_error = Error (status = 412 , detail = "ETag mismatch" )
96- return scim_response (scim_error .model_dump_json (), HTTPStatus .PRECONDITION_FAILED )
97- # -- precondition-helper-end --
114+ # -- scim-exception-helper-end --
98115
99116
100117# -- error-handler-start --
101118def handler404 (request , exception ):
102119 """Turn Django 404 errors into SCIM error responses."""
103120 scim_error = Error (status = 404 , detail = str (exception ))
104121 return scim_response (scim_error .model_dump_json (), HTTPStatus .NOT_FOUND )
122+
123+
105124# -- error-handler-end --
106125# -- refinements-end --
107126
@@ -135,18 +154,14 @@ def get(self, request, app_record):
135154 return resp
136155
137156 def delete (self , request , app_record ):
138- try :
139- check_etag (app_record , request .META .get ("HTTP_IF_MATCH" ))
140- except PreconditionFailed :
141- return scim_precondition_error ()
157+ if resp := check_etag (app_record , request ):
158+ return resp
142159 delete_record (app_record ["id" ])
143160 return scim_response ("" , HTTPStatus .NO_CONTENT )
144161
145162 def put (self , request , app_record ):
146- try :
147- check_etag (app_record , request .META .get ("HTTP_IF_MATCH" ))
148- except PreconditionFailed :
149- return scim_precondition_error ()
163+ if resp := check_etag (app_record , request ):
164+ return resp
150165 existing_user = to_scim_user (app_record , resource_location (request , app_record ))
151166 try :
152167 replacement = User .model_validate (
@@ -166,7 +181,9 @@ def put(self, request, app_record):
166181 except SCIMException as error :
167182 return scim_exception_error (error )
168183
169- response_user = to_scim_user (updated_record , resource_location (request , updated_record ))
184+ response_user = to_scim_user (
185+ updated_record , resource_location (request , updated_record )
186+ )
170187 resp = scim_response (
171188 response_user .model_dump_json (
172189 scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE
@@ -176,10 +193,8 @@ def put(self, request, app_record):
176193 return resp
177194
178195 def patch (self , request , app_record ):
179- try :
180- check_etag (app_record , request .META .get ("HTTP_IF_MATCH" ))
181- except PreconditionFailed :
182- return scim_precondition_error ()
196+ if resp := check_etag (app_record , request ):
197+ return resp
183198 try :
184199 patch = PatchOp [User ].model_validate (
185200 json .loads (request .body ),
@@ -202,6 +217,8 @@ def patch(self, request, app_record):
202217 )
203218 resp ["ETag" ] = make_etag (updated_record )
204219 return resp
220+
221+
205222# -- single-resource-end --
206223
207224
@@ -217,7 +234,9 @@ def get(self, request):
217234 return scim_validation_error (error )
218235
219236 total , page = list_records (req .start_index_0 , req .stop_index_0 )
220- resources = [to_scim_user (record , resource_location (request , record )) for record in page ]
237+ resources = [
238+ to_scim_user (record , resource_location (request , record )) for record in page
239+ ]
221240 response = ListResponse [User ](
222241 total_results = total ,
223242 start_index = req .start_index or 1 ,
@@ -298,6 +317,8 @@ def get(self, request, schema_id):
298317 return scim_response (
299318 schema .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE )
300319 )
320+
321+
301322# -- schemas-end --
302323
303324
@@ -337,6 +358,8 @@ def get(self, request, resource_type_id):
337358 return scim_response (
338359 rt .model_dump_json (scim_ctx = Context .RESOURCE_QUERY_RESPONSE )
339360 )
361+
362+
340363# -- resource-types-end --
341364
342365
@@ -350,6 +373,8 @@ def get(self, request):
350373 scim_ctx = Context .RESOURCE_QUERY_RESPONSE
351374 )
352375 )
376+
377+
353378# -- service-provider-config-end --
354379
355380
0 commit comments