SCIM definition
Every field of a SCIM connector's definition.json, the declaration Owlie derives from it, and the
rules Owlie checks. The definition describes a SCIM 2.0 endpoint as the standard's defaults plus the
vendor's deviations, so every field except engine and baseUrl is optional and defaults to the
standard's behavior. The machine-readable form is
scim.schema.json.
How to read the tables:
- Default is the value Owlie uses when the field is absent.
- Editable says whether the value can be install-editable: written as
{ "$oxl": "config.<name>" }naming a declared config field, whosedefaultis the seed every install copies and may then edit. Only three values can be:users.fields,groups.fieldsandfiltering.attributes. Every other value must be a literal, apart frombaseUrl, which is a full expression evaluated per install. - A key the format does not define is refused, at any level, so a misspelled field is an error rather than a silent default.
A complete definition is in Example: SCIM provider.
Top level
| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
engine | integer | yes | Always 1, the version of the definition format. | |
description | string | no | What the catalog shows. Non-empty. | |
icon | string | no | The catalog icon: @ plus a domain, such as @kestreldesk.example, or an image URL. Non-empty. | |
config | object | no | The install's configuration fields. Its one key is properties. | |
baseUrl | string or expression | yes | The SCIM base URL (Base URL). | |
auth | object | no | all four options | The authentication options an install may choose (Authentication). |
http | object | no | Content type and static headers (Requests). | |
pagination | object | no | How lists are paged (Pagination). | |
filtering | object | no | Filterable attributes and incremental Sync (Filtering). | |
patch | object | no | How updates are sent (PATCH). | |
users | object | no | The Users resource (Users). | |
groups | object | no | The Groups resource and membership (Groups). | |
errors | object | no | Status codes and "already done" outcomes (Errors). |
Config
config.properties is an object keyed by config field name, each value a JSON Schema object (type,
title, description, default, minLength, items and so on). The derived declaration's install
configuration is exactly these properties. Every property is required unless its default is "" or
null.
A property that an install-editable value reads must have the matching type: object for
users.fields and groups.fields, array for filtering.attributes. Its default is the seed.
Base URL
baseUrl is either:
- a literal string, which must be an absolute
httpsURL; or - an expression,
{ "$oxl": "<expression>" }, in OXL overconfig, the install's configuration values. The expression never sees credentials. It has one function besides the standard ones,encode(value), which URL-encodes a value as a path segment.
After evaluating it for an install, Owlie refuses the URL unless it is an absolute https URL whose
host is a DNS name containing at least one dot (not an IP address), with no user name or password, no
query string and no fragment, and whose path has no . or .. segment, no encoded slash (%2F,
%5C) and no backslash. Trailing slashes are removed. Owlie evaluates it when the install's
configuration is saved, once every config value the expression reads has a value, refusing a save that
gives no valid URL, and again for every operation.
encode turns a / in a value into %2F, which these checks refuse. A value placed in the path
without encode is not checked that way: a / in it adds path segments, and only a whole . or ..
segment is refused. A value placed in the host is never encoded or checked beyond the host rules, so
constrain it in its config schema, with an enum or a pattern.
The evaluated URL's host, and the host of a token URL the install enters for
oauth2-client-credentials, are the hosts the install's connector may reach.
Authentication
auth is an object whose keys are the allowed options. Absent, all four are allowed. An empty object
is refused.
| Field | Type | What it does |
|---|---|---|
auth.bearer | object, no fields | Allows a bearer token. |
auth.basic | object, no fields | Allows HTTP Basic authentication. |
auth.oauth2-client-credentials | object | Allows the OAuth 2.0 client credentials grant. |
auth.oauth2-client-credentials.tokenUrl | string | Fixes the token URL. An absolute https URL. |
auth.oauth2-client-credentials.scope | string | Fixes the scope. |
auth.oauth2-client-credentials.audience | string | Fixes the audience. |
auth.custom-header | object | Allows a secret sent in a custom header. |
auth.custom-header.headerName | string | Fixes the header name. A valid header name other than Authorization, Content-Type, Content-Length, Host or Cookie. |
Each allowed option becomes an authentication option of the derived declaration. A value the definition fixes is left out of the install form.
| Option key | Title | Install fields | Applied as |
|---|---|---|---|
bearer | Bearer token | token (secret, required) | Authorization: Bearer <token> |
basic | Basic auth | username, password (secret), both required | HTTP Basic authentication |
oauth2-client-credentials | OAuth 2.0 client credentials | tokenUrl (unless fixed), clientId, clientSecret (secret), all required; scope and audience (unless fixed), optional | A token acquired from the token URL with the client credentials, renewed the same way, sent as Authorization: Bearer <access token> |
custom-header | Custom header, or the fixed header name | headerName (unless fixed) and headerValue (secret), required | The value in the named header |
Requests
| Field | Type | Default | What it does |
|---|---|---|---|
http.contentType | application/scim+json or application/json | application/scim+json | The Content-Type of every request with a body. Responses of either type are read, whatever this says. |
http.headers | object of strings | none | Static, non-secret headers sent on every request, keyed by header name. The same reserved names as headerName are refused. |
Pagination
| Field | Type | Default | What it does |
|---|---|---|---|
pagination.style | startIndex-count, offset-limit, cursor or none | startIndex-count | The paging dialect. |
pagination.pageSize | integer, 1 to 1000 | 100 | Records requested per page. |
pagination.startIndexParam | parameter name | startIndex | startIndex-count: the 1-based start parameter. |
pagination.countParam | parameter name | count | startIndex-count and cursor: the page size parameter. |
pagination.offsetParam | parameter name | offset | offset-limit: the 0-based offset parameter. |
pagination.limitParam | parameter name | limit | offset-limit: the page size parameter. |
pagination.cursorParam | parameter name | cursor | cursor: the parameter that carries the cursor. |
pagination.nextCursorPath | parameter name | nextCursor | cursor: the key of the list response that holds the next cursor. |
pagination.emptyFirstCursor | boolean | false | cursor: send the cursor parameter present but empty on the first request. |
A parameter name is letters, digits, _, . and -. Paging stops at an empty page, and otherwise:
for startIndex-count when startIndex plus itemsPerPage exceeds totalResults; for
offset-limit when the offset reaches totalResults; for either, without totalResults, at a page
shorter than pageSize; for cursor at a response with no next cursor; for none after one request.
A list response without Resources, unless totalResults is 0, and a cursor that does not advance,
fail the operation.
Filtering
The server must support filter. Every identifier field, the required userName among them, is
looked up by filter, so there is no setting that turns filtering off.
| Field | Type | Default | Editable | What it does |
|---|---|---|---|---|
filtering.attributes | array of SCIM paths | any path | yes | The paths the server accepts in filters. Owlie refuses to send a filter on any other path. |
filtering.incremental | object | no | Incremental Sync of users. | |
filtering.incremental.enabled | boolean | false | no | Turn on incremental Sync. The user entity then supports incremental runs. |
filtering.incremental.lastModifiedPath | dotted path | meta.lastModified | no | The attribute that holds the last-modified time. |
filtering.incremental.operator | gt or ge | gt | no | The comparison the incremental filter uses. |
An incremental run filters users on <lastModifiedPath> <operator> "<marker>", where the marker is
the latest last-modified value the previous run saw. Groups are always read in full. Every filter
value is sent as a quoted, escaped string.
PATCH
| Field | Type | Default | What it does |
|---|---|---|---|
patch.supported | boolean | true | Whether the server accepts PATCH. Without it, updates and enable/disable read the resource, merge the change and PUT it back (with If-Match when the server gave an ETag). |
patch.selectorPaths | boolean | true | Whether PATCH accepts a path with a filter. When false, fields whose path has a filter are updated by the merged PUT; other fields still use PATCH. |
patch.opCase | lower or title | lower | Operation names add, remove, replace, or Add, Remove, Replace. |
patch.valueStyle | path-value or object | path-value | path-value: { "op", "path", "value" } per field. object: { "op", "value" } with the value nested at the field's path. |
Users
| Field | Type | Default | Editable | What it does |
|---|---|---|---|---|
users.endpoint | endpoint | /Users | no | The Users resource, relative to the base URL. |
users.idPath | dotted path | id | no | Where a user's server-assigned id is read. The account's id identifier is provider-stable only when this is id. |
users.fields | object of field bindings | the default user fields | yes | The user fields (Field bindings). A list replaces the defaults whole. |
users.activeAttribute | object | no | The attribute that enables and disables an account. | |
users.activeAttribute.path | dotted path | active | no | Its path. |
users.activeAttribute.enabledValue | any JSON value | true | no | The value written to enable an account. |
users.activeAttribute.disabledValue | any JSON value | false | no | The value written to disable an account. A lookup of one user reads it as disabled when its attribute equals this; Sync does not use it. |
An endpoint is a path such as /Users or /scim/Groups: one or more /-separated segments of
letters, digits, ., _, ~ and -, with no query and no . or .. segment. A dotted path is
attribute names joined by ., such as meta.lastModified, with no filter or URN.
Groups
| Field | Type | Default | Editable | What it does |
|---|---|---|---|---|
groups.enabled | boolean | true | no | Whether the connector has groups. When false, there is no group entity and no membership Grant kind. |
groups.endpoint | endpoint | /Groups | no | The Groups resource. |
groups.idPath | dotted path | id | no | Where a group's id is read. |
groups.fields | object of field bindings | displayName only | yes | The group fields. A list replaces the default whole. |
groups.manage | object | no | The group writes Owlie may make. | |
groups.manage.create | boolean | false | no | Allow creating groups. |
groups.manage.update | boolean | false | no | Allow updating groups. |
groups.manage.delete | boolean | false | no | Allow deleting groups. |
groups.membership | object | no | How membership is read and written. | |
groups.membership.mode | patchMembers or replaceGroupMembers | patchMembers | no | patchMembers adds or removes one member with PATCH. replaceGroupMembers reads the group and PUTs its whole member list back, without If-Match. |
groups.membership.addStyle | path-value, object or bare-array | path-value | no | The shape of the PATCH add: { "op": "add", "path": "members", "value": [...] }, { "op": "add", "value": { "members": [...] } } or { "op": "add", "value": [...] }. |
groups.membership.membersPath | dotted path | members | no | The group attribute that lists members. |
groups.membership.memberValue | id, userName or externalId | id | no | What a member's value holds: the user's SCIM id, or the user field of that name, read from the user's record before the write. |
groups.membership.readMembers | included, fetchGroup or userFilter | fetchGroup | no | How Sync reads members: by reading each group (fetchGroup, included; they differ only in excludedAttributes on listings), or by filtering users (userFilter). |
groups.membership.userFilterPath | dotted path | groups.value | no | With userFilter: members are read as GET <users.endpoint>?filter=<userFilterPath> eq "<group id>". |
groups.membership.listExcludedAttributes | array of parameter names | ["members"] with fetchGroup, else [] | no | Sent as excludedAttributes when groups are listed. |
A remove is always a PATCH remove with the path <membersPath>[value eq "<member>"], or, with
replaceGroupMembers, a PUT of the group without the member.
Errors
| Field | Type | Default | What it does |
|---|---|---|---|
errors.conflictStatus | integer, 100 to 599 | 409 | The status that means a duplicate. On create, the account already exists; on a member add, the member is already there; elsewhere, a conflict. |
errors.notFoundStatus | integer, 100 to 599 | 404 | The status that means not found. A read answering it finds nothing; on a member remove, the member is not there once the group is confirmed to exist. |
errors.rateLimitStatus | integer, 100 to 599 | 429 | The status that means rate limited, reported as retryable. 401 and 403 always mean an authentication failure, whatever this is set to. |
errors.idempotency | object | Whether an "already done" answer is success. | |
errors.idempotency.assignExistingMember | success or fail | success | A member add answered with conflictStatus reports "already present" (success) or fails. |
errors.idempotency.revokeMissingMember | success or fail | success | A member remove answered with notFoundStatus, on an existing group, reports "already absent" (success) or fails. |
errors.idempotency.deleteMissingUser | success or fail | success | Deleting a user who cannot be found succeeds (success) or fails. |
Other statuses map the same way for every definition: 401 and 403 fail as authentication errors,
any 5xx and 412 are reported as retryable, 400 fails as an invalid value, and any other 4xx
fails. The rule for "already present" and "already absent" is in the
Grant write contract.
Field bindings
users.fields and groups.fields are objects keyed by field name. A field name is letters, digits
and _, starting with a letter or _; id is reserved, because every record's id is read from
idPath.
| Field | Type | Default | What it does |
|---|---|---|---|
path | SCIM path | required | The attribute the field reads and writes. |
type | string, boolean, number or datetime | string | The field's type. datetime is a string in date-time format. |
title | string | The label Owlie shows. Non-empty. | |
identifier | boolean | false | The field identifies a record and may be used to look one up. |
required | boolean | false | The field is required in the record schema. |
sensitive | boolean | false | Mask the value in logs. |
readOnly | boolean | false | The server sets the value; Owlie never writes it. |
writeOnly | boolean | false | Owlie writes the value and never reads it back. |
createOnly | boolean | false | The value can be written only when the record is created. |
reference | object | The value points at another user. | |
reference.attribute | string | required in reference | The user attribute the value holds, such as externalId. |
elementExtras | object | Constants added to the list element created for a path with a filter, such as { "primary": true }. Keys are parameter names. | |
baseline | string | An OXL expression over the identity, such as identity.email: the value a new account gets. |
A SCIM path is a dotted attribute path (name.givenName), optionally prefixed by an extension schema
URN (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department), with at most one
filter of the form attribute[subAttribute eq "value"] (emails[type eq "work"].value). The only
filter operator is eq, and the value is one double-quoted string. A list path without a filter,
such as emails.value, reads the primary element or else the first, and writes a one-element list
for the standard multi-valued attributes emails, phoneNumbers, ims, photos, addresses,
entitlements, roles, groups, members and x509Certificates.
Default field lists
| List | Field | path | Other properties |
|---|---|---|---|
| users | userName | userName | identifier, required, baseline: "identity.email" |
| users | externalId | externalId | identifier |
| users | givenName | name.givenName | baseline: "identity.first_name" |
| users | familyName | name.familyName | baseline: "identity.last_name" |
| users | displayName | displayName | baseline: "identity.display_name" |
| users | email | emails[type eq "work"].value | baseline: "identity.email" |
| users | active | active | type: "boolean", readOnly |
| users | title | title | |
| users | department | urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department | |
| users | employeeNumber | urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber | |
| users | manager | urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value | reference: { "attribute": "externalId" } |
| groups | displayName | displayName | identifier |
Each field also has the title Owlie shows by default, such as Username or Work email.
Rules Owlie checks
Owlie refuses a definition, or an install's configuration, that breaks any of these:
engineis1,baseUrlis present, and every value has its documented type and range.auth, when present, allows at least one option.- The user field list has an identifier field whose
pathisuserName; with groups enabled, the group field list has one whosepathisdisplayName. - An identifier field is neither
writeOnlynorsensitive. - Every identifier field's
pathis infiltering.attributeswhen that list is set. - A
requireduser field that is notreadOnlyhas abaseline. filtering.incremental.enabledneeds itslastModifiedPathinfiltering.attributeswhen that list is set.readMembers: "userFilter"needsuserFilterPathinfiltering.attributeswhen that list is set.memberValueother thanidnames a user field that is neitherwriteOnlynorsensitive.mode: "patchMembers"needspatch.supported.- An install-editable value is exactly
{ "$oxl": "config.<name>" }, naming a declared config field of the matching type. - An install's own field list keeps the seed's identifier fields: Owlie adds back any the install
left out, in the seed's order, and refuses one whose
pathchanged or that is no longer an identifier, and any field the install newly marks as an identifier. - For an install, the evaluated base URL passes the checks in Base URL.
The derived declaration
| Part | Derived value |
|---|---|
provides | ["sync", "provision"] |
| Placement | No execution target: the connector runs in Owlie's cloud, or on a private-network gateway when an install is assigned one. |
auth | One option per allowed auth key, as in Authentication. |
config | config.properties, each required unless its default is "" or null. |
Entity user | The account entity (is_account: true), named User. |
Entity group | With groups enabled, named Group. |
Grant kind member_of | With groups enabled: source user, target group, discovered_via group, fetch_mode per-entity, membership: true, writable: true. |
Each entity's record schema has a read-only id identifier first, then one property per field
binding. A binding compiles to a property of its type (datetime as a date-time string) with its
title, and: identifier to x-identifier, sensitive to x-sensitive, readOnly and
writeOnly as themselves, createOnly to x-provisioning: { "writableOn": ["PROVISION"] }, and
reference to x-reference: { "entity": "user", "attribute": ... }. required fields are listed in
the schema's required. The id property is also x-provider-stable-id for users when
users.idPath is id. identifier_paths is id followed by the identifier fields in order.
The user entity also gets:
sync_support: incremental whenfiltering.incremental.enabledistrue, neversupports_deleted;- a starting Sync profile:
is_activeisactive != false, read from the record'sactivefield whateverusers.activeAttributesays, and one matching rule compares the identity'semailwith the record'semail || userName; - a starting provisioning profile: a new account's attributes are each non-
readOnlyfield'sbaseline, created directly, and a revoke removes managed Grants and disables the account.
The group entity never supports incremental Sync.
Operations
The engine enables exactly these handlers, and an override may replace any of them:
| Handler | Enabled |
|---|---|
connection.validateConfig, connection.testConnection | always |
user.fetchOne, user.fetchAll, user.create, user.update, user.delete, user.enable, user.disable | always |
user.fetchAllSince | with filtering.incremental.enabled |
group.fetchOne, group.fetchAll, group.fetchRelationships | with groups enabled |
group.create, group.update, group.delete | with groups enabled and the matching groups.manage flag |
grant.member_of.add, grant.member_of.remove | with groups enabled |
connection.validateConfig resolves the definition with the install's values and evaluates the base
URL. connection.testConnection lists one user, and one group when groups are enabled. A create sends
the record's field values with a schemas attribute that names the resource's core schema
(urn:ietf:params:scim:schemas:core:2.0:User or urn:ietf:params:scim:schemas:core:2.0:Group), then
each extension schema a written field's path is under, once each. A PATCH carries the standard
PatchOp schema. A PUT (an update without PATCH, or a replaceGroupMembers membership change)
sends the schemas the server returned for the resource, with the core schema first if that list
lacks it, then each extension object the body carries that the list does not name yet, whether a
written field's path is under it or the server returned it without listing its schema.