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 says | Setting |
|---|---|
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.encodeturns 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...encodecannot protect the host, soregionis anenumof the two regions: an install cannot point the connector at another host. - Bearer only. Leaving
authout would offer admins Basic, OAuth and custom-header options that Kestrel Desk does not accept. groups.valuein the filterable attributes.userFilterreads members by filtering ongroups.value, and Owlie refuses a definition whoseuserFilterPathis not infiltering.attributes. The same rule holds for each install's copy of the list: Owlie refuses an install configuration that removesgroups.value, or the path of any identifier (userName,externalId,displayName), from it.elementExtrasonemail. 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
409for a duplicate and an existing member, and404for a missing resource and a missing member, as the standard does.
What Owlie derives
| Part | Derived |
|---|---|
Account entity user | Identifiers id (the SCIM id), userName, externalId; fields from the install's user field list |
Entity group | Identifiers id and displayName |
Grant kind member_of | A user is a member of a group; writable, one member per write |
| Authentication | One option, Bearer token: the install enters the token |
| Install fields | Region (default us), Workspace ID, User fields, Filterable attributes |
| Operations | Connection 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.