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
+8
View File
@@ -796,6 +796,14 @@ impl AccessToken {
}
impl AccessTokenInner {
/// inbuxa: SCIM-27: the account's own effective permission, from its
/// roles, its own settings and its tenant, before a credential narrows it
pub fn account_has_permission(&self, permission: Permission) -> bool {
self.scopes
.first()
.is_some_and(|scope| scope.permissions.get(permission as usize))
}
pub fn from_id(account_id: u32) -> Self {
Self {
account_id,
+6 -1
View File
@@ -69,7 +69,8 @@ pub struct DomainCache {
pub const DOMAIN_FLAG_RELAY: u8 = 1;
pub const DOMAIN_FLAG_SUB_ADDRESSING: u8 = 1 << 1;
// inbuxa: SCIM-15, SCIM-58
pub const DOMAIN_FLAG_SCIM: u8 = 1 << 2;
#[derive(Debug, Clone, Default)]
pub struct AccountCache {
@@ -329,4 +330,8 @@ impl DomainCache {
self.names.first().map(|s| s.as_ref()).unwrap_or_default()
}
// inbuxa: SCIM-15, SCIM-58
pub fn allows_scim(&self) -> bool {
self.flags & DOMAIN_FLAG_SCIM != 0
}
}
+5 -1
View File
@@ -158,7 +158,11 @@ impl Server {
if domain.allow_relaying {
flags |= DOMAIN_FLAG_RELAY;
}
// inbuxa: SCIM-15, SCIM-58: the domain is open to SCIM, and SCIM is
// authoritative for its accounts
if domain.allow_scim_provisioning {
flags |= crate::auth::DOMAIN_FLAG_SCIM;
}
let sub_addressing_custom = match domain.sub_addressing {
SubAddressing::Enabled => {