88)
99from authlib .oauth2 .rfc6750 import BearerTokenValidator
1010from flask import Blueprint
11- from flask import Response
1211from flask import abort
1312from flask import current_app
1413from flask import request
2221from scim2_models import Schema
2322from scim2_models import SearchRequest
2423from werkzeug .exceptions import HTTPException
24+ from werkzeug .exceptions import PreconditionFailed
2525
2626from canaille .app import models
2727from canaille .app .flask import csrf
2828from canaille .backends import Backend
2929
3030from .casting import group_from_canaille_to_scim_server
3131from .casting import group_from_scim_to_canaille
32+ from .casting import make_etag
3233from .casting import user_from_canaille_to_scim_server
3334from .casting import user_from_scim_to_canaille
3435from .models import EnterpriseUser
@@ -58,6 +59,34 @@ def add_scim_content_type(response):
5859 return response
5960
6061
62+ @bp .after_request
63+ def set_etag_header (response ):
64+ """Extract ``ETag`` from ``meta.version`` and handle conditional responses."""
65+ data = response .get_json (silent = True )
66+ if meta := (data or {}).get ("meta" ):
67+ if version := meta .get ("version" ):
68+ response .headers ["ETag" ] = version
69+ response .make_conditional (request )
70+ return response
71+
72+
73+ @bp .before_request
74+ def check_etag ():
75+ """Verify ``If-Match`` on write operations."""
76+ if request .method not in ("PUT" , "PATCH" , "DELETE" ):
77+ return
78+ arg = next (iter (request .view_args .values ()))
79+ if_match = request .headers .get ("If-Match" )
80+ if not if_match :
81+ return
82+ if if_match .strip () == "*" :
83+ return
84+ etag = make_etag (arg )
85+ tags = [t .strip () for t in if_match .split ("," )]
86+ if etag not in tags :
87+ raise PreconditionFailed ("ETag mismatch" )
88+
89+
6190@bp .errorhandler (HTTPException )
6291def http_error_handler (error ):
6392 obj = Error (detail = str (error ), status = error .code )
@@ -268,6 +297,7 @@ def search():
268297@csrf .exempt
269298@require_oauth ()
270299def create_user ():
300+ req = ResponseParameters .model_validate (request .args .to_dict ())
271301 request_user = User [EnterpriseUser ].model_validate (
272302 request .json , scim_ctx = Context .RESOURCE_CREATION_REQUEST
273303 )
@@ -277,14 +307,21 @@ def create_user():
277307 f"SCIM created user { user .id } by client { current_token .client .client_id } "
278308 )
279309 response_user = user_from_canaille_to_scim_server (user )
280- payload = response_user .model_dump_json (scim_ctx = Context .RESOURCE_CREATION_RESPONSE )
281- return Response (payload , status = HTTPStatus .CREATED )
310+ return (
311+ response_user .model_dump (
312+ scim_ctx = Context .RESOURCE_CREATION_RESPONSE ,
313+ attributes = req .attributes ,
314+ excluded_attributes = req .excluded_attributes ,
315+ ),
316+ HTTPStatus .CREATED ,
317+ )
282318
283319
284320@bp .route ("/Groups" , methods = ["POST" ])
285321@csrf .exempt
286322@require_oauth ()
287323def create_group ():
324+ req = ResponseParameters .model_validate (request .args .to_dict ())
288325 request_group = Group .model_validate (
289326 request .json , scim_ctx = Context .RESOURCE_CREATION_REQUEST
290327 )
@@ -294,58 +331,69 @@ def create_group():
294331 f"SCIM created group { group .id } by client { current_token .client .client_id } "
295332 )
296333 response_group = group_from_canaille_to_scim_server (group )
297- payload = response_group .model_dump_json (
298- scim_ctx = Context .RESOURCE_CREATION_RESPONSE
334+ return (
335+ response_group .model_dump (
336+ scim_ctx = Context .RESOURCE_CREATION_RESPONSE ,
337+ attributes = req .attributes ,
338+ excluded_attributes = req .excluded_attributes ,
339+ ),
340+ HTTPStatus .CREATED ,
299341 )
300- return Response (payload , status = HTTPStatus .CREATED )
301342
302343
303344@bp .route ("/Users/<user:user>" , methods = ["PUT" ])
304345@csrf .exempt
305346@require_oauth ()
306347def replace_user (user ):
348+ req = ResponseParameters .model_validate (request .args .to_dict ())
307349 original_scim_user = user_from_canaille_to_scim_server (user )
308350 request_scim_user = User [EnterpriseUser ].model_validate (
309351 request .json ,
310352 scim_ctx = Context .RESOURCE_REPLACEMENT_REQUEST ,
311- original = original_scim_user ,
312353 )
354+ request_scim_user .replace (original_scim_user )
313355 updated_user = user_from_scim_to_canaille (request_scim_user , user )
314356 Backend .instance .save (updated_user )
315357 current_app .logger .security (
316358 f"SCIM replaced user { updated_user .id } by client { current_token .client .client_id } "
317359 )
318360 response_scim_user = user_from_canaille_to_scim_server (updated_user )
319- payload = response_scim_user .model_dump (
320- scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE
361+ return response_scim_user .model_dump (
362+ scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE ,
363+ attributes = req .attributes ,
364+ excluded_attributes = req .excluded_attributes ,
321365 )
322- return payload
323366
324367
325368@bp .route ("/Groups/<group:group>" , methods = ["PUT" ])
326369@csrf .exempt
327370@require_oauth ()
328371def replace_group (group ):
372+ req = ResponseParameters .model_validate (request .args .to_dict ())
329373 original_scim_group = group_from_canaille_to_scim_server (group )
330374 request_scim_group = Group .model_validate (
331375 request .json ,
332376 scim_ctx = Context .RESOURCE_REPLACEMENT_REQUEST ,
333- original = original_scim_group ,
334377 )
378+ request_scim_group .replace (original_scim_group )
335379 updated_group = group_from_scim_to_canaille (request_scim_group , group )
336380 Backend .instance .save (updated_group )
337381 current_app .logger .security (
338382 f"SCIM replaced group { updated_group .id } by client { current_token .client .client_id } "
339383 )
340384 response_group = group_from_canaille_to_scim_server (updated_group )
341- payload = response_group .model_dump (scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE )
342- return payload
385+ return response_group .model_dump (
386+ scim_ctx = Context .RESOURCE_REPLACEMENT_RESPONSE ,
387+ attributes = req .attributes ,
388+ excluded_attributes = req .excluded_attributes ,
389+ )
343390
344391
345392@bp .route ("/Users/<user:user>" , methods = ["PATCH" ])
346393@csrf .exempt
347394@require_oauth ()
348395def patch_user (user ):
396+ req = ResponseParameters .model_validate (request .args .to_dict ())
349397 scim_user = user_from_canaille_to_scim_server (user )
350398 patch_op = PatchOp [User [EnterpriseUser ]].model_validate (
351399 request .json , scim_ctx = Context .RESOURCE_PATCH_REQUEST
@@ -360,13 +408,18 @@ def patch_user(user):
360408 )
361409 scim_user = user_from_canaille_to_scim_server (updated_user )
362410
363- return scim_user .model_dump (scim_ctx = Context .RESOURCE_PATCH_RESPONSE )
411+ return scim_user .model_dump (
412+ scim_ctx = Context .RESOURCE_PATCH_RESPONSE ,
413+ attributes = req .attributes ,
414+ excluded_attributes = req .excluded_attributes ,
415+ )
364416
365417
366418@bp .route ("/Groups/<group:group>" , methods = ["PATCH" ])
367419@csrf .exempt
368420@require_oauth ()
369421def patch_group (group ):
422+ req = ResponseParameters .model_validate (request .args .to_dict ())
370423 scim_group = group_from_canaille_to_scim_server (group )
371424 patch_op = PatchOp [Group ].model_validate (
372425 request .json , scim_ctx = Context .RESOURCE_PATCH_REQUEST
@@ -381,7 +434,11 @@ def patch_group(group):
381434 )
382435 scim_group = group_from_canaille_to_scim_server (updated_group )
383436
384- return scim_group .model_dump (scim_ctx = Context .RESOURCE_PATCH_RESPONSE )
437+ return scim_group .model_dump (
438+ scim_ctx = Context .RESOURCE_PATCH_RESPONSE ,
439+ attributes = req .attributes ,
440+ excluded_attributes = req .excluded_attributes ,
441+ )
385442
386443
387444@bp .route ("/Users/<user:user>" , methods = ["DELETE" ])
0 commit comments