Skip to main content

Example: SCIM provider

A complete SCIM connector for Kestrel Desk, a fictional help-desk product. Kestrel Desk has agents (its accounts) and teams, and exposes both over SCIM 2.0 with a few documented deviations from the standard. The connector syncs agents and teams, provisions agents, and adds and removes team members. It is one file, definition.json, and it derives without errors.

What the vendor documents​

The connector encodes exactly these statements from Kestrel Desk's (fictional) SCIM documentation. Everything the documentation does not mention follows the SCIM standard, so the definition leaves it at its default.

Kestrel Desk's documentation saysSetting
Each workspace has its own SCIM base URL, https://<region>.kestreldesk.example/scim/v2/workspaces/<workspace id>, in the us or eu region.baseUrl built from config.region and config.workspaceId
Authenticate with a bearer token created in the workspace's SCIM security settings.auth.bearer only
Lists page with a cursor: send after, read nextCursor, at most 50 per page.pagination
Filters are supported on userName, externalId, displayName and groups.value only. There is no filter on meta.lastModified.filtering.attributes; incremental Sync stays off
PATCH operation names are capitalized: Add, Remove, Replace.patch.opCase: "title"
A team read does not return its members. List a team's members by filtering users on groups.value.groups.membership.readMembers: "userFilter"
Teams can be created and deleted through SCIM, not renamed.groups.manage
Agents store the standard name and work email, and the enterprise extension's department.users.fields

Customers use different attributes, so the user fields and the filterable attributes are install-editable.

definition.json​

{
"engine": 1,
"description": "Kestrel Desk agents and teams over SCIM, synced and provisioned.",
"icon": "@kestreldesk.example",
"config": {
"properties": {
"region": {
"type": "string",
"enum": ["us", "eu"],
"title": "Region",
"description": "The region your Kestrel Desk workspace is hosted in: us or eu.",
"default": "us"
},
"workspaceId": {
"type": "string",
"minLength": 1,
"title": "Workspace ID",
"description": "Shown under Admin > Security > SCIM in Kestrel Desk, for example ws_4f9a2c."
},
"userFields": {
"type": "object",
"title": "User fields",
"description": "The user attributes this install syncs and provisions, keyed by field name, each bound to a SCIM path.",
"default": {
"userName": {
"path": "userName",
"title": "Username",
"identifier": true,
"required": true,
"baseline": "identity.email"
},
"externalId": {
"path": "externalId",
"title": "External ID",
"identifier": true
},
"givenName": {
"path": "name.givenName",
"title": "Given name",
"baseline": "identity.first_name"
},
"familyName": {
"path": "name.familyName",
"title": "Family name",
"baseline": "identity.last_name"
},
"email": {
"path": "emails[type eq \"work\"].value",
"title": "Work email",
"baseline": "identity.email",
"elementExtras": { "primary": true }
},
"active": {
"path": "active",
"title": "Active",
"type": "boolean",
"readOnly": true
},
"department": {
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department",
"title": "Department"
}
}
},
"filterAttributes": {
"type": "array",
"items": { "type": "string" },
"title": "Filterable attributes",
"description": "The SCIM paths Kestrel Desk accepts in filters. A lookup on any other path is refused rather than sent.",
"default": ["userName", "externalId", "displayName", "groups.value"]
}
}
},
"baseUrl": {
"$oxl": "'https://' + config.region + '.kestreldesk.example/scim/v2/workspaces/' + encode(config.workspaceId)"
},
"auth": {
"bearer": {}
},
"pagination": {
"style": "cursor",
"pageSize": 50,
"cursorParam": "after",
"nextCursorPath": "nextCursor"
},
"filtering": {
"attributes": { "$oxl": "config.filterAttributes" }
},
"patch": {
"opCase": "title"
},
"users": {
"fields": { "$oxl": "config.userFields" }
},
"groups": {
"enabled": true,
"manage": {
"create": true,
"update": false,
"delete": true
},
"membership": {
"readMembers": "userFilter",
"userFilterPath": "groups.value"
}
}
}

The decisions​

  • The base URL. The region goes into the host and the workspace id into the path, through encode. encode turns a / in a workspace id into %2F, which Owlie refuses when the install's configuration is saved, as it refuses an id that is exactly ... encode cannot protect the host, so region is an enum of the two regions: an install cannot point the connector at another host.
  • Bearer only. Leaving auth out would offer admins Basic, OAuth and custom-header options that Kestrel Desk does not accept.
  • groups.value in the filterable attributes. userFilter reads members by filtering on groups.value, and Owlie refuses a definition whose userFilterPath is not in filtering.attributes. The same rule holds for each install's copy of the list: Owlie refuses an install configuration that removes groups.value, or the path of any identifier (userName, externalId, displayName), from it.
  • elementExtras on email. A new agent's work email is created as the primary email.
  • No groups.fields. Teams keep the default field list: displayName, as an identifier.
  • Errors at their defaults. Kestrel Desk answers 409 for a duplicate and an existing member, and 404 for a missing resource and a missing member, as the standard does.

What Owlie derives​

PartDerived
Account entity userIdentifiers id (the SCIM id), userName, externalId; fields from the install's user field list
Entity groupIdentifiers id and displayName
Grant kind member_ofA user is a member of a group; writable, one member per write
AuthenticationOne option, Bearer token: the install enters the token
Install fieldsRegion (default us), Workspace ID, User fields, Filterable attributes
OperationsConnection validation and test; user read, list, create, update, delete, enable and disable; group read, list, members, create and delete; member add and remove

There is no group update, because groups.manage.update is false, and no incremental Sync, because filtering.incremental is off.

One install​

An install in the eu region with workspace ws_4f9a2c evaluates the base URL to:

https://eu.kestreldesk.example/scim/v2/workspaces/ws_4f9a2c

Owlie allows the connector to reach eu.kestreldesk.example for that install, with no egress list to maintain. The engine then sends requests such as these, relative to that base URL:

GET /Users?count=50 first page of agents
GET /Users?count=50&after=<nextCursor> next page
GET /Users?count=50&filter=groups.value eq "<team id>" a team's members
POST /Users create an agent
PATCH /Groups/<team id> { "op": "Add", "path": "members", "value": [{ "value": "<user id>" }] }
PATCH /Groups/<team id> { "op": "Remove", "path": "members[value eq \"<user id>\"]" }
PATCH /Users/<user id> { "op": "Replace", "path": "active", "value": false } disable

Each PATCH body is a standard PatchOp message with the operation shown. Creating Lena, a new agent, sends this body. Its schemas names the core User schema, then the enterprise extension, because the department field's path is under it:

{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"userName": "lena@acme.example",
"name": { "givenName": "Lena" },
"emails": [{ "type": "work", "value": "lena@acme.example", "primary": true }],
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "Support" }
}

When a remove answers 404, Owlie reads the team. If the team exists, the member was already gone and the remove reports "already absent"; if the team read fails too, the remove fails.

An install that edits its fields​

A customer that stores agents' job titles, and has no external ids, edits its copy of User fields: it adds "title": { "path": "title", "title": "Job title" } and deletes externalId. Owlie derives that install's user entity with a title field and puts externalId back, because identifiers stay as the author seeded them. Marking email as an identifier, or changing userName's path, would be refused.

Build your own from Start a SCIM connector. Every field is in the SCIM definition reference.