From bd5dcb61534cc3655de5f0a8fb58fa07b5aa04f3 Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Tue, 24 Mar 2026 20:58:16 +0100 Subject: [PATCH] fix: do not wrap singular value in slice for patch with value expression When a PATCH replace operation targets a specific element within a multi-valued attribute using a value expression (e.g. addresses[type eq "work"]), the validated value was incorrectly wrapped in a []interface{} slice. Per RFC 7644 Section 3.5.2.3, the value represents the replacement for matched records and should be returned as-is. --- internal/patch/patch.go | 5 +++-- internal/patch/replace_test.go | 2 +- internal/patch/update.go | 6 ++++++ internal/patch/update_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/internal/patch/patch.go b/internal/patch/patch.go index 70beef0..410ae18 100644 --- a/internal/patch/patch.go +++ b/internal/patch/patch.go @@ -100,8 +100,9 @@ func NewValidator(patchReq []byte, s schema.Schema, extensions ...schema.Schema) } // Validate validates the PATCH operation. Unknown attributes in complex values are ignored. The returned interface -// contains a (sanitised) version of given value based on the attribute it targets. Multi-valued attributes will always -// be returned wrapped in a slice, even if it is just one value that was defined within the operation. +// contains a (sanitised) version of given value based on the attribute it targets. Multi-valued attributes are +// returned wrapped in a slice, unless a value expression is present in the path (e.g. addresses[type eq "work"]), +// in which case a singular value is returned unwrapped as it targets a specific matched element. func (v OperationValidator) Validate() (interface{}, error) { switch v.Op { case OperationAdd, OperationReplace: diff --git a/internal/patch/replace_test.go b/internal/patch/replace_test.go index 34347ec..2d054ba 100644 --- a/internal/patch/replace_test.go +++ b/internal/patch/replace_test.go @@ -92,5 +92,5 @@ func Example_replaceWorkAddress() { validator, _ := NewValidator(operation, schema.CoreUserSchema()) fmt.Println(validator.Validate()) // Output: - // [map[country:BE locality:ExampleCity postalCode:0001 primary:true streetAddress:ExampleStreet 1 type:work]] + // map[country:BE locality:ExampleCity postalCode:0001 primary:true streetAddress:ExampleStreet 1 type:work] } diff --git a/internal/patch/update.go b/internal/patch/update.go index 8c0f599..599a746 100644 --- a/internal/patch/update.go +++ b/internal/patch/update.go @@ -62,5 +62,11 @@ func (v OperationValidator) validateUpdate() (interface{}, error) { if scimErr != nil { return nil, scimErr } + // When a value expression is present (e.g. addresses[type eq "work"]), + // the value replaces the matched element and should not be wrapped in a + // slice. Only wrap when replacing the entire multi-valued attribute. + if v.Path != nil && v.Path.ValueExpression != nil { + return attr, nil + } return []interface{}{attr}, nil } diff --git a/internal/patch/update_test.go b/internal/patch/update_test.go index 4c18ee3..750bd92 100644 --- a/internal/patch/update_test.go +++ b/internal/patch/update_test.go @@ -131,3 +131,33 @@ func TestOperationValidator_ValidateUpdate(t *testing.T) { }) } } + +func TestValidateUpdate_SingularValueNotWrapped(t *testing.T) { + // RFC 7644 Section 3.5.2.3 (page 43): "If the target location is a + // multi-valued attribute and a value selection ("valuePath") filter is + // specified that matches one or more values of the multi-valued attribute, + // then all matching record values SHALL be replaced." + // + // The singular value should NOT be wrapped in a slice, as it represents the + // replacement for matched records. + // https://github.com/elimity-com/scim/issues/188 + op, _ := json.Marshal(map[string]interface{}{ + "op": "replace", + "path": `complexMultiValued[attr1 eq "value"]`, + "value": map[string]interface{}{"attr1": "new"}, + }) + validator, err := NewValidator(op, patchSchema, patchSchemaExtension) + if err != nil { + t.Fatal(err) + } + v, err := validator.Validate() + if err != nil { + t.Fatal(err) + } + if _, ok := v.([]interface{}); ok { + t.Error("singular value with value expression should not be wrapped in a slice") + } + if _, ok := v.(map[string]interface{}); !ok { + t.Errorf("expected map[string]interface{}, got %T", v) + } +}