Skip to content

Patching resources

Pascal Knüppel edited this page Nov 2, 2023 · 21 revisions

Behaviour changed since 1.12.0

Due to the issues #200 and #201 several bugs and unwanted behaviour was discovered within patch operations. The primary change is that a bad request will be returned if a "path" within a patch operation has no matching targets. There are still some exceptions though as the following:

operation: remove 

path: emails.type                 // does not cause an error if no target is present (for multivalued-complex-types only, would cause a bad request for non-multivalued-complex e.g. name.givenName)
path: emails[type eq "work"].type // does cause a bad request if no target is present

The difference here is that the first example should remove all type-attributes from all emails. If at least one email does not have the type attribute it seems wrong to throw an exception. The second case should be clear though. If no email is matching the filter a bad request should be expected.

The changes made are done on operations using the patchOp: remove and to all operations using a filter within the path-attribute. The rest is unchanged.

How Patch is handled:

Patch was implemented as defined in RFC7644 section 3.5.2 but some parts of patch were not described or just vague which is why this page shall demonstrate how patch within this implementation is handled.


NOTE:

In case that a patch-operation is executed the patch-operation will call the get-resource-method and will then execute the patch operation on the returned resource. Eventually the update method is called with the patched resource. The update method is only called under the circumstance that the resource was effectively changed. If the patch operation did not give any changes the update method will not be called!


In case that the client uses either the add or the replace operation and omits the path attribute only a single value is allowed which must be a complex json node that describes the resource itself. In this case there are only a few differences between add and replace:

  • In case of simple attributes as "userName" or "locale" there is no difference between add and replace
  • In case of simple array types the both operations do exactly what you would expect:
    • add: adds the given values to the array
    • replace: replaces the whole array with the values in the patch operation
  • In case of complex types e.g. "name":
    • add: adds the single inner simple attributes from the given value into the already existing complex node.
    • replace: replaces the whole complex node
  • In case of multi valued complex types e.g. "emails":
    • add: adds the given complex nodes to the emails-array
    • replace: replaces the whole array with the given email-representations

But the more interesting part is if a path-attribute is present. This was separated into two cases

  1. simple path values e.g.: "userName" or "name.givenName" or "emails.value"
  2. filter path values e.g.: "name[givenName eq "Norris"].familyName or "emails[displayName sw "Kn"]"

We will start with the first case:

simple path values

The short description is that this case behaves almost exactly as the way without a path attribute. The difference here is that dependent on the target type several values are allowed or not


imagine the following request

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "userName",
    "op" : "add",
    "value" : [ "chuck" ]
  } ]
}

add, replace:

  • "userName" present
    • the attribute is replaced
  • "userName" not present
    • the attribute is added

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "userName",
    "op" : "remove"
  } ]
}

add, replace:

  • "userName" present
    • the attribute will be removed
  • "userName" not present
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "name",
    "op" : "add",
    "value" : [ "{\"givenName\": \"Carlos\", \"familyName\": \"Norris\"}" ]
  } ]
}

add:

  • "name" present
    • the values "givenName" and "familyName" are added to the existing name-attribute
  • "name" not present
    • the name attribute is added with the two values "givenName" and "familyName"

replace

  • "name" present
    • the whole name attribute is replaced so that only a name attribute with the values "givenName" and "familyName" remains
  • "name" not present
    • the name attribute is added with the two values "givenName" and "familyName"

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "name",
    "op" : "remove"
  } ]
}

remove:

  • "name" present
    • the complex attribute "name" will be removed
  • "name" not present
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "name.givenName",
    "op" : "add",
    "value" : [ "Carlos" ]
  } ]
}

add, replace:

  • "name" not present
    • the attribute name is created
  • "name.givenName" present
    • the attribute is replaced
  • "name.givenName" not present
    • the attribute is added

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "name.givenName",
    "op" : "remove"
  } ]
}

remove:

  • "name" not present
    • a bad request is returned with scimType 'no_target'
  • "name.givenName" present
    • the attribute is removed
  • "name.givenName" not present
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails",
    "op" : "add",
    "value" : [ "{\"value\": \"[email protected]\", \"type\": \"work\"}", 
                "{\"value\": \"[email protected]\", \"type\": \"home\"}" ]
  } ]
}

add

  • "emails" present
    • two new complex email-nodes are added to the "emails"-attribute array
  • "emails" not present
    • the emails attribute is created and the new email-nodes are added to it

replace

  • "emails" present
    • the whole emails-array is exchanged for the given two new email-nodes
  • "emails" not present
    • the emails attribute is created and the new email-nodes are added to it

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails",
    "op" : "remove"
  } ]
}

remove

  • "emails" present
    • the multivalued complex attribute will be removed
  • "emails" not present
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails.type",
    "op" : "add",
    "value" : [ "work" ]
  } ]
}

add, replace:

  • "emails" not present
    • nothing happens and a http 200 is returned. (All type values should have been changed to "work" but there are no nodes to change is not directly an error)
  • "emails.type" present
    • the attribute is replaced within each existing email-node
  • "emails.type" not present
    • the attribute is added within each existing email-node

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails.type",
    "op" : "remove"
  } ]
}

remove:

  • "emails" not present
    • a bad request is returned with scimType 'no_target'
  • "emails.type" present
    • the attribute will be removed from all emails
  • "emails.type" not present on any email attribute
    • nothing happens and a 200 is returned. None matching targets on multi-valued-complex nodes will not cause an error if no filter is set.

In the standard resources defined in RFC7643 "User" and "Group" you will not find any declarations of simple-arrays. But imagine the following patch operation

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "multivalued.stringarray",
    "op" : "add",
    "value" : [ "hello", "world" ]
  } ]
}

add:

  • "multivalued" not present
    • nothing happens and a http 200 is returned. (All type values should have been changed to "work" but there are no nodes to change is not directly an error)
  • "multivalued.stringarray" present
    • the values "hello" and "world" are added to each existing "multivalued.stringarray"-node
  • "multivalued.stringarray" not present
    • a new "stringarray" attribute is created in each existing "multivalued"-node with the values "hello" and "world"

replace:

  • "multivalued" not present
    • nothing happens and a http 200 is returned. (All type values should have been changed to "work" but there are no nodes to change is not directly an error)
  • "multivalued.stringarray" present
    • the "stringarray" attributes of each existing "multivalued"-node is replaced with a new array containing the two values "hello" and "world"
  • "multivalued.stringarray" not present
    • a new "stringarray" attribute containing the two values "hello" and "world" is created for each existing "multivalued"-node

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "multivalued.stringarray",
    "op" : "remove"
  } ]
}

remove:

  • "multivalued" not present
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" present
    • the array attribute "multivalued.stringarray" is removed from all "multivalued"-nodes.
  • "multivalued.stringarray" not present
    • nothing happens and a 200 is returned. None matching targets on multi-valued-complex nodes will not cause an error if no filter is set.

filter path values

this were the easy patch operations. Now I will describe the more complex patch-operations with filter-expressions

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "userName[type eq \"work\"]",
    "op" : "add",
    "value" : [ "chuck" ]
  } ]
}
  • throws simply a bad request because its an invalid filter. The attribute "userName" is a simple attribute and therefore an attribute with the scheme "userName.type" does not exist

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "name[givenName eq \"work\"].familyName",
    "op" : "add",
    "value" : [ "chuck" ]
  } ]
}

add, replace

  • "name" not present
    • a bad request is returned with scimType 'no_target'
  • "name.givenName" not present
    • a bad request is returned with scimType 'no_target'
  • "name.givenName" present and filter matches
    • the attribute "familyName" is added to the name-attribute
  • "name.givenname" present and filter does not match
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "name[givenName eq \"work\"].familyName",
    "op" : "remove"
  } ]
}

remove

  • "name" not present
    • a bad request is returned with scimType 'no_target'
  • "name.givenName" not present
    • a bad request is returned with scimType 'no_target'
  • "name.givenName" present and filter matches
    • the attribute "familyName" is added to the name-attribute
  • "name.givenname" present and filter does not match
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails[value eq \"123456\"].type",
    "op" : "add",
    "value" : [ "home" ]
  } ]
}

add, replace

  • "emails" not present
    • a bad request is returned with scimType 'no_target'
  • "emails.value" present and filter matches
    • the type-value of all nodes that have a "value" attribute with "123456" will be changed to "home"
  • "emails.value" present and filter does not match
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails[value eq \"123456\"].type",
    "op" : "remove"
  } ]
}

remove

  • "emails" not present
    • a bad request is returned with scimType 'no_target'
  • "emails.value" present and filter matches
    • the type-value of all nodes that have a "value" attribute with "123456" will be removed
  • "emails.value" present and filter does not match
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails[value eq \"[email protected]\"]",
    "op" : "replace",
    "value" : [ "{\"value\": \"[email protected]\", \"type\": \"work\"}" ]
  } ]
}

add

  • "emails" not present
    • a bad request is returned with scimType 'no_target'
  • "emails.value" present and filter matches
    • If at least one match is found for the filter the new email will be added to the emails-array
  • "emails.value" present and filter does not match
    • a bad request is returned with scimType 'no_target'

replace

  • "emails" not present
    • a bad request is returned with scimType 'no_target'
  • "emails.value" present and filter matches
    • all matching nodes will be removed and the new node will be added
  • "emails.value" present and filter does not match
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "emails[value eq \"[email protected]\"]",
    "op" : "remove"
  } ]
}

remove

  • "emails" not present
    • a bad request is returned with scimType 'no_target'
  • "emails.value" present and filter matches
    • all matching emails will be removed
  • "emails.value" present and filter does not match
    • a bad request is returned with scimType 'no_target'

In the standard resources defined in RFC7643 "User" and "Group" you will not find any declarations of simple-arrays. But imagine the following patch operation

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "multivalued[stringarray eq \"hello\" and stringarray eq \"world\"].stringarray",
    "op" : "add",
    "value" : [ "hello", "goldfish" ]
  } ]
}

add

  • "multivalued" not present
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" present and filter matches [stringarray must contain a value "hello" and a value "world"]
    • the values "hello" and "goldfish" will be added to the stringarray attribute
  • "multivalued.stringarray" present and filter does not match
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" not present
    • a bad request is returned with scimType 'no_target'

replace

  • "multivalued" not present
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" present and filter matches [stringarray must contain a value "hello" and a value "world"]
    • the matchin "stringarray"-attributes replace all their values for the two values "hello" and "goldfish"
  • "multivalued.stringarray" present and filter does not match
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" not present
    • a bad request is returned with scimType 'no_target'

{
  "schemas" : [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations" : [ {
    "path" : "multivalued[stringarray eq \"hello\" and stringarray eq \"world\"].stringarray",
    "op" : "remove"
  } ]
}

remove

  • "multivalued" not present
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" present and filter matches [stringarray must contain a value "hello" and a value "world"]
    • all nodes having a "stringArray"-attribute "['hello', ..., 'world']" will be removed
  • "multivalued.stringarray" present and filter does not match
    • a bad request is returned with scimType 'no_target'
  • "multivalued.stringarray" not present
    • a bad request is returned with scimType 'no_target'

Ignore unknown attributes (@since 1.19.0)

Patch requests are defined to fail if a target-attribute in the patch-request does not exist. The request should fail with a status of 400 and a scimType of invalidPath. This behaviour can be toggled with a configuration option that tells the patch-handler to simply ignore unknown attributes.

PatchConfig.builder().setIgnoreUnknownAttribute(true).build();