From 95cc75941a9b18b3c3dff525c0633b7c3acb1dfc Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Wed, 22 Jul 2026 08:48:43 +0200 Subject: [PATCH 1/2] fix: support extension schema URI containers in path-less PATCH A PATCH add/replace without a "path" whose value nests extension attributes under their schema URI (RFC 7643 Section 3.3, RFC 7644 Section 3.5.2) failed with "invalid uri prefix". validateEmptyPath ran the URI key through ParsePath, which strips the trailing segment and produces a URI that never matches a registered schema. Detect keys that exactly match a known schema URI and validate their members against that schema instead. --- internal/patch/add_test.go | 19 +++++++++++++++++++ internal/patch/patch.go | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/internal/patch/add_test.go b/internal/patch/add_test.go index fddd1b8..bd4ed17 100644 --- a/internal/patch/add_test.go +++ b/internal/patch/add_test.go @@ -42,3 +42,22 @@ func Example_addWithoutPath() { // Output: // map[emails:[map[type:work value:quint@elimity.com]] nickname:di-wu] } + +// The following example shows how to add extension attributes to a User resource without using a "path" attribute, +// where the extension is provided as a nested object keyed by its schema URI. See RFC 7643 Section 3.3 +// and RFC 7644 Section 3.5.2. +func Example_addExtensionWithoutPath() { + operation, _ := json.Marshal(map[string]interface{}{ + "op": "add", + "value": map[string]interface{}{ + "userName": "test", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": map[string]interface{}{ + "organization": "SUSE", + }, + }, + }) + validator, _ := NewValidator(operation, schema.CoreUserSchema(), schema.ExtensionEnterpriseUser()) + fmt.Println(validator.Validate()) + // Output: + // map[urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:map[organization:SUSE] userName:test] +} diff --git a/internal/patch/patch.go b/internal/patch/patch.go index 410ae18..609b2f7 100644 --- a/internal/patch/patch.go +++ b/internal/patch/patch.go @@ -184,6 +184,28 @@ func (v OperationValidator) validateEmptyPath() (interface{}, error) { rootValue := map[string]interface{}{} for p, value := range attributes { + // A key that exactly matches a schema URI is a JSON container for that + // extension's attributes, not an attribute path (RFC 7643 Section 3.3, + // RFC 7644 Section 3.5.2). Validate its members against the extension schema. + if extSchema, ok := v.schemas[p]; ok { + extAttributes, ok := value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("the value of %s should be a complex attribute", p) + } + extValidator := OperationValidator{ + Op: v.Op, + value: extAttributes, + schema: extSchema, + schemas: v.schemas, + } + extValue, err := extValidator.validateEmptyPath() + if err != nil { + return nil, err + } + rootValue[p] = extValue + continue + } + path, err := filter.ParsePath([]byte(p)) if err != nil { return nil, fmt.Errorf("invalid attribute path: %s", p) From 96da93b774a42e708bc25eb0106c84931ebbc692 Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Wed, 22 Jul 2026 09:01:45 +0200 Subject: [PATCH 2/2] chore: satisfy goarrange in add_test --- internal/patch/add_test.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/patch/add_test.go b/internal/patch/add_test.go index bd4ed17..dc16b5c 100644 --- a/internal/patch/add_test.go +++ b/internal/patch/add_test.go @@ -6,6 +6,25 @@ import ( "github.com/elimity-com/scim/schema" ) +// The following example shows how to add extension attributes to a User resource without using a "path" attribute, +// where the extension is provided as a nested object keyed by its schema URI. See RFC 7643 Section 3.3 +// and RFC 7644 Section 3.5.2. +func Example_addExtensionWithoutPath() { + operation, _ := json.Marshal(map[string]interface{}{ + "op": "add", + "value": map[string]interface{}{ + "userName": "test", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": map[string]interface{}{ + "organization": "SUSE", + }, + }, + }) + validator, _ := NewValidator(operation, schema.CoreUserSchema(), schema.ExtensionEnterpriseUser()) + fmt.Println(validator.Validate()) + // Output: + // map[urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:map[organization:SUSE] userName:test] +} + // The following example shows how to add a member to a group. func Example_addMemberToGroup() { operation, _ := json.Marshal(map[string]interface{}{ @@ -42,22 +61,3 @@ func Example_addWithoutPath() { // Output: // map[emails:[map[type:work value:quint@elimity.com]] nickname:di-wu] } - -// The following example shows how to add extension attributes to a User resource without using a "path" attribute, -// where the extension is provided as a nested object keyed by its schema URI. See RFC 7643 Section 3.3 -// and RFC 7644 Section 3.5.2. -func Example_addExtensionWithoutPath() { - operation, _ := json.Marshal(map[string]interface{}{ - "op": "add", - "value": map[string]interface{}{ - "userName": "test", - "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": map[string]interface{}{ - "organization": "SUSE", - }, - }, - }) - validator, _ := NewValidator(operation, schema.CoreUserSchema(), schema.ExtensionEnterpriseUser()) - fmt.Println(validator.Validate()) - // Output: - // map[urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:map[organization:SUSE] userName:test] -}