SCIM: users, groups, queries, PATCH, Bulk and cursors at /scim/v2, over x:Account (SCIM-1 to SCIM-57)

Every SCIM operation becomes the x:Account get, query or set JMAP makes,
as the service principal, so permissions, tenant scope and limits,
address uniqueness and account destruction are enforced in one place.
Discovery is anonymous; everything else takes an API key as a bearer
token and nothing else. Domains open to SCIM carry a flag in the domain
cache. Filters take eq and and, answered from the account indexes, with
unindexed attributes checked on at most 200 candidates. Cursors are
stateless, HMAC-sealed under the server key. PATCH applies to the
resource in memory and saves it as a PUT, so it is all or nothing.
Groups get an address from their display name on the principal's
domain; membership is written on each user.

Every write emits one of five new scim.* events (ids 637 to 641), also
added to the packaged schema. The helpers the surviving SCIM suites
import are rebuilt from the spec; scim_tests runs the new acceptance
suite and the surviving tenant isolation suite, and both pass.
This commit is contained in:
2026-09-19 09:35:23 -07:00
parent 776d18d06e
commit 0ca26070d7
28 changed files with 6141 additions and 22 deletions
+14 -5
View File
@@ -25,11 +25,13 @@ impl PatchPath {
let text = text.trim();
let (head, filter, after) = match text.find('[') {
Some(open) => {
let close = text.rfind(']').filter(|close| *close > open).ok_or_else(invalid)?;
let close = text
.rfind(']')
.filter(|close| *close > open)
.ok_or_else(invalid)?;
let inner = &text[open + 1..close];
let filter = Filter::parse(inner).map_err(|err| {
ScimError::invalid_path(format!("'{text}': {}", err.detail))
})?;
let filter = Filter::parse(inner)
.map_err(|err| ScimError::invalid_path(format!("'{text}': {}", err.detail)))?;
let after = &text[close + 1..];
let after = if after.is_empty() {
None
@@ -85,7 +87,14 @@ mod tests {
#[test]
fn refuses_bad_paths() {
for text in ["", "members[", "members[value eq]", "a[b eq 1]x", "1a", "a.b[c eq 1]"] {
for text in [
"",
"members[",
"members[value eq]",
"a[b eq 1]x",
"1a",
"a.b[c eq 1]",
] {
assert!(PatchPath::parse(text).is_err(), "{text}");
}
}