| Developer Portal

SCIM Patch Operations

SCIM Patch Operations aggregate a collection of operations on a target resource. SCIM Patching is described in RFC7644 Section 3.5.2. This guide covers basics and common usecases.

Overview

An example SCIM User Patch Request might look like this:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "replace",
      "path": "title",
      "value": "Bossman"
    },
    {
      "op": "replace",
      "path": "name:familyName",
      "value": "Smith"
    },
    {
      "op": "add",
      "path": "emails",
      "value": { "value": "my-new-emails", "primary": true }
    },
    {
      "op": "remove",
      "path": "emails[primary eq false]"
    }
  ]
}

Each SCIM Patch consists of one or more Operations in an ordered list.

Each operation has the following properties:

Property Description Values/Notes/...
op Patch Operation add, replace, remove
path SCIM Path to value we're operating on Optional
value Value being added or using as replacement See note if no path

The op property defines the operation, each doing as it suggests. replace completely replaces a value, remove removes a value (and can operate on single- or multi-valued properties), and add adds values to multi-valued properties (but also can act as a replace).

The path property targets the value being modified, e.g., the current userName, a collection of addresses, or properties within collections. SCIM Filters may be used to further refine targeting. path may be omitted; see below.

Not all PatchOps require the use of existing entity values, e.g., remove operations don't care about the current value.

šŸ”

Targeting Properties

SCIM Paths use a colon : as a path separator. This is true even when a path portion contains a colon, like the extension schemas. Example paths include:

const examplePaths = [
  { "path": "title" },                             // Root-level property
  { "path": "name:familyName" },                   // Nested property
  { "path": "urn:SocialChorus:1.0:User:managerName" }, // User extension property
]

Some properties are not at the root level, like SCIM's Enterprise User extension schema (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User, or Firstup's own User extension schema, urn:SocialChorus:1.0:User, or SocialChorus:1.0:User, depending on configuration.

SCIM paths use a colon : as a path separator. To target our User extension schema's workLocation property the path would be:

{
  "op": "replace",
  "path": "urn:SocialChorus:1.0:User:workLocation",
  "value": "Updated work location"
}

path-less Targeting

If the optional path property is omitted it is assumed the target is the entire resource, e.g., a SCIM User or Group. In this case the value property's own property names will align with the resource.

For example, we can replace a user's displayName with either a path:

{
  "op": "replace", 
  "path": "displayName", 
  "value": "User McUser"
}

Or by using a replacement value object:

{ 
  "op": "replace",
  "value": {
    "displayName":  "User McUser"
  }
}

Notes

Lists (Arrays)

Targeting a list element requires filtering. For example, if we wanted to updated a misspelled street name in a user's address we might target based on the misspelling:

{
  "op": "replace",
  "path": "address[streetAddress eq \"42 Marn St\"].streetAddress",
  "value": "42 Main St"
}

SCIM does not support position-based operations, meaning we can't target the "first" address, or re-order addresses from within SCIM itself. Those types of operations would need to be handled on the client side followed by a value replacement.

Objects (like name)

A SCIM User's name property is an object with givenName and familyName, e.g.,

{
  "name": {
    "givenName": "Leonardo",
    "familyName": "da Vinci"
  }
}

Updating the familyName follows SCIM pathing rules using a colon : as the separator:

{
  "op": "replace",
  "path": "name:familyName",
  "value": "Ninja Turtle"
}

A complete value replacement is similar, with a subtle "gotcha":

{
  "op": "replace",
  "value": {
    "name": {
      "familyName": "Ninja Turtle",
      "givenName": "Leonardo"
    }
  }
}

Here we're replacing the name property, but if we don't include givenName its value will be removed. In other words the value object's name property isn't merged with the resource's current value--it's replaced.

Lists of Objects (like emails or addresses)

Lists of objects boil down to what we've already seen, for example targeting an address as shown in the example above. The first step is to target the specific list item, the second is to target a specific property (or the entire object).

šŸ”

SCIM User Examples

Title

Replacing a value is the most common PATCH operation. Here we replace the user's title attribute.

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "replace",
      "path": "title",
      "value": "Da Boss"
    }
  ]
}

As noted, SCIM PatchOps are a collection of operations: a single PATCH request may contain multiple operations. Here we'll replace both the user's title and locale:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "replace",
      "path": "title",
      "value": "Boss"
    },
    {
      "op": "replace",
      "path": "locale",
      "value": "en-UK"
    }
  ]
}

Emails

Adding an Email

Adding an email is straight-forward:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "add",
      "path": "emails",
      "value": {
        "value": "baz@amazon.com"
      }
    }
  ]
}

Note: value shows up a lot in the above request. This is because the object used in the value itself has a value attribute: emails have value and primary attributes.

Adding multiple emails is also possible by providing an array of email objects as the PatchOp's value attribute:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "add",
      "path": "emails",
      "value": [
        {
          "value": "plugh@amazon.com"
        },
        {
          "value": "xyzzy@amazon.com"
        }
      ]
    }
  ]
}

Multiple Primary Emails

Only one email at a time can be the primary email. If multiple emails are marked as primary in the request it is a validation error. Since there's a primary email by default, even if none are set explicitly, a PatchOp that adds an additional primary email will result in the previous primary email being "demoted" and the added email will become the new primary:

emails prior to PatchOp add:

"emails": [
  {
    "value": "plugh@com.com",
    "primary": true
  },
  {
    "value": "xyzzy@com.com",
    "primary": false
  }
],

If we add emails, one marked as primary:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "add",
      "path": "emails",
      "value": [
        {
          "value": "foo@com.com",
          "primary": true
        },
        {
          "value": "bar@com.com",
          "primary": false
        }
      ]
    }
  ]
}

This will result in the following emails:

"emails": [
  {
    "value": "plugh@com.com",
    "primary": false
  },
  {
    "value": "xyzzy@com.com",
    "primary": false
  },
  {
    "value": "foo@com.com",
    "primary": true
  },
  {
    "value": "bar@com.com",
    "primary": false
  }
]

The request's intent is impossible to guess, so we best-effort based on what we think is meant.

Swapping the Primary Email

To change the primary email we can replace the primary value on the email we want to make primary. This has the secondary effect of setting the previously-primary email's primary attribute to `false.

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "replace",
      "path": "emails[value eq \"bar@com.com\"].primary",
      "value": true
    }
  ]
}

Phone Numbers

Phone numbers are another multi-valued property, each with a type and value property. The Firstup API currently supports two types of phone numbers, "main" and "mobile". The phone number type must be sent with the phone number. Phone numbers with a missing or invalid type are silently ignored.

Removing a Phone Number

Phone numbers may be removed by their value or type, or, most robustly, with both. For example, let's say a user has the same phone number for both their "main" and "mobile" number. We cannot use the phone number as the value comparison because this would remove both numbers. We can specify just the type, but we can also specify the number just in case.

Photos

Firstup uses the photos property to define the user avatar, and nothing else. Only the first photo in the photos array is examined, validated, and used. All other photos entries are ignored. It is, however, still a multi-valued property because SCIM says it is, so it must be treated as such in PatchOps.

šŸ”

Custom Attributes

Adding a Single Custom Attribute

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "add",
      "path": "urn:SocialChorus:1.0:User:customAttributes",
      "value": { "name": "ca1", "value": "ca1 value" }
    }
  ]
}

Adding Multiple Custom Attributes

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "add",
      "path": "urn:SocialChorus:1.0:User:customAttributes",
      "value": [
        { "name": "ca1", "value": "ca1 value" },
        { "name": "ca2", "value": "ca2 value" },
        { "name": "ca3", "value": "ca3 value" }
      ]
    }
  ]
}

Replacing a Custom Attribute Value

We specify the path to the custom attribute with a combination of the path, a query, and the attribute we're changing. Normally for a custom attribute this would be the value attribute.

For example, if we're updating a custom attribute named "job_code" we'd do it like this:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "replace",
      "path": "urn:SocialChorus:1.0:User:customAttributes[name eq \"job_code\"].value",
      "value": "THX1138"
    }
  ]
}

Removing a Custom Attribute

Similarly if we want to completely remove a custom attribute named "employee_type":

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "remove",
      "path": "urn:SocialChorus:1.0:User:customAttributes[name eq \"employee_type\"]"
    }
  ]
}

Removing All Custom Attributes

We can remove all custom attributes with remove:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

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

Or we can replace the value with an empty array:

PATCH {{PAPI_URL}}/scim/v2/Users/{{USER_ID}} HTTP/1.1
Authorization: Bearer {{PAPI_TOKEN}}
Content-Type: application/json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "replace",
      "path": "urn:SocialChorus:1.0:User:customAttributes",
      "value": []
    }
  ]
}

šŸ”

SCIM Group Examples

Changing the members of a Group

The PATCH endpoint allows to modify the list of members of a Group, adding or removing users:

Please note that the "add" and "replace" operations do not have the same meaning.

In order to perform any of those operations, the identifierField may be specified. It determines how we identify users. Allowed values are "id", "email" and "userName".

PATCH {{PAPI_URL}}/scim/v2/Groups/{{group_id}}
Authorization: Bearer {{token}}
Content-Type: application/scim+json

{
  "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "SocialChorus:1.0:Group": { "identifierField":"email" },
  "Operations": [
    {
      "op": "add",
      "path": "members",
      "value": [
        { "value":"jane.doe@my_company.com" },
        { "value":"john.smith@my_company.com" }
      ]
    }
  ]
}

If 'identifierField' is not present in the request, the users will be identified by 'userNameā€™.

PATCH {{PAPI_URL}}/scim/v2/Groups/{{group_id}}
Authorization: Bearer {{token}}
Content-Type: application/scim+json

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

Rules when more than one operation is included for "members"

If a "replace" operation is used, we recommend to not include any additional operations in the same request, in order to avoid unexpected results. The main reasons are:

  1. A "replace" is a complete replacement of the list of members.
  2. Each "members" operation is performed asynchronously, so race conditions could apply.

If more that one operation is included in a PATCH call, we will apply the following rules:

  1. If the API request asks both to "add" and "remove" with the same user, that user's changes will be ignored (it will be removed from those two operations).
  2. If a "replace" operation is received, along with an "add" and/or "remove", as the "replace" means a full members replace, we will resolve it this way:
    • The users to `"remove"` will be ignored (as the previous existing users will be replaced)
    • The users to `"add"` will be added to the "replace" list (kept in the final set of members)

Please note that if the three operations are used, and same user is both in the "add" and the "remove" lists, it will not be added to the "replace" list (unless is was present there as well).

Modifying Group details

The general details of the Users' membership can be edited independently or at the same time with the list of members.

The Group-specific attributes that can be modified are (all String values):

Attribute name Description Example
displayName Name to be displayed "Editors Group"
description Description of this group purpose "Users allowed to edit news releases"
externalId Identifier used in an external system "edit_group_123"

For example, having a group with these details:

group_id:    1234
displayName: Editors Group
description: Users allowed to edit all news releases
externalId:  edit_group_123

This API call would change the displayName and description:

PATCH {{PAPI_URL}}/scim/v2/Groups/1234
Authorization: Bearer {{token}}
Content-Type: application/scim+json

{
  "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
  "Operations": [
    {
      "op": "replace",
      "path": "displayName",
      "value":"XYZ News Editors"
    },
    {
      "op": "replace",
      "path": "description",
      "value":"News editors for the new project XYZ"
    }
  ]
}

After performing the API call we'll have:

group_id:    1234
displayName: XYZ News Editors
description: News editors for the new project XYZ
externalId:  edit_group_123

šŸ”