Skip to main content

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, whose default is the seed every install copies and may then edit. Only three values can be: users.fields, groups.fields and filtering.attributes. Every other value must be a literal, apart from baseUrl, 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​

FieldTypeRequiredDefaultWhat it does
engineintegeryesAlways 1, the version of the definition format.
descriptionstringnoWhat the catalog shows. Non-empty.
iconstringnoThe catalog icon: @ plus a domain, such as @kestreldesk.example, or an image URL. Non-empty.
configobjectnoThe install's configuration fields. Its one key is properties.
baseUrlstring or expressionyesThe SCIM base URL (Base URL).
authobjectnoall four optionsThe authentication options an install may choose (Authentication).
httpobjectnoContent type and static headers (Requests).
paginationobjectnoHow lists are paged (Pagination).
filteringobjectnoFilterable attributes and incremental Sync (Filtering).
patchobjectnoHow updates are sent (PATCH).
usersobjectnoThe Users resource (Users).
groupsobjectnoThe Groups resource and membership (Groups).
errorsobjectnoStatus 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 https URL; or
  • an expression, { "$oxl": "<expression>" }, in OXL over config, 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.

FieldTypeWhat it does
auth.bearerobject, no fieldsAllows a bearer token.
auth.basicobject, no fieldsAllows HTTP Basic authentication.
auth.oauth2-client-credentialsobjectAllows the OAuth 2.0 client credentials grant.
auth.oauth2-client-credentials.tokenUrlstringFixes the token URL. An absolute https URL.
auth.oauth2-client-credentials.scopestringFixes the scope.
auth.oauth2-client-credentials.audiencestringFixes the audience.
auth.custom-headerobjectAllows a secret sent in a custom header.
auth.custom-header.headerNamestringFixes 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 keyTitleInstall fieldsApplied as
bearerBearer tokentoken (secret, required)Authorization: Bearer <token>
basicBasic authusername, password (secret), both requiredHTTP Basic authentication
oauth2-client-credentialsOAuth 2.0 client credentialstokenUrl (unless fixed), clientId, clientSecret (secret), all required; scope and audience (unless fixed), optionalA token acquired from the token URL with the client credentials, renewed the same way, sent as Authorization: Bearer <access token>
custom-headerCustom header, or the fixed header nameheaderName (unless fixed) and headerValue (secret), requiredThe value in the named header

Requests​

FieldTypeDefaultWhat it does
http.contentTypeapplication/scim+json or application/jsonapplication/scim+jsonThe Content-Type of every request with a body. Responses of either type are read, whatever this says.
http.headersobject of stringsnoneStatic, non-secret headers sent on every request, keyed by header name. The same reserved names as headerName are refused.

Pagination​

FieldTypeDefaultWhat it does
pagination.stylestartIndex-count, offset-limit, cursor or nonestartIndex-countThe paging dialect.
pagination.pageSizeinteger, 1 to 1000100Records requested per page.
pagination.startIndexParamparameter namestartIndexstartIndex-count: the 1-based start parameter.
pagination.countParamparameter namecountstartIndex-count and cursor: the page size parameter.
pagination.offsetParamparameter nameoffsetoffset-limit: the 0-based offset parameter.
pagination.limitParamparameter namelimitoffset-limit: the page size parameter.
pagination.cursorParamparameter namecursorcursor: the parameter that carries the cursor.
pagination.nextCursorPathparameter namenextCursorcursor: the key of the list response that holds the next cursor.
pagination.emptyFirstCursorbooleanfalsecursor: 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.

FieldTypeDefaultEditableWhat it does
filtering.attributesarray of SCIM pathsany pathyesThe paths the server accepts in filters. Owlie refuses to send a filter on any other path.
filtering.incrementalobjectnoIncremental Sync of users.
filtering.incremental.enabledbooleanfalsenoTurn on incremental Sync. The user entity then supports incremental runs.
filtering.incremental.lastModifiedPathdotted pathmeta.lastModifiednoThe attribute that holds the last-modified time.
filtering.incremental.operatorgt or gegtnoThe 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​

FieldTypeDefaultWhat it does
patch.supportedbooleantrueWhether 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.selectorPathsbooleantrueWhether 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.opCaselower or titlelowerOperation names add, remove, replace, or Add, Remove, Replace.
patch.valueStylepath-value or objectpath-valuepath-value: { "op", "path", "value" } per field. object: { "op", "value" } with the value nested at the field's path.

Users​

FieldTypeDefaultEditableWhat it does
users.endpointendpoint/UsersnoThe Users resource, relative to the base URL.
users.idPathdotted pathidnoWhere a user's server-assigned id is read. The account's id identifier is provider-stable only when this is id.
users.fieldsobject of field bindingsthe default user fieldsyesThe user fields (Field bindings). A list replaces the defaults whole.
users.activeAttributeobjectnoThe attribute that enables and disables an account.
users.activeAttribute.pathdotted pathactivenoIts path.
users.activeAttribute.enabledValueany JSON valuetruenoThe value written to enable an account.
users.activeAttribute.disabledValueany JSON valuefalsenoThe 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​

FieldTypeDefaultEditableWhat it does
groups.enabledbooleantruenoWhether the connector has groups. When false, there is no group entity and no membership Grant kind.
groups.endpointendpoint/GroupsnoThe Groups resource.
groups.idPathdotted pathidnoWhere a group's id is read.
groups.fieldsobject of field bindingsdisplayName onlyyesThe group fields. A list replaces the default whole.
groups.manageobjectnoThe group writes Owlie may make.
groups.manage.createbooleanfalsenoAllow creating groups.
groups.manage.updatebooleanfalsenoAllow updating groups.
groups.manage.deletebooleanfalsenoAllow deleting groups.
groups.membershipobjectnoHow membership is read and written.
groups.membership.modepatchMembers or replaceGroupMemberspatchMembersnopatchMembers adds or removes one member with PATCH. replaceGroupMembers reads the group and PUTs its whole member list back, without If-Match.
groups.membership.addStylepath-value, object or bare-arraypath-valuenoThe shape of the PATCH add: { "op": "add", "path": "members", "value": [...] }, { "op": "add", "value": { "members": [...] } } or { "op": "add", "value": [...] }.
groups.membership.membersPathdotted pathmembersnoThe group attribute that lists members.
groups.membership.memberValueid, userName or externalIdidnoWhat 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.readMembersincluded, fetchGroup or userFilterfetchGroupnoHow Sync reads members: by reading each group (fetchGroup, included; they differ only in excludedAttributes on listings), or by filtering users (userFilter).
groups.membership.userFilterPathdotted pathgroups.valuenoWith userFilter: members are read as GET <users.endpoint>?filter=<userFilterPath> eq "<group id>".
groups.membership.listExcludedAttributesarray of parameter names["members"] with fetchGroup, else []noSent 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​

FieldTypeDefaultWhat it does
errors.conflictStatusinteger, 100 to 599409The status that means a duplicate. On create, the account already exists; on a member add, the member is already there; elsewhere, a conflict.
errors.notFoundStatusinteger, 100 to 599404The 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.rateLimitStatusinteger, 100 to 599429The status that means rate limited, reported as retryable. 401 and 403 always mean an authentication failure, whatever this is set to.
errors.idempotencyobjectWhether an "already done" answer is success.
errors.idempotency.assignExistingMembersuccess or failsuccessA member add answered with conflictStatus reports "already present" (success) or fails.
errors.idempotency.revokeMissingMembersuccess or failsuccessA member remove answered with notFoundStatus, on an existing group, reports "already absent" (success) or fails.
errors.idempotency.deleteMissingUsersuccess or failsuccessDeleting 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.

FieldTypeDefaultWhat it does
pathSCIM pathrequiredThe attribute the field reads and writes.
typestring, boolean, number or datetimestringThe field's type. datetime is a string in date-time format.
titlestringThe label Owlie shows. Non-empty.
identifierbooleanfalseThe field identifies a record and may be used to look one up.
requiredbooleanfalseThe field is required in the record schema.
sensitivebooleanfalseMask the value in logs.
readOnlybooleanfalseThe server sets the value; Owlie never writes it.
writeOnlybooleanfalseOwlie writes the value and never reads it back.
createOnlybooleanfalseThe value can be written only when the record is created.
referenceobjectThe value points at another user.
reference.attributestringrequired in referenceThe user attribute the value holds, such as externalId.
elementExtrasobjectConstants added to the list element created for a path with a filter, such as { "primary": true }. Keys are parameter names.
baselinestringAn 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​

ListFieldpathOther properties
usersuserNameuserNameidentifier, required, baseline: "identity.email"
usersexternalIdexternalIdidentifier
usersgivenNamename.givenNamebaseline: "identity.first_name"
usersfamilyNamename.familyNamebaseline: "identity.last_name"
usersdisplayNamedisplayNamebaseline: "identity.display_name"
usersemailemails[type eq "work"].valuebaseline: "identity.email"
usersactiveactivetype: "boolean", readOnly
userstitletitle
usersdepartmenturn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department
usersemployeeNumberurn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber
usersmanagerurn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.valuereference: { "attribute": "externalId" }
groupsdisplayNamedisplayNameidentifier

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:

  • engine is 1, baseUrl is 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 path is userName; with groups enabled, the group field list has one whose path is displayName.
  • An identifier field is neither writeOnly nor sensitive.
  • Every identifier field's path is in filtering.attributes when that list is set.
  • A required user field that is not readOnly has a baseline.
  • filtering.incremental.enabled needs its lastModifiedPath in filtering.attributes when that list is set.
  • readMembers: "userFilter" needs userFilterPath in filtering.attributes when that list is set.
  • memberValue other than id names a user field that is neither writeOnly nor sensitive.
  • mode: "patchMembers" needs patch.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 path changed 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​

PartDerived value
provides["sync", "provision"]
PlacementNo execution target: the connector runs in Owlie's cloud, or on a private-network gateway when an install is assigned one.
authOne option per allowed auth key, as in Authentication.
configconfig.properties, each required unless its default is "" or null.
Entity userThe account entity (is_account: true), named User.
Entity groupWith groups enabled, named Group.
Grant kind member_ofWith 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 when filtering.incremental.enabled is true, never supports_deleted;
  • a starting Sync profile: is_active is active != false, read from the record's active field whatever users.activeAttribute says, and one matching rule compares the identity's email with the record's email || userName;
  • a starting provisioning profile: a new account's attributes are each non-readOnly field's baseline, 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:

HandlerEnabled
connection.validateConfig, connection.testConnectionalways
user.fetchOne, user.fetchAll, user.create, user.update, user.delete, user.enable, user.disablealways
user.fetchAllSincewith filtering.incremental.enabled
group.fetchOne, group.fetchAll, group.fetchRelationshipswith groups enabled
group.create, group.update, group.deletewith groups enabled and the matching groups.manage flag
grant.member_of.add, grant.member_of.removewith 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.