Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-grpc/src/service/sso.rs

//! `Sso`: company single sign-on, SCIM and the CSV import (see `sso.proto`
//! and [`crate::sso`]).
//!
//! Two kinds of caller. The ACCOUNT ADMIN configures (Admin over the
//! account's namespace — `Accounts.EnrollEmployee`'s rule). The RELAY — the
//! console's own credential, a server admin — hands over what the browser
//! and the IdP said, exactly as it does for `Access.WebLogin`; gunnar verifies
//! it, decides the person and enrols the session key.
//!
//! Every login, every provisioning write and every configuration change is an
//! audit record (the ledger and `gunnar::audit`); no secret — a client
//! secret, a SCIM bearer, a code, a nonce, an assertion — is in any of them.

use std::sync::Arc;

use gunnar_proto::sso_server::Sso;
use gunnar_proto::{
    BeginRequest, BeginResponse, CreateScimTokenRequest, CreateScimTokenResponse, DirectoryUser,
    DiscoverRequest, DiscoverResponse, FinishRequest, FinishResponse, GetProviderRequest,
    GetProviderResponse, GrantLevel, ImportStatusRequest, ImportUsersRequest, ImportUsersResponse,
    ListDirectoryRequest, ListDirectoryResponse, ListScimTokensRequest, ListScimTokensResponse,
    ProviderView, RebindUsersRequest, RebindUsersResponse, RemoveProviderRequest,
    RemoveProviderResponse, RevokeScimTokenRequest, RevokeScimTokenResponse, ScimRequest,
    ScimResponse, SecondFactorNeed, SecondFactorRequest, SecondFactorResponse, SetProviderRequest,
    SetProviderResponse, SetUserActiveRequest, SetUserActiveResponse, SsoProtocol, SsoProvider,
    VerifyDomainRequest, VerifyDomainResponse,
};
use tonic::{Request, Response, Status};

use crate::auth::{Access as Scope, AuditRecord, Caller, AUDIT_TARGET};
use crate::service::SsoService;
use crate::sso::directory::{normalize_domain, normalize_user_name};
use crate::sso::plane::{Room, SecretChange, SsoLoginAsk, SsoPlane, UserPatch};
use crate::sso::{oidc, saml, scim, Second, SCIM_PER_MINUTE, SECOND_TTL_MS};

fn now_ms() -> i64 {
    crate::bus::now_unix_ms()
}

fn account_of(raw: &str) -> Result<String, Status> {
    let account = raw.trim();
    if account.is_empty() || account.len() > 128 || account.contains('/') {
        return Err(Status::invalid_argument(
            "`account` is required (an account name)",
        ));
    }
    Ok(account.to_owned())
}

fn origin_ok(origin: &str, loopback: bool) -> bool {
    let o = origin.trim().trim_end_matches('/');
    let rest = if let Some(r) = o.strip_prefix("https://") {
        r
    } else if loopback {
        match o.strip_prefix("http://") {
            Some(r) => r,
            None => return false,
        }
    } else {
        return false;
    };
    !rest.is_empty() && !rest.contains(['/', '?', '#', '@', ' '])
}

fn same_origin(a: &str, b: &str) -> bool {
    a.trim()
        .trim_end_matches('/')
        .eq_ignore_ascii_case(b.trim().trim_end_matches('/'))
}

fn protocol_of(p: &SsoProvider) -> SsoProtocol {
    SsoProtocol::try_from(p.protocol).unwrap_or(SsoProtocol::Unspecified)
}

fn hash(secret: &str) -> String {
    crate::webauthn::sha256_hex(secret.as_bytes())
}

/// A list of names an admin typed: trimmed, deduplicated, each `[A-Za-z0-9_:./-]`.
fn tokens(list: &[String], what: &str, most: usize) -> Result<Vec<String>, Status> {
    let mut out: Vec<String> = Vec::new();
    for raw in list {
        let t = raw.trim();
        if t.is_empty() {
            continue;
        }
        if t.len() > 256
            || !t
                .bytes()
                .all(|b| b.is_ascii_alphanumeric() || b"_:./-".contains(&b))
        {
            return Err(Status::invalid_argument(format!(
                "{what} {t:?} is not a plain name"
            )));
        }
        if !out.iter().any(|o| o == t) {
            out.push(t.to_owned());
        }
    }
    if out.len() > most {
        return Err(Status::invalid_argument(format!("at most {most} {what}s")));
    }
    Ok(out)
}

/// **The account's own IdP CA for this save** (owner 2026-10-06): pasted —
/// judged and set; not resent — the stored one kept; cleared on ask. Answers
/// the CA (DER) and the audit detail (` idp_ca=<fingerprints> (<change>)`).
/// One CA per account (`OidcSettings.idp_ca_pem`, the one CA field): an OIDC
/// provider's calls trust it, and so does a SAML provider's metadata fetch
/// (A34-41).
fn idp_ca_for(
    stored: Option<&SsoProvider>,
    pasted: &str,
    clear: bool,
) -> Result<(Vec<Vec<u8>>, String), Status> {
    let stored_ca = stored
        .and_then(|p| p.oidc.as_ref())
        .map(oidc::ca_of)
        .unwrap_or_default();
    let (ca, change) = match (clear, pasted.trim()) {
        (true, _) => (Vec::new(), "cleared"),
        (false, "") => (stored_ca, "kept"),
        (false, pem) => (
            crate::sso::idp_ca::parse_idp_ca(pem, now_ms())
                .map_err(|why| Status::invalid_argument(format!("the IdP CA: {why}")))?,
            "set",
        ),
    };
    let said = format!(
        " idp_ca={} ({change})",
        if ca.is_empty() {
            "none".to_owned()
        } else {
            ca.iter()
                .map(|d| saml::fingerprint(d))
                .collect::<Vec<_>>()
                .join(",")
        }
    );
    Ok((ca, said))
}

/// `field` as sent, or — sent empty — the stored one (A34-22, A34-48: what
/// the page does not resend is kept).
fn or_stored(field: &str, stored: Option<&str>) -> String {
    match field.trim() {
        "" => stored.unwrap_or_default().to_owned(),
        sent => sent.to_owned(),
    }
}

fn claim_name(raw: &str, what: &str) -> Result<String, Status> {
    let t = raw.trim();
    if t.len() > 256 || t.chars().any(char::is_control) {
        return Err(Status::invalid_argument(format!(
            "{what} is not a claim or attribute name"
        )));
    }
    Ok(t.to_owned())
}

impl SsoService {
    fn plane(&self) -> Result<&dyn SsoPlane, Status> {
        self.ctx.control.sso().ok_or_else(|| {
            Status::unimplemented(
                "single sign-on needs an identity table; this control plane has none",
            )
        })
    }

    /// The account admin's door. Authenticated FIRST (an unauthenticated
    /// caller is UNAUTHENTICATED whatever the request says), then the account
    /// read out of the request, then the account-admin rule.
    /// [`Self::admin`], and **a fresh user-verified touch** (A34-10): the
    /// caller's identity-core session stepped up (a user-verified touch of
    /// their own passkey) within the window. A stolen admin session cannot point the account at a
    /// hostile IdP, drop it, mint a SCIM bearer or import a directory. Refused
    /// with `[stepup]`, which the console answers with a touch in place.
    fn admin_stepped_up<T>(
        &self,
        request: &Request<T>,
        account: impl FnOnce(&T) -> &str,
        method: &'static str,
    ) -> Result<(Caller, String), Status> {
        let (caller, account) = self.admin(request, account, method)?;
        if !crate::auth::stepped_up(request) {
            self.audit(
                method,
                &caller.principal_id,
                &account,
                false,
                "sso.step_up_refused",
                "[stepup] no fresh user-verified touch",
            );
            return Err(Status::permission_denied(format!(
                "[stepup] {method} needs a fresh touch of your own passkey (user-verified, within the last \
                 10 minutes)"
            )));
        }
        Ok((caller, account))
    }

    fn admin<T>(
        &self,
        request: &Request<T>,
        account: impl FnOnce(&T) -> &str,
        method: &'static str,
    ) -> Result<(Caller, String), Status> {
        let caller = self.ctx.caller(request)?;
        let account = account_of(account(request.get_ref()))?;
        self.ctx
            .require(
                &caller,
                Scope::namespace(&account),
                GrantLevel::Admin,
                method,
            )
            .map_err(|status| {
                crate::service::repos::account_admin_only_for(
                    status,
                    &account,
                    "the identity provider and directory of account",
                )
            })?;
        // **A clan is no account** (clans + SSO, 2026-10-05). A clan's admin
        // holds Admin over the clan's namespace — the rule above — but an
        // identity provider, a SCIM bearer or an import there would make the
        // people they let in EMPLOYEES of the clan, and a clan's members are
        // accounts of their own. Refused as `Accounts.EnrollEmployee` refuses
        // it, and only after the admin rule, so an outsider learns nothing
        // about the name.
        if let Some(clans) = self.ctx.control.clans() {
            if clans.clan_get(&account)?.is_some() {
                return Err(Status::failed_precondition(format!(
                    "{account} is a clan, not an account: a clan has no identity provider, \
                     SCIM bearer or directory — its members are accounts of their own, and \
                     each account's admin configures its own"
                )));
            }
        }
        Ok((caller, account))
    }

    /// The relay's door: the console's own credential.
    fn relay<T>(&self, request: &Request<T>, method: &'static str) -> Result<Caller, Status> {
        self.ctx
            .authorize(request, Scope::Server, GrantLevel::Admin, method)
    }

    /// One audit record: the ledger, and `gunnar::audit`.
    fn audit(
        &self,
        method: &'static str,
        principal: &str,
        account: &str,
        allowed: bool,
        event: &'static str,
        reason: &str,
    ) {
        let scope = format!("account:{account}");
        if let Some(ledger) = self.ctx.audit.as_deref() {
            ledger.record(AuditRecord {
                method,
                principal,
                fingerprint: "",
                scope: &scope,
                level: "",
                auth_mode: self.ctx.config.auth.as_str(),
                decision: if allowed { "allow" } else { "deny" },
                reason,
            });
        }
        if allowed {
            tracing::info!(target: AUDIT_TARGET, event, method, principal, account, reason, "single sign-on");
        } else {
            tracing::warn!(target: AUDIT_TARGET, event, method, principal, account, reason, "single sign-on refused");
        }
        // **The ACCOUNT's own log** (the owner's enterprise list, 2026-10-05):
        // every step of its single sign-on is its admin's to read — a sign-in
        // is a `login` like a passkey's, a SCIM or CSV change is its member's.
        if !account.is_empty() {
            let (kind, target) = match event {
                "sso.login" | "sso.login_refused" => ("login", "sso"),
                "sso.second_factor" | "sso.second_factor_refused" => ("login.second_factor", "sso"),
                "sso.user_deactivated" => ("member.deactivated", "sso"),
                "sso.user_reactivated" => ("member.reactivated", "sso"),
                "sso.scim" | "sso.scim_refused" => ("member.scim", "scim"),
                "sso.csv_import" => ("member.imported", "csv"),
                other => (other, "sso"),
            };
            self.ctx.account_event(
                Some(account.to_owned()),
                crate::enterprise::AccountAuditEvent {
                    kind: kind.to_owned(),
                    actor: principal.to_owned(),
                    actor_machine: false,
                    target: target.to_owned(),
                    detail: reason.to_owned(),
                    refused: !allowed,
                },
            );
        }
    }

    fn room(&self, account: &str) -> Result<Room, Status> {
        let (cap, stored) = match crate::service::accounts::account_room(&self.ctx, account)? {
            Some((cap, stored)) => (Some(cap), stored),
            None => (None, 0),
        };
        Ok(Room {
            room_bytes: crate::service::accounts::ROOM_PER_USER_BYTES,
            cap,
            stored,
        })
    }

    /// The provider as an answer: secrets never, certificates as fingerprints.
    fn view(&self, provider: SsoProvider) -> ProviderView {
        let mut shown = provider.clone();
        if let Some(s) = shown.saml.as_mut() {
            s.idp_certificates = saml::certificates(s)
                .unwrap_or_default()
                .iter()
                .map(|der| saml::fingerprint(der))
                .collect();
            s.idp_metadata_xml.clear();
        }
        // The account's IdP CA: its fingerprints, never the certificates.
        if let Some(o) = shown.oidc.as_mut() {
            o.idp_ca_fingerprints = oidc::ca_of(o)
                .iter()
                .map(|d| saml::fingerprint(d))
                .collect();
            o.idp_ca_pem.clear();
        }
        let origin = provider.console_origin.clone();
        let is_saml = protocol_of(&provider) == SsoProtocol::Saml;
        ProviderView {
            redirect_uri: oidc::gunnar_core_paths::redirect_uri(&origin),
            sp_entity_id: oidc::gunnar_core_paths::sp_entity_id(&origin, &provider.account),
            acs_url: oidc::gunnar_core_paths::acs_url(&origin),
            sp_metadata_xml: if is_saml {
                saml::sp_metadata(&provider)
            } else {
                String::new()
            },
            domain_proofs: provider
                .domains
                .iter()
                .map(|d| gunnar_proto::DomainProof {
                    domain: d.clone(),
                    txt_name: crate::sso::domains::txt_name(d),
                    txt_value: crate::sso::domains::proof_value(&provider.account, d),
                    verified: provider.verified_domains.contains(d),
                })
                .collect(),
            provider: Some(shown),
        }
    }

    fn directory_user(user: crate::sso::plane::DirUser) -> DirectoryUser {
        user.user
    }
}

/// Why no domain proof is read on a box fed no resolver (RA5-24).
pub const NO_RESOLVER: &str = "feed sso_dns_resolver: no DNS-over-HTTPS resolver is named for domain proofs, and none is dialled by default (the operator names the one this box may ask)";

#[tonic::async_trait]
impl Sso for SsoService {
    /// **Configure the account's identity provider** — the account admin's.
    async fn set_provider(
        &self,
        request: Request<SetProviderRequest>,
    ) -> Result<Response<SetProviderResponse>, Status> {
        const METHOD: &str = "Sso.SetProvider";
        let (caller, account) = self.admin_stepped_up(
            &request,
            |r: &SetProviderRequest| r.provider.as_ref().map_or("", |p| p.account.as_str()),
            METHOD,
        )?;
        let input = request
            .get_ref()
            .provider
            .clone()
            .ok_or_else(|| Status::invalid_argument("`provider` is required"))?;
        let plane = self.plane()?;
        let request = request.into_inner();
        let loopback = self.ctx.sso.allow_loopback_http();
        let protocol = protocol_of(&input);
        if !origin_ok(&input.console_origin, loopback) {
            return Err(Status::invalid_argument(
                "`console_origin` is the console's https origin (https://host[:port])",
            ));
        }
        let display_name = match input.display_name.trim() {
            "" => account.clone(),
            d if d.chars().count() <= 80 && !d.chars().any(char::is_control) => d.to_owned(),
            _ => {
                return Err(Status::invalid_argument(
                    "`display_name` is at most 80 printable characters",
                ))
            }
        };
        let mut domains = Vec::new();
        for raw in &input.domains {
            if raw.trim().is_empty() {
                continue;
            }
            let d = normalize_domain(raw).ok_or_else(|| {
                Status::invalid_argument(format!("{raw:?} is not an email domain"))
            })?;
            // A public mail domain leads to no company (A34-5).
            crate::sso::domains::admissible(&d)
                .map_err(|r| Status::invalid_argument(r.message()))?;
            if !domains.contains(&d) {
                domains.push(d);
            }
        }
        if domains.len() > 20 {
            return Err(Status::invalid_argument("at most 20 domains"));
        }
        let stored = plane.provider(&account)?;
        let mut provider = SsoProvider {
            account: account.clone(),
            protocol: protocol as i32,
            display_name,
            domains,
            oidc: None,
            saml: None,
            require_sso: input.require_sso,
            require_passkey: input.require_passkey,
            console_origin: input.console_origin.trim().trim_end_matches('/').to_owned(),
            has_client_secret: false,
            updated_at_unix_ms: now_ms(),
            updated_by: caller.principal_id.clone(),
            verified_domains: Vec::new(),
            jit: input.jit,
        };
        let secret;
        let ca_said;
        match protocol {
            SsoProtocol::Oidc => {
                let o = input
                    .oidc
                    .clone()
                    .ok_or_else(|| Status::invalid_argument("an OIDC provider needs `oidc`"))?;
                let issuer = o.issuer.trim().trim_end_matches('/').to_owned();
                crate::sso::url_allowed_in(
                    &oidc::discovery_url(&issuer),
                    loopback,
                    &self.ctx.sso.private_nets(),
                )
                .map_err(|why| Status::invalid_argument(format!("the issuer: {why}")))?;
                // **The account's own IdP CA**: trusted for THIS provider's calls alone.
                let (ca, said) = idp_ca_for(stored.as_ref(), &o.idp_ca_pem, request.clear_idp_ca)?;
                ca_said = said;
                let stored_oidc = stored.as_ref().and_then(|p| p.oidc.as_ref());
                let client_id = o.client_id.trim().to_owned();
                if client_id.is_empty() || client_id.len() > 512 {
                    return Err(Status::invalid_argument("the client id is required"));
                }
                provider.oidc = Some(gunnar_proto::OidcSettings {
                    issuer: issuer.clone(),
                    client_id,
                    scopes: tokens(&o.scopes, "scope", 20)?,
                    username_claim: claim_name(&o.username_claim, "the user-name claim")?,
                    // Not on the page: sent empty, the stored one is kept (A34-48).
                    display_name_claim: claim_name(
                        &or_stored(
                            &o.display_name_claim,
                            stored_oidc.map(|s| s.display_name_claim.as_str()),
                        ),
                        "the display-name claim",
                    )?,
                    idp_ca_pem: crate::sso::idp_ca::idp_ca_pem(&ca),
                    idp_ca_fingerprints: Vec::new(),
                });
                secret = match (request.clear_client_secret, request.client_secret.trim()) {
                    (true, _) => SecretChange::Clear,
                    (false, "") => SecretChange::Keep,
                    (false, s) if s.len() > 4096 => {
                        return Err(Status::invalid_argument(
                            "the client secret is longer than 4 KiB",
                        ))
                    }
                    (false, s) => SecretChange::Set(s.to_owned()),
                };
                // The issuer is read once now, so a typo is said to the admin
                // here and not to every employee at the login page.
                // Only while the operator turned SSO's network on (A34-16); an airgap box
                // stores the provider unread, and its sign-ins are refused by name.
                if let Ok(egress) = self.ctx.sso.egress() {
                    oidc::idp(&self.ctx.sso, egress, &issuer, &ca, now_ms(), true)
                        .await
                        .map_err(|why| {
                            Status::failed_precondition(format!(
                                "the issuer could not be read: {why}"
                            ))
                        })?;
                }
            }
            SsoProtocol::Saml => {
                let s = input
                    .saml
                    .clone()
                    .ok_or_else(|| Status::invalid_argument("a SAML provider needs `saml`"))?;
                let mut s = s;
                let stored_saml = stored.as_ref().and_then(|p| p.saml.as_ref());
                // What the admin did not resend is the stored one (A34-22: the
                // page cannot send the XML back, nor the certificates — an
                // answer shows fingerprints, never the certificate): with no
                // metadata in this save, the stored entity id, sign-on URL and
                // certificates stand, so the toggles can be saved alone.
                let no_metadata =
                    s.idp_metadata_xml.trim().is_empty() && s.idp_metadata_url.trim().is_empty();
                if no_metadata {
                    s.idp_entity_id = or_stored(
                        &s.idp_entity_id,
                        stored_saml.map(|p| p.idp_entity_id.as_str()),
                    );
                    s.idp_sso_url =
                        or_stored(&s.idp_sso_url, stored_saml.map(|p| p.idp_sso_url.as_str()));
                }
                let keep_certs =
                    s.idp_certificates.iter().all(|c| c.starts_with("SHA256:")) && no_metadata;
                if keep_certs {
                    s.idp_certificates = stored_saml.map(|p| {
                        p.idp_certificates.iter().map(|b64| format!("-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----")).collect()
                    }).unwrap_or_default();
                }
                // The account's IdP CA (A34-41): the metadata URL is read
                // trusting it, as an OIDC provider's calls are.
                let pasted = input.oidc.as_ref().map_or("", |o| o.idp_ca_pem.as_str());
                let (ca, said) = idp_ca_for(stored.as_ref(), pasted, request.clear_idp_ca)?;
                ca_said = said;
                let egress = self
                    .ctx
                    .sso
                    .egress()
                    .ok()
                    .map(|e| saml::trusting(e, ca.clone()));
                let mut resolved = saml::resolve_input(&self.ctx.sso, egress, &s)
                    .await
                    .map_err(Status::invalid_argument)?;
                // Not on the page: sent empty, the stored one is kept (A34-48).
                resolved.display_name_attribute = or_stored(
                    &resolved.display_name_attribute,
                    stored_saml.map(|p| p.display_name_attribute.as_str()),
                );
                provider.saml = Some(resolved);
                // A SAML provider keeps the account's CA in the one CA field.
                provider.oidc = (!ca.is_empty()).then(|| gunnar_proto::OidcSettings {
                    idp_ca_pem: crate::sso::idp_ca::idp_ca_pem(&ca),
                    ..Default::default()
                });
                secret = SecretChange::Clear;
            }
            SsoProtocol::Unspecified => {
                return Err(Status::invalid_argument("`protocol` is OIDC or SAML"))
            }
        }
        let saved = plane
            .set_provider(provider, secret, &caller.principal_id)
            .map_err(|err| {
                self.audit(
                    METHOD,
                    &caller.principal_id,
                    &account,
                    false,
                    "sso.provider_refused",
                    &err.to_string(),
                );
                Status::from(err)
            })?;
        self.audit(
            METHOD,
            &caller.principal_id,
            &account,
            true,
            "sso.provider_set",
            &format!(
                "protocol={} require_sso={} require_passkey={} domains={}{ca_said}",
                if protocol == SsoProtocol::Oidc {
                    "oidc"
                } else {
                    "saml"
                },
                saved.require_sso,
                saved.require_passkey,
                saved.domains.join(",")
            ),
        );
        Ok(Response::new(SetProviderResponse {
            view: Some(self.view(saved)),
        }))
    }

    async fn get_provider(
        &self,
        request: Request<GetProviderRequest>,
    ) -> Result<Response<GetProviderResponse>, Status> {
        let (_, account) = self.admin(&request, |r| r.account.as_str(), "Sso.GetProvider")?;
        let view = self.plane()?.provider(&account)?.map(|p| self.view(p));
        // A34-47: the addresses are gunnar's, for the console's origin, so the
        // console renders them and builds none.
        let origin = request
            .get_ref()
            .console_origin
            .trim()
            .trim_end_matches('/')
            .to_owned();
        let addresses = if origin.is_empty() {
            None
        } else if origin_ok(&origin, self.ctx.sso.allow_loopback_http()) {
            use oidc::gunnar_core_paths as paths;
            Some(gunnar_proto::ConsoleAddresses {
                redirect_uri: paths::redirect_uri(&origin),
                sp_entity_id: paths::sp_entity_id(&origin, &account),
                acs_url: paths::acs_url(&origin),
                login_url: paths::login_url(&origin, &account),
                scim_base_url: paths::scim_base_url(&origin),
            })
        } else {
            return Err(Status::invalid_argument(format!(
                "`console_origin` {origin:?} is not an https origin"
            )));
        };
        Ok(Response::new(GetProviderResponse { view, addresses }))
    }

    async fn remove_provider(
        &self,
        request: Request<RemoveProviderRequest>,
    ) -> Result<Response<RemoveProviderResponse>, Status> {
        const METHOD: &str = "Sso.RemoveProvider";
        let (caller, account) = self.admin_stepped_up(&request, |r| r.account.as_str(), METHOD)?;
        let removed = self.plane()?.remove_provider(&account)?;
        self.audit(
            METHOD,
            &caller.principal_id,
            &account,
            true,
            "sso.provider_removed",
            if removed { "removed" } else { "there was none" },
        );
        Ok(Response::new(RemoveProviderResponse { removed }))
    }

    async fn create_scim_token(
        &self,
        request: Request<CreateScimTokenRequest>,
    ) -> Result<Response<CreateScimTokenResponse>, Status> {
        const METHOD: &str = "Sso.CreateScimToken";
        let (caller, account) = self.admin_stepped_up(&request, |r| r.account.as_str(), METHOD)?;
        let label = request
            .get_ref()
            .label
            .trim()
            .chars()
            .take(128)
            .collect::<String>();
        // A34-27: an optional expiry (at most a year) and a read-only scope.
        let expires = scim::bearer::expiry_of(request.get_ref().expires_in_days, now_ms())
            .map_err(Status::invalid_argument)?;
        let read_only = request.get_ref().read_only;
        let secret = format!(
            "gscim_{}",
            crate::webauthn::b64url(&crate::webauthn::random_bytes(32))
        );
        let id = crate::webauthn::hex(&crate::webauthn::random_bytes(8));
        let token = self.plane()?.create_scim_token(
            &account,
            &id,
            &label,
            &hash(&secret),
            &caller.principal_id,
            expires,
            read_only,
        )?;
        self.audit(
            METHOD,
            &caller.principal_id,
            &account,
            true,
            "sso.scim_token_created",
            &format!(
                "token={id} scope={} expires={}",
                if read_only { "read" } else { "read-write" },
                if expires == 0 {
                    "never".to_owned()
                } else {
                    crate::sso::rfc3339(expires / 1000)
                }
            ),
        );
        Ok(Response::new(CreateScimTokenResponse {
            token: Some(token),
            secret,
        }))
    }

    async fn list_scim_tokens(
        &self,
        request: Request<ListScimTokensRequest>,
    ) -> Result<Response<ListScimTokensResponse>, Status> {
        let (_, account) = self.admin(&request, |r| r.account.as_str(), "Sso.ListScimTokens")?;
        let now = now_ms();
        let tokens = self
            .plane()?
            .scim_tokens(&account)?
            .into_iter()
            .map(|mut t| {
                // A34-27: a bearer unused for long, expiring or expired is
                // flagged on the admin's list.
                t.attention = scim::bearer::needs_attention(
                    t.created_at_unix_ms,
                    t.last_used_unix_ms,
                    t.expires_at_unix_ms,
                    now,
                )
                .unwrap_or_default()
                .to_owned();
                t
            })
            .collect();
        Ok(Response::new(ListScimTokensResponse { tokens }))
    }

    async fn revoke_scim_token(
        &self,
        request: Request<RevokeScimTokenRequest>,
    ) -> Result<Response<RevokeScimTokenResponse>, Status> {
        const METHOD: &str = "Sso.RevokeScimToken";
        let (caller, account) = self.admin(&request, |r| r.account.as_str(), METHOD)?;
        let id = request.get_ref().token_id.trim().to_owned();
        let revoked = self.plane()?.revoke_scim_token(&account, &id)?;
        self.audit(
            METHOD,
            &caller.principal_id,
            &account,
            true,
            "sso.scim_token_revoked",
            &format!("token={id} revoked={revoked}"),
        );
        Ok(Response::new(RevokeScimTokenResponse { revoked }))
    }

    async fn list_directory(
        &self,
        request: Request<ListDirectoryRequest>,
    ) -> Result<Response<ListDirectoryResponse>, Status> {
        let (_, account) = self.admin(&request, |r| r.account.as_str(), "Sso.ListDirectory")?;
        let plane = self.plane()?;
        let users = plane
            .directory(&account)?
            .into_iter()
            .filter(|u| !u.deleted)
            .map(Self::directory_user)
            .collect();
        let groups = plane.groups(&account)?.len() as u32;
        Ok(Response::new(ListDirectoryResponse { users, groups }))
    }

    async fn set_user_active(
        &self,
        request: Request<SetUserActiveRequest>,
    ) -> Result<Response<SetUserActiveResponse>, Status> {
        const METHOD: &str = "Sso.SetUserActive";
        let (caller, account) = self.admin(&request, |r| r.account.as_str(), METHOD)?;
        let request = request.into_inner();
        let (user, changed) = self.plane()?.update_user(
            &account,
            request.principal_id.trim(),
            UserPatch {
                active: Some(request.active),
                ..UserPatch::default()
            },
            &caller.principal_id,
        )?;
        self.audit(
            METHOD,
            &caller.principal_id,
            &account,
            true,
            if request.active {
                "sso.user_reactivated"
            } else {
                "sso.user_deactivated"
            },
            &format!("employee={} changed={changed}", user.user.principal_id),
        );
        Ok(Response::new(SetUserActiveResponse {
            user: Some(Self::directory_user(user)),
        }))
    }

    /// **The CSV import, as a background job** (A34-17): checked here (the
    /// account admin with a fresh touch, the size), then queued on the
    /// server's jera lane; the whole file is applied in one write or not at
    /// all, and the answer is the job as it stands (`Sso.ImportStatus`
    /// follows it).
    async fn import_users(
        &self,
        request: Request<ImportUsersRequest>,
    ) -> Result<Response<ImportUsersResponse>, Status> {
        const METHOD: &str = "Sso.ImportUsers";
        let (caller, account) = self.admin_stepped_up(&request, |r| r.account.as_str(), METHOD)?;
        let request = request.into_inner();
        // A34-44: the one limit, refused by name before anything is queued.
        if request.csv.len() > crate::sso::directory::CSV_MAX_BYTES {
            return Err(Status::invalid_argument(crate::sso::directory::too_large()));
        }
        self.plane()?;
        let room = self.room(&account)?;
        let audit = {
            let me = self.clone();
            let principal = caller.principal_id.clone();
            let account = account.clone();
            let dry_run = request.dry_run;
            Arc::new(move |done: &Result<scim::Imported, String>| {
                if dry_run {
                    return;
                }
                match done {
                    Ok(done) => me.audit(
                        METHOD,
                        &principal,
                        &account,
                        true,
                        "sso.csv_import",
                        &format!(
                            "created={} updated={} deactivated={} reactivated={} errors={}",
                            done.created,
                            done.updated,
                            done.deactivated,
                            done.reactivated,
                            done.errors.len()
                        ),
                    ),
                    Err(why) => {
                        me.audit(METHOD, &principal, &account, false, "sso.csv_import", why)
                    }
                }
            })
        };
        let job = self.ctx.control.sso_import(scim::ImportOrder {
            account: account.clone(),
            csv: Arc::from(request.csv.as_str()),
            dry_run: request.dry_run,
            room,
            actor: format!("csv:{}", caller.principal_id),
            plane: Arc::downgrade(&self.ctx.control),
            finished: audit,
        })?;
        Ok(Response::new(import_answer(job, request.dry_run)))
    }

    /// **Where the account's import job is** — the account admin's.
    async fn import_status(
        &self,
        request: Request<ImportStatusRequest>,
    ) -> Result<Response<ImportUsersResponse>, Status> {
        let (_, account) = self.admin(&request, |r| r.account.as_str(), "Sso.ImportStatus")?;
        let wanted = request.get_ref().job_id.trim().to_owned();
        match self.ctx.control.sso_import_status(&account)? {
            Some(job) if wanted.is_empty() || job.job_id == wanted => {
                let dry_run = job.outcome.dry_run;
                Ok(Response::new(import_answer(job, dry_run)))
            }
            _ => Err(Status::not_found(format!(
                "no import job {wanted} for account {account} (a finished job is kept ten minutes)"
            ))),
        }
    }

    /// The login page's "Sign in with <company>" — the relay's.
    async fn discover(
        &self,
        request: Request<DiscoverRequest>,
    ) -> Result<Response<DiscoverResponse>, Status> {
        self.relay(&request, "Sso.Discover")?;
        let plane = self.plane()?;
        // Whether the login page offers the company field at all (round 5.3):
        // on every answer, the empty hint's included. It names no account.
        let any_provider = plane.any_provider()?;
        let hint = request.get_ref().hint.trim().to_ascii_lowercase();
        if hint.is_empty() || hint.len() > 320 {
            return Ok(Response::new(DiscoverResponse {
                any_provider,
                ..Default::default()
            }));
        }
        // **Proven domains only** (A34-39, A34-5): an address or a domain finds
        // the account that proved the domain by DNS; an account name, or a domain
        // nobody proved, finds nothing, so Discover lists no account.
        let domain = hint.rsplit_once('@').map_or(hint.as_str(), |(_, d)| d);
        let account = match normalize_domain(domain) {
            Some(d) => plane.account_for_domain(&d)?,
            None => None,
        };
        let found = match account {
            Some(a) if !a.contains('/') => plane.provider(&a)?,
            _ => None,
        };
        Ok(Response::new(match found {
            Some(p) => DiscoverResponse {
                found: true,
                account: p.account.clone(),
                display_name: p.display_name.clone(),
                protocol: p.protocol,
                any_provider,
            },
            None => DiscoverResponse {
                any_provider,
                ..Default::default()
            },
        }))
    }

    /// **Begin a sign-in** — the relay's.
    async fn begin(
        &self,
        request: Request<BeginRequest>,
    ) -> Result<Response<BeginResponse>, Status> {
        const METHOD: &str = "Sso.Begin";
        self.relay(&request, METHOD)?;
        let request = request.into_inner();
        let account = account_of(&request.account)?;
        let plane = self.plane()?;
        let now = now_ms();
        // Counted per (account, client) (A34-6): one address at its limit never holds
        // back another address of the same account.
        let client = nornir_identity_sso::client_key(&request.client);
        if !self.ctx.sso.allow_begin(&account, &client, now) {
            self.audit(
                METHOD,
                "",
                &account,
                false,
                "sso.rate_limited",
                "too many sign-ins begun this minute",
            );
            return Err(Status::resource_exhausted(format!(
                "[sso-busy] too many sign-ins to {account} from this address this minute; try again shortly"
            )));
        }
        let provider = plane
            .provider(&account)?
            .ok_or_else(|| Status::not_found(format!("{account} has no single sign-on")))?;
        if !same_origin(&request.console_origin, &provider.console_origin) {
            return Err(Status::failed_precondition(format!(
                "{account}'s single sign-on is configured for {}, not for this console",
                provider.console_origin
            )));
        }
        let (url, state, flow) = match protocol_of(&provider) {
            SsoProtocol::Oidc => {
                // Refused by name while the operator has not turned SSO's network on (A34-16).
                let egress = self.ctx.sso.egress().map_err(Status::unavailable)?;
                oidc::begin(
                    &self.ctx.sso,
                    egress,
                    &provider,
                    request.login_hint.trim(),
                    now,
                )
                .await
                .map_err(|why| {
                    Status::unavailable(format!("{account}'s identity provider: {why}"))
                })?
            }
            SsoProtocol::Saml => {
                saml::begin(&provider, now).map_err(Status::failed_precondition)?
            }
            SsoProtocol::Unspecified => {
                return Err(Status::failed_precondition(
                    "the provider names no protocol",
                ))
            }
        };
        let expires = flow.expires_ms;
        let flow = crate::sso::Flow { client, ..flow };
        self.ctx
            .sso
            .begin(hash(&state), flow, now)
            .map_err(Status::resource_exhausted)?;
        tracing::info!(target: AUDIT_TARGET, event = "sso.begin", account = %account, "a single sign-on began");
        Ok(Response::new(BeginResponse {
            redirect_url: url,
            state,
            expires_at_unix_ms: expires,
            protocol: provider.protocol,
        }))
    }

    /// **Finish a sign-in** — the relay's: verified, mapped, enrolled.
    async fn finish(
        &self,
        request: Request<FinishRequest>,
    ) -> Result<Response<FinishResponse>, Status> {
        const METHOD: &str = "Sso.Finish";
        self.relay(&request, METHOD)?;
        let request = request.into_inner();
        let now = now_ms();
        // Spent FIRST, whatever happens next: a state is single use.
        let flow = self
            .ctx
            .sso
            .take_flow(&hash(request.state.trim()), now)
            .ok_or_else(|| {
                Status::permission_denied(nornir_identity_sso::Refusal::Expired.message())
            })?;
        let account = flow.account.clone();
        // The client finishing (the one that began, when the relay names none): its
        // failures hold back that client alone (A34-6).
        let client = match request.client.trim() {
            "" => flow.client.clone(),
            named => nornir_identity_sso::client_key(named),
        };
        let refuse = |why: String| {
            // Every refusal carries its code (A34-46); a protocol's own words are
            // the IdP's refusal.
            let why = match nornir_identity_sso::code_of(&why) {
                Some(_) => why,
                None => nornir_identity_sso::Refusal::IdpRefused.with(&why),
            };
            self.ctx.sso.note_failure(&account, &client, now);
            self.audit(METHOD, "", &account, false, "sso.login_refused", &why);
            Status::permission_denied(format!("single sign-on refused: {why}"))
        };
        if self.ctx.sso.failures_held(&account, &client, now) {
            self.audit(
                METHOD,
                "",
                &account,
                false,
                "sso.rate_limited",
                "too many failed sign-ins this minute",
            );
            return Err(Status::resource_exhausted(
                "[sso-busy] too many failed sign-ins to this account from this address this minute; try again shortly",
            ));
        }
        if !same_origin(&request.console_origin, &flow.console_origin) {
            return Err(refuse(nornir_identity_sso::Refusal::OtherConsole.message()));
        }
        if !request.idp_error.trim().is_empty() {
            let said: String = request.idp_error.trim().chars().take(100).collect();
            return Err(refuse(format!("the identity provider said {said:?}")));
        }
        let identity = self.ctx.identity.clone().ok_or_else(|| {
            Status::unavailable("this server serves no identity core, so it signs nobody in")
        })?;
        let plane = self.plane()?;
        let provider = match plane.provider(&account)? {
            Some(p) if same_origin(&p.console_origin, &flow.console_origin) => p,
            _ => {
                return Err(refuse(
                    nornir_identity_sso::Refusal::ProviderChanged.message(),
                ))
            }
        };
        let protocol = protocol_of(&provider);
        let (issuer, subject, user_name, display_name, email, email_verified, idp_ids) =
            match (protocol, flow.saml) {
                (SsoProtocol::Oidc, false) => {
                    // Refused by name while the operator has not turned SSO's network on (A34-16).
                    let egress = self.ctx.sso.egress().map_err(Status::unavailable)?;
                    let secret = plane.client_secret(&account)?.unwrap_or_default();
                    let v = oidc::finish(
                        &self.ctx.sso,
                        egress,
                        &provider,
                        &flow,
                        request.code.trim(),
                        &secret,
                        now,
                    )
                    .await
                    .map_err(refuse)?;
                    (
                        v.issuer,
                        v.subject,
                        v.user_name,
                        v.display_name,
                        v.email,
                        v.email_verified,
                        v.stable_ids,
                    )
                }
                (SsoProtocol::Saml, true) => {
                    let v = saml::finish(
                        &self.ctx.sso,
                        &provider,
                        &flow,
                        request.saml_response.trim(),
                        now,
                    )
                    .map_err(refuse)?;
                    // SAML asserts no e-mail verification; the stable id is the
                    // NameID (or the mapped attribute for a transient one).
                    let ids = vec![v.subject.clone()];
                    (
                        v.issuer,
                        v.subject,
                        v.user_name,
                        v.display_name,
                        v.email,
                        false,
                        ids,
                    )
                }
                _ => {
                    return Err(refuse(
                        nornir_identity_sso::Refusal::ProviderChanged.message(),
                    ))
                }
            };
        let user_name = normalize_user_name(&user_name)
            .ok_or_else(|| refuse(nornir_identity_sso::Refusal::NamedNobody.message()))?;
        let room = self.room(&account)?;
        let outcome = plane
            .login(SsoLoginAsk {
                account: account.clone(),
                issuer,
                subject,
                user_name: user_name.clone(),
                display_name,
                email,
                email_verified,
                idp_ids,
                room,
                // The session is the identity core's (below), never a key.
                session: None,
                now_unix_ms: now,
            })
            .map_err(|err| match err {
                crate::seam::ControlError::FailedPrecondition(m) => {
                    self.audit(METHOD, "", &account, false, "sso.login_refused", &m);
                    Status::failed_precondition(m)
                }
                other => refuse(other.to_string()),
            })?;
        let principal_id = outcome.principal.id.clone();
        let mut answer = FinishResponse {
            principal: Some(outcome.principal.clone()),
            account: account.clone(),
            created: outcome.created,
            ..FinishResponse::default()
        };
        if provider.require_passkey {
            let passkeys = plane.passkeys_of(&principal_id)?;
            let register = passkeys.is_empty();
            let ticket = crate::webauthn::b64url(&crate::webauthn::random_bytes(32));
            self.ctx
                .sso
                .park_second(
                    hash(&ticket),
                    Second {
                        account: account.clone(),
                        principal_id: principal_id.clone(),
                        register,
                        // The shared ticket keeps a session-key slot (edda's
                        // `Second`); Gunnar's second factor mints a core session
                        // instead, so the slot stays empty.
                        session_openssh: String::new(),
                        session_expires_ms: 0,
                        expires_ms: now + SECOND_TTL_MS,
                    },
                    now,
                )
                .map_err(Status::resource_exhausted)?;
            if register {
                // The person's first passkey: the core's link onto them, as
                // short-lived as the ticket.
                let link = identity
                    .enrolment(
                        &principal_id,
                        &format!("sso:{account}"),
                        Some(SECOND_TTL_MS / 1000),
                    )
                    .map_err(crate::identity::status_of)?;
                answer.enrolment = link.secret;
            }
            answer.second_factor = if register {
                SecondFactorNeed::PasskeyRegistration
            } else {
                SecondFactorNeed::Passkey
            } as i32;
            answer.pending = ticket;
            answer.credential_ids = passkeys;
        } else {
            // **The identity core's session for whom the IdP vouched**: the
            // same opaque session a passkey login mints.
            let session = crate::identity::vouched_session(&identity, &principal_id, false)
                .map_err(crate::identity::status_of)?;
            // Company sign-on vouched: the session acts under `require_sso`.
            identity
                .host()
                .gunnar_store()
                .admit_sso_session(&session.token)
                .map_err(|e| Status::unavailable(e.0))?;
            // ...and may mint the employee's own credential (A34-24): the same persisted
            // admission says so (RA5-38).
            answer.session = session.token;
        }
        self.audit(
            METHOD,
            &principal_id,
            &account,
            true,
            "sso.login",
            &format!(
                "protocol={} created={} claimed={}{} second_factor={}",
                if flow.saml { "saml" } else { "oidc" },
                outcome.created,
                outcome.claimed,
                if outcome.claimed_by.is_empty() {
                    String::new()
                } else {
                    format!(" claimed_by={}", outcome.claimed_by)
                },
                provider.require_passkey
            ),
        );
        Ok(Response::new(answer))
    }

    /// **The passkey after the IdP** — the relay's.
    async fn second_factor(
        &self,
        request: Request<SecondFactorRequest>,
    ) -> Result<Response<SecondFactorResponse>, Status> {
        const METHOD: &str = "Sso.SecondFactor";
        self.relay(&request, METHOD)?;
        let request = request.into_inner();
        let now = now_ms();
        let second = self
            .ctx
            .sso
            .take_second(&hash(request.pending.trim()), now)
            .ok_or_else(|| {
                Status::permission_denied(nornir_identity_sso::Refusal::Expired.message())
            })?;
        let refuse = |why: String| {
            // By code (A34-46): the console maps it to its own words.
            let why = match nornir_identity_sso::code_of(&why) {
                Some(_) => why,
                None => nornir_identity_sso::Refusal::SecondFactor.with(&why),
            };
            self.audit(
                METHOD,
                &second.principal_id,
                &second.account,
                false,
                "sso.second_factor_refused",
                &why,
            );
            Status::permission_denied(format!("{METHOD} refused: {why}"))
        };
        // **The passkey was the identity core's** (a login, or the first
        // passkey's redemption): its session must be the person the IdP named.
        let identity = self.ctx.identity.clone().ok_or_else(|| {
            Status::unavailable("this server serves no identity core, so it signs nobody in")
        })?;
        let view = identity
            .session(request.session.trim())
            .map_err(|refused| {
                refuse(format!(
                    "the passkey's session is no session here ({})",
                    refused.code
                ))
            })?;
        if view.user_id != second.principal_id {
            return Err(refuse(
                "this passkey is not the signed-in person's".to_owned(),
            ));
        }
        // After the IdP, the passkey's session is admitted (`require_sso`).
        identity
            .host()
            .gunnar_store()
            .admit_sso_session(request.session.trim())
            .map_err(|e| Status::unavailable(e.0))?;
        let principal = self
            .ctx
            .control
            .principal(&second.principal_id)?
            .ok_or_else(|| Status::not_found(format!("no principal {}", second.principal_id)))?;
        self.audit(
            METHOD,
            &second.principal_id,
            &second.account,
            true,
            "sso.second_factor",
            if second.register {
                "registered the first passkey"
            } else {
                "passkey verified"
            },
        );
        Ok(Response::new(SecondFactorResponse {
            principal: Some(principal),
            user_verified: view.stepped_up,
        }))
    }

    /// **Prove a domain by DNS** (A34-5) — the account admin's, with a fresh touch:
    /// the TXT record is read through the operator-switched egress from the
    /// operator's resolver; only then does the domain lead sign-ins here.
    async fn verify_domain(
        &self,
        request: Request<VerifyDomainRequest>,
    ) -> Result<Response<VerifyDomainResponse>, Status> {
        const METHOD: &str = "Sso.VerifyDomain";
        let (caller, account) = self.admin_stepped_up(&request, |r| r.account.as_str(), METHOD)?;
        let raw = request.get_ref().domain.trim().to_owned();
        let domain = normalize_domain(&raw)
            .ok_or_else(|| Status::invalid_argument(format!("{raw:?} is not an email domain")))?;
        let plane = self.plane()?;
        let state = Arc::clone(&self.ctx.sso);
        let (who, what) = (account.clone(), domain.clone());
        // **No default public resolver** (RA5-24): the operator names the resolver
        // (`sso_dns_resolver`) or no proof is read; outbound needs explicit config (owner
        // rule 2026-10-25). Since nornir-identity-sso 0.1.2 edda's state keeps a blank
        // resolver blank (no dns.google default), so blank is the one "not fed".
        let resolver = state.dns_resolver();
        let resolver_named = !resolver.trim().is_empty();
        let read = match resolver_named {
            false => Err(crate::sso::Refusal::DomainResolver.with(NO_RESOLVER)),
            true => tokio::task::spawn_blocking(move || {
                crate::sso::domains::verify(&state, &who, &what)
            })
            .await
            .map_err(|e| Status::internal(format!("the DNS proof was not read: {e}")))?,
        };
        let proven = read.and_then(|()| {
            plane
                .verify_domain(&account, &domain)
                .map_err(|e| e.to_string())
        });
        match proven {
            Ok(saved) => {
                self.audit(
                    METHOD,
                    &caller.principal_id,
                    &account,
                    true,
                    "sso.domain_verified",
                    &format!("domain={domain}"),
                );
                Ok(Response::new(VerifyDomainResponse {
                    view: Some(self.view(saved)),
                }))
            }
            Err(why) => {
                self.audit(
                    METHOD,
                    &caller.principal_id,
                    &account,
                    false,
                    "sso.domain_refused",
                    &format!("domain={domain} {why}"),
                );
                Err(Status::failed_precondition(why))
            }
        }
    }

    /// **Open a rebind** (A34-11, an IdP migration) — the account admin's, with a
    /// fresh touch, audited.
    async fn rebind_users(
        &self,
        request: Request<RebindUsersRequest>,
    ) -> Result<Response<RebindUsersResponse>, Status> {
        const METHOD: &str = "Sso.RebindUsers";
        let (caller, account) = self.admin_stepped_up(&request, |r| r.account.as_str(), METHOD)?;
        let request = request.into_inner();
        let ids: Vec<String> = request
            .principal_ids
            .iter()
            .map(|p| p.trim().to_owned())
            .filter(|p| !p.is_empty())
            .collect();
        let rows = self
            .plane()?
            .rebind_users(&account, &ids, request.external_id.trim())?;
        self.audit(
            METHOD,
            &caller.principal_id,
            &account,
            true,
            "sso.rebind",
            &format!(
                "employees={} external_id={}",
                rows.len(),
                !request.external_id.trim().is_empty()
            ),
        );
        Ok(Response::new(RebindUsersResponse {
            users: rows.into_iter().map(Self::directory_user).collect(),
        }))
    }

    /// **One SCIM request** — relayed; the bearer decides the account.
    async fn scim(&self, request: Request<ScimRequest>) -> Result<Response<ScimResponse>, Status> {
        const METHOD: &str = "Sso.Scim";
        self.relay(&request, METHOD)?;
        let request = request.into_inner();
        let plane = self.plane()?;
        let now = now_ms();
        let respond = |answer: scim::Answer, ended: Vec<String>| {
            let body = if answer.body.is_null() {
                Vec::new()
            } else {
                serde_json::to_vec(&answer.body).unwrap_or_default()
            };
            Response::new(ScimResponse {
                status: u32::from(answer.status),
                body,
                content_type: scim::CONTENT_TYPE.to_owned(),
                location: answer.location,
                ended_sessions: ended,
            })
        };
        let bearer = request.bearer.trim();
        let token_hash = hash(bearer);
        let found = if bearer.is_empty() || bearer.len() > 512 {
            None
        } else {
            plane.scim_account(&token_hash)?
        };
        // A34-27: an expired bearer is refused as an unknown one is, with its
        // own sentence; every refusal is counted per client, so a flood of
        // them neither fills the audit log nor goes on answered at full speed.
        let refused = match &found {
            None => Some(scim::error_answer(
                401,
                None,
                "the bearer is unknown or revoked",
            )),
            Some((_, b)) => b.check(now).err(),
        };
        if let Some(answer) = refused {
            let client = request.client.trim();
            let detail = answer.body["detail"]
                .as_str()
                .unwrap_or_default()
                .to_owned();
            match scim::REFUSALS.refused(client, now) {
                scim::Refusal::Audit => self.audit(
                    METHOD,
                    "",
                    found.as_ref().map(|(a, _)| a.as_str()).unwrap_or(""),
                    false,
                    "sso.scim_refused",
                    &format!("{detail} (client {})", scim::bearer::client_key(client)),
                ),
                scim::Refusal::Quiet => {}
                scim::Refusal::Throttle => {
                    return Ok(respond(
                        scim::error_answer(
                            429,
                            None,
                            "too many refused requests from this client this minute",
                        ),
                        Vec::new(),
                    ))
                }
            }
            return Ok(respond(answer, Vec::new()));
        }
        let Some((account, bearer_rules)) = found else {
            unreachable!("a missing bearer was refused above");
        };
        if !self
            .ctx
            .sso
            .allow(&format!("scim:{token_hash}"), SCIM_PER_MINUTE, now)
        {
            return Ok(respond(
                scim::error_answer(429, None, "too many requests this minute"),
                Vec::new(),
            ));
        }
        let room = self.room(&account)?;
        let actor = format!("scim:{account}");
        let store = scim::PlaneDirectory {
            plane,
            account: &account,
            room,
            actor: &actor,
            now_unix_ms: now,
        };
        let call = scim::Call {
            method: request.method.trim(),
            path: request.path.trim(),
            query: request.query.trim(),
            body: &request.body,
            base_url: request.base_url.trim(),
            read_only: bearer_rules.scope == scim::Scope::Read,
        };
        let (answer, effect) = scim::handle(&store, &call);
        // A user deactivated or deleted had its web sessions revoked with it:
        // named in the answer, so the console signs them out too.
        let ended: Vec<String> = effect
            .as_ref()
            .and_then(scim::Effect::ended_session)
            .map(|p| vec![p.to_owned()])
            .unwrap_or_default();
        // ...and their identity-core sessions end here (IDENTITY-HOST): a
        // reactivation later does not bring the old ones back.
        if let Some(identity) = &self.ctx.identity {
            for principal in &ended {
                if let Err(refused) = identity.end_sessions(principal, &format!("scim:{account}")) {
                    tracing::warn!(target: AUDIT_TARGET, event = "sso.scim_sessions_not_ended", principal = %principal, reason = %refused, "a deactivated user's sessions could not be ended; the directory still refuses them");
                }
            }
        }
        if let Some(effect) = effect {
            self.audit(
                METHOD,
                &actor,
                &account,
                true,
                "sso.scim",
                &format!("{} {}", effect.what, effect.principal),
            );
        } else if answer.status >= 400 {
            tracing::info!(target: AUDIT_TARGET, event = "sso.scim_answer", account = %account, status = answer.status, method = %call.method, path = %call.path, "a SCIM request was answered with an error");
        }
        Ok(respond(answer, ended))
    }
}

/// An import job as `Sso.ImportUsers`/`Sso.ImportStatus` answer it.
fn import_answer(job: scim::ImportJob, dry_run: bool) -> ImportUsersResponse {
    let o = job.outcome;
    ImportUsersResponse {
        created: o.created,
        updated: o.updated,
        deactivated: o.deactivated,
        reactivated: o.reactivated,
        unchanged: o.unchanged,
        errors: o.errors,
        dry_run,
        job_id: job.job_id,
        state: job.state.to_owned(),
        error: job.error,
        done_rows: job.done_rows,
        total_rows: job.total_rows,
        ended_sessions: o.ended_sessions,
    }
}

/// Wire an egress into the context's SSO state (`gunnar-server` does).
pub fn with_egress(ctx: &crate::service::ServiceContext, egress: Arc<dyn crate::sso::Egress>) {
    ctx.sso.set_egress(egress);
}