@@ -55,6 +55,33 @@ func applyToMatching(list []interface{}, t *valueExprTarget, value interface{},
5555 return result , matched , nil
5656}
5757
58+ func attrFromTarget (target interface {}) schema.CoreAttribute {
59+ switch t := target .(type ) {
60+ case * attributeTarget :
61+ return t .attr
62+ case * subAttributeTarget :
63+ return t .attr
64+ case * valueExprTarget :
65+ return t .attr
66+ }
67+ panic ("unknown target type" )
68+ }
69+
70+ // checkMutability validates that the operation is compatible with the
71+ // attribute's mutability. Returns a mutability ScimError if not.
72+ func checkMutability (op string , attr schema.CoreAttribute , exists bool ) error {
73+ switch attr .Mutability () {
74+ case "readOnly" :
75+ return scimErrors .ScimErrorMutability
76+ case "immutable" :
77+ if op == PatchOperationAdd && ! exists {
78+ return nil
79+ }
80+ return scimErrors .ScimErrorMutability
81+ }
82+ return nil
83+ }
84+
5885func copyMap (m map [string ]interface {}) map [string ]interface {} {
5986 cp := make (map [string ]interface {}, len (m ))
6087 for k , v := range m {
@@ -230,6 +257,11 @@ func applyAdd(attrs ResourceAttributes, op PatchOperation, s schema.Schema, exte
230257 return nil , err
231258 }
232259
260+ _ , exists := attrs [attrName ]
261+ if err := checkMutability (op .Op , attrFromTarget (target ), exists ); err != nil {
262+ return nil , err
263+ }
264+
233265 switch t := target .(type ) {
234266 case * attributeTarget :
235267 existing , exists := attrs [attrName ]
@@ -330,14 +362,19 @@ func applyOperation(attrs ResourceAttributes, op PatchOperation, s schema.Schema
330362
331363func applyRemove (attrs ResourceAttributes , op PatchOperation , s schema.Schema , extensions []schema.Schema ) (ResourceAttributes , error ) {
332364 if op .Path == nil {
333- return nil , fmt . Errorf ( "path is required for remove operations" )
365+ return nil , scimErrors . ScimErrorNoTarget
334366 }
335367
336368 attrName , target , err := resolveTarget (op .Path , s , extensions )
337369 if err != nil {
338370 return nil , err
339371 }
340372
373+ _ , exists := attrs [attrName ]
374+ if err := checkMutability (op .Op , attrFromTarget (target ), exists ); err != nil {
375+ return nil , err
376+ }
377+
341378 switch t := target .(type ) {
342379 case * attributeTarget :
343380 delete (attrs , attrName )
@@ -389,8 +426,38 @@ func applyReplace(attrs ResourceAttributes, op PatchOperation, s schema.Schema,
389426 return nil , err
390427 }
391428
429+ _ , exists := attrs [attrName ]
430+ if err := checkMutability (op .Op , attrFromTarget (target ), exists ); err != nil {
431+ return nil , err
432+ }
433+
392434 switch t := target .(type ) {
393435 case * attributeTarget :
436+ // RFC 7644 Section 3.5.2.3: if the target location path specifies
437+ // an attribute that does not exist, the service provider SHALL
438+ // treat the operation as an "add".
439+ if _ , exists := attrs [attrName ]; ! exists {
440+ return applyAdd (attrs , op , s , extensions )
441+ }
442+ // RFC 7644 Section 3.5.2.3: "If the target location specifies a
443+ // complex attribute, a set of sub-attributes SHALL be specified in
444+ // the 'value' parameter, which replaces any existing values or adds
445+ // where an attribute did not previously exist. Sub-attributes that
446+ // are not specified in the 'value' parameter are left unchanged."
447+ if t .attr .HasSubAttributes () && ! t .attr .MultiValued () {
448+ existingMap , ok := attrs [attrName ].(map [string ]interface {})
449+ if ok {
450+ valueMap , ok := op .Value .(map [string ]interface {})
451+ if ok {
452+ merged := copyMap (existingMap )
453+ for k , v := range valueMap {
454+ merged [k ] = v
455+ }
456+ attrs [attrName ] = merged
457+ return attrs , nil
458+ }
459+ }
460+ }
394461 attrs [attrName ] = op .Value
395462 case * subAttributeTarget :
396463 existing , exists := attrs [attrName ]
@@ -411,7 +478,8 @@ func applyReplace(attrs ResourceAttributes, op PatchOperation, s schema.Schema,
411478 // RFC 7644 Section 3.5.2.3: if the target location is a multi-valued
412479 // attribute for which a value selection filter ("valuePath") has been
413480 // supplied and no record match was made, the service provider SHALL
414- // return a 400 error with a scimType of noTarget.
481+ // indicate failure by returning HTTP status code 400 and a "scimType"
482+ // error code of "noTarget".
415483 existing , exists := attrs [attrName ]
416484 if ! exists {
417485 return nil , scimErrors .ScimErrorNoTarget
0 commit comments