scim
SCIM 2.0 provisioning semantics — the PROVISIONING half of enterprise SSO, counterpart to cx-stdlib/oidc's login half. It is a pure semantics module, not a mounted service: a feature routes /Users and /Groups itself, with its own bearer check and its own tenancy, and calls these verbs for the part that is identical everywhere and hard everywhere. apply-patch is RFC 7644 section 3.5.2 in full — add appends to a multi-valued attribute and replaces a single-valued one, replace with no path merges, a path may carry a value filter, attribute names and op verbs fold case, removing what is absent is a no-op. parse-filter covers the whole section 3.4.2.2 grammar and returns a walkable element tree rather than a predicate, so a deployment can push a filter into its own store. validate returns the validated resource rather than a boolean beside it. Resources are ordinary CX maps and multi-valued attributes are arrays — exactly what cx-stdlib/json parse yields — so nothing is translated between the wire and these verbs. project narrows a response per RFC 7644 section 3.9, and each attribute's schema returned characteristic outranks the request in both directions — always survives any exclusion, and never leaves no response whatever was asked for, which is why the core password attribute cannot ride a patched echo back to the IdP; list-response projects every member of a page for the same reason. Schemas are VALUES: supplying a custom extension schema in opts is the entire cost of supporting it, projection included. Every verb is pure, so a whole provisioning exchange replays in a fixture.
scim:parse-filter
[$scim:parse-filter] -> element — Parse an RFC 7644 section 3.4.2.2 filter into a walkable element tree; raises CXER5601 on a malformed filter.
[?lib 'cx-stdlib/scim']
[?let [= $f [$scim:parse-filter 'userName eq "bjensen"']]
[$concat $f/compare/@op "|" $f/compare/path/@name "|" $f/compare/value]]
'eq|userName|bjensen'
scim:match-filter
[$scim:match-filter] -> bool — Answer whether a resource satisfies a parsed filter, honouring each attribute's schema caseExact.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[$scim:match-filter [$json:parse '{"userName":"BJensen"}']
[$scim:parse-filter 'userName eq "bjensen"'] {}]
true
scim:parse-path
[$scim:parse-path] -> element — Parse an RFC 7644 section 3.5.2 PATCH path — bare, dotted, or value-filtered — into the same element vocabulary filters use.
[?lib 'cx-stdlib/scim']
[$concat [$scim:parse-path 'userName']/path/@name "|"
[$scim:parse-path 'name.familyName']/path/@name "|"
[$scim:parse-path 'name.familyName']/path/@sub]
'userName|name|familyName'
scim:apply-patch
[$scim:apply-patch] -> map — Apply a whole RFC 7644 PatchOp request to a resource, purely — add appends to a multi-valued attribute and replaces a single-valued one.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[?let [= $r [$json:parse '{"userName":"bj","emails":[{"value":"a@x.com","type":"work"}]}']]
[= $p [$json:parse '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"add","path":"emails","value":[{"value":"b@x.com","type":"home"}]}]}']]
[$json:emit [$scim:apply-patch $r $p]]]
'{"emails":[{"type":"work","value":"a@x.com"},{"type":"home","value":"b@x.com"}],"userName":"bj"}'
scim:validate
[$scim:validate] -> map — Check a resource against the schemas it declares and return the VALIDATED resource — never a boolean beside an unchecked one.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[$json:emit [$scim:validate
[$json:parse '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"bj","active":true}'] {}]]
'{"active":true,"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"bj"}'
scim:schemas
[$scim:schemas] -> [sequence map] — The RFC 7643 schema resources this module ships — core User, core Group and the enterprise-user extension — as the values everything schema-driven reads.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/map']
[?lib 'cx-stdlib/strings']
[$strings:join [?for [in $s [$scim:schemas {}]] [yield [$map:get $s 'id']]] " "]
'urn:ietf:params:scim:schemas:core:2.0:User urn:ietf:params:scim:schemas:core:2.0:Group urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
scim:resource-types
[$scim:resource-types] -> [sequence map] — The /ResourceTypes discovery documents for User and Group, naming each endpoint and its schema extensions.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/map']
[?lib 'cx-stdlib/strings']
[$strings:join [?for [in $t [$scim:resource-types {}]]
[yield [$concat [$map:get $t 'id'] "=" [$map:get $t 'endpoint']]]] " "]
'User=/Users Group=/Groups'
scim:service-provider-config
[$scim:service-provider-config] -> map — The /ServiceProviderConfig document, telling the truth about this module: patch and filter supported, bulk and sort not.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/map']
[?let [= $c [$scim:service-provider-config {}]]
([$map:get [$map:get $c 'patch'] 'supported'],
[$map:get [$map:get $c 'filter'] 'supported'],
[$map:get [$map:get $c 'bulk'] 'supported'],
[$map:get [$map:get $c 'sort'] 'supported'])]
(true, true, false, false)
scim:project
[$scim:project] -> map — Narrow a response per RFC 7644 section 3.9 — attributes: or excluded-attributes: in opts — with each attribute's schema returned outranking the request: always survives any exclusion and never leaves no response, whatever was asked for.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[?let [= $p [$json:parse '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"replace","path":"password","value":"t1meMa$heen"}]}']]
[= $r [$json:parse '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"id":"2819c223","userName":"bjensen"}']]
[$json:emit [$scim:project [$scim:apply-patch $r $p] {}]]]
'{"id":"2819c223","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"bjensen"}'
scim:list-response
[$scim:list-response] -> map — Wrap a page of resources in the SCIM ListResponse envelope, reporting the store's own total rather than the page length.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[$json:emit [$scim:list-response ([$json:parse '{"id":"1"}'], [$json:parse '{"id":"2"}']) {}]]
'{"Resources":[{"id":"1"},{"id":"2"}],"itemsPerPage":2,"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],"startIndex":1,"totalResults":2}'
scim:version
[$scim:version] -> string — The resource's strong entity-tag per RFC 7232 — the canonical content's hash, quoted — so equal content is one version whatever order the keys arrived in and a changed attribute is a different one.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[?let [= $a [$json:parse '{"userName":"bj","active":true}']]
[= $b [$json:parse '{"active":true,"userName":"bj"}']]
[= $c [$json:parse '{"userName":"bj","active":false}']]
([= [$scim:version $a] [$scim:version $b]], [= [$scim:version $a] [$scim:version $c]])]
(true, false)
scim:stamp
[$scim:stamp] -> map — Complete a resource's meta — resourceType, location, created/lastModified FROM THE STORE RECORD (this module has no clock), and version as the strong ETag.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[?lib 'cx-stdlib/map']
[?let [= $r [$json:parse '{"id":"2819c223","userName":"bjensen"}']]
[= $s [$scim:stamp $r {'resource-type': 'User', 'base-url': 'https://x.example/scim/v2',
'created': '2026-01-01T00:00:00Z', 'last-modified': '2026-01-02T00:00:00Z'}]]
[= $m [$map:get $s 'meta']]
([= [$map:get $m 'resourceType'] 'User'],
[= [$map:get $m 'location'] 'https://x.example/scim/v2/Users/2819c223'],
[= [$map:get $m 'created'] '2026-01-01T00:00:00Z'],
[= [$map:get $m 'lastModified'] '2026-01-02T00:00:00Z'],
[= [$map:get $m 'version'] [$scim:version $s]])]
(true, true, true, true, true)
scim:precondition
[$scim:precondition] -> map — Answer the RFC 7232 preconditions over a resource: If-Match refuses a mismatch with CXER5608, If-None-Match answers unchanged, and a weak validator is declined — the 412/304 status is the mount's.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[?lib 'cx-stdlib/map']
[?let [= $r [$json:parse '{"id":"1","userName":"bj"}']]
[= $v [$scim:version $r]]
[= $p [$scim:precondition $r {'If-Match': $v}]]
([$map:get $p 'unchanged'], [= [$map:get $p 'version'] $v])]
(false, true)
scim:delete
[$scim:delete] -> map — The module's answer to DELETE: a refusal naming active: false, because hard delete is not offered in v0.18 and mapping DELETE onto the soft one would make a later GET of a supposedly deleted resource a lie.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[?let [= $e [$scim:delete {}]]
[$json:emit [$scim:error $e@code $e@message]]]
'{"detail":"scim/delete: DELETE is not offered - deprovision by setting active: false (RFC 7644; RULED: 1406-a)","schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"status":"501"}'
scim:error
[$scim:error] -> map — Project one of this module's errs into the RFC 7644 error object, with the scimType and the STRING status the RFC assigns.
[?lib 'cx-stdlib/scim']
[?lib 'cx-stdlib/json']
[$json:emit [$scim:error 'cx-err:CXER5602' 'no such attribute']]
'{"detail":"no such attribute","schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"scimType":"invalidPath","status":"400"}'