Skip to content

Patching resources

Pascal Knüppel edited this page Mar 6, 2024 · 21 revisions

Catch Patch-Operations and handle them manually (@since 1.22.0)

Patch is normally handled as described in the Note-block here: How Patch is handled:

With version 1.22.0 it is possible to interfere with this implementation and to catch the different patch-operations. Doing so has just one disadvantage: The RequestValidator#validateUpdate(...) will not be called for this request if you do so.

Why should I do this?

Because you might get a lot of performance boost if you have e.g. large groups with thousands of members. Normally the getResource-method would be called that retrieves all members than the members are modified and passed to the updateResource-method that will update all members. Now you can react to patch-add/remove/replace operations and directly add a member without reading the other members and checking if other members are still present and/or modified. This is of course not restricted to members. You can do this with each patch-operation. For patch operations you do not want to handle manually you can still simply call the super-method of the default implementation.

How can I do this?

The ResourceHandler implementation does now define a new method:

/**
   * Retrieves a handler that is able to apply single
   * {@link de.captaingoldfish.scim.sdk.common.request.PatchRequestOperation}s to a resource. The lifetime of
   * the returned implementation will end after all patch-operations were applied to the resource and the
   * {@link PatchOperationHandler#doAfter()} method was called.
   *
   * @param context the current SCIM request context
   * @return the patch-handler implementation that applies specific patch-operation to the current resource
   */
  public PatchOperationHandler<T> getPatchOpResourceHandler(String resourceId, Context context)
  {
    return new DefaultPatchOperationHandler<>(type, serviceProvider.getPatchConfig(), resourceType, context);
  }

Override the DefaultPatchOperationHandler. It defines several methods that can be used to handle patch-operations. I tried to make it as easy as possible for you by defining several different methods that accept different kinds of patch-operations. Each method is thoroughly documented with examples what kind of operations are handled in the corresponding method:

// only called if an extension is completely removed with a patch-operation
public boolean handleOperation(String id, RemoveExtensionRefOperation patchOperation);

// for simple-attributes and complex-sub-attributes only
public boolean handleOperation(String id, SimpleAttributeOperation patchOperation);

// for simple-multivalued-attributes
public boolean handleOperation(String id, MultivaluedSimpleAttributeOperation patchOperation);

// called if a complete complex-attribute is removed. E.g. remove `name` from the user-schema
public boolean handleOperation(String id, RemoveComplexAttributeOperation patchOperation);

// for operations that directly reference multivalued-complex-attributes
public boolean handleOperation(String id, MultivaluedComplexAttributeOperation patchOperation);

// for attributes that directly reference multivalued-complex-simple-sub-attributes
public boolean handleOperation(String id, MultivaluedComplexSimpleSubAttributeOperation patchOperation);

// for attributes that directly reference multivalued-complex-multivalued-sub-attributes
public boolean handleOperation(String id, MultivaluedComplexMultivaluedSubAttributeOperation patchOperation);

Please check the method documentations from the sources to get examples and a thorough explanation.

Also there are two more methods that require explanation:

public T getPatchedResource(String id);

public T getUpdatedResource(...);

The first method getPatchedResource is used by the default-patch-implementation to get the current resource and to apply the patch operations to this result.

The method getUpdatedResource must either return the full patched resource or simply null. If null is returned the patch-endpoint will return a responseCode of 204 (No Content) and if a resource is returned the endpoint will return a responseCode of 200 with the given resourceNode.

Adding and modifying custom Patch Workarounds (@since 1.22.0)

You can now access the ArrayList that contains the implementations for repairing and modifying patch-requests.

public class ResourceEndpoint 
{
  ...
  @Getter
  private final List<Supplier<PatchWorkaround>> patchWorkarounds = new ArrayList<>();
  ...
}

resourceEndpoint.getPatchWorkarounds()

You can remove existing patch-workarounds change the order of handling (handled from index 0 to n) and add your own customized workarounds if needed. Simply implement a new subtype of PatchWorkaround and add it.

Support for special filter-expression (@since 1.20.0)

If you have a simple array attribute definition like:

    {
      "name": "myArray",
      "type": "string",
      "description": "a simple array with some values.",
      "mutability": "readWrite",
      "returned": "default",
      "uniqueness": "none",
      "multiValued": true,
      "required": false,
      "caseExact": false
    }

You can remove a specific value from this array with the following patch-filter expression:

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

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();

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\": \"chuck@norris.com\", \"type\": \"work\"}", 
                "{\"value\": \"for_home@norris.com\", \"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 \"max.mustermann@muster.de\"]",
    "op" : "replace",
    "value" : [ "{\"value\": \"chuck@norris.com\", \"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 \"max.mustermann@muster.de\"]",
    "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'