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

nordisk/gunnar

crates/gunnar-server/tests/sso.rs

//! **Company SSO, end to end** (owner, 2026-10-05): the `Sso` service over the
//! REAL plane and stores, against a fake OpenID provider and a fake SAML IdP
//! running in this process over real HTTP (`nornir_auth_oidc::fake_op`,
//! `nornir_auth_saml::fake_idp`), reached through gunnar's own egress.
//!
//! What must hold, each its own test:
//! * the happy path, both protocols: verified by gunnar, a NEW employee of
//!   THAT account just in time, the session key the person's;
//! * every refusal — wrong audience, expired, bad signature (another key, the
//!   HS256 confusion), the replayed state, the replayed assertion, the wrong
//!   nonce, another account's IdP — creates nobody and enrols nothing;
//! * an existing principal is never attached;
//! * `require_sso` refuses an employee's passkey-only login, and the account
//!   admin keeps the break-glass; `require_passkey` puts a passkey after the
//!   IdP (registered on the first login);
//! * SCIM create/update/deactivate/delete and groups, and the CSV import;
//! * the client secret and the SCIM bearer never come back and are never in
//!   an audit record.
#![cfg(feature = "sso-egress")] // A32-18: the airgap default carries no HTTP client; the appliance rows and CI name `sso-egress`.

use std::sync::{Arc, Mutex};

use gunnar_grpc::auth::{AuditLedger, AuditRecord, AuthGate, PersonSession, SESSION_METADATA_KEY};
use gunnar_grpc::identity::GunnarIdentity;
use gunnar_grpc::{Caller, ControlPlane, EventBus, ServerConfig, ServiceContext, SsoService};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::sso_server::Sso as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use nornir_auth_oidc::fake_op::{self, FakeOp};
use nornir_auth_saml::fake_idp::{self, FakeIdp};
use p256::ecdsa::signature::Signer as _;
use sha2::Digest as _;
use tonic::{Code, Request};

const ORIGIN: &str = "http://localhost:7000";
const RP_ID: &str = "localhost";
const SECRET: &str = "oidc-client-secret-NEVER-LOGGED";

#[derive(Default)]
struct Ledger(Mutex<Vec<String>>);

impl AuditLedger for Ledger {
    fn record(&self, r: AuditRecord<'_>) {
        self.0.lock().unwrap().push(format!(
            "{} principal={} scope={} {} {}",
            r.method, r.principal, r.scope, r.decision, r.reason
        ));
    }
}

struct Harness {
    _dir: tempfile::TempDir,
    plane: Arc<CoreControlPlane>,
    identity: Arc<GunnarIdentity>,
    stores: Arc<ServerStores>,
    ctx: ServiceContext,
    ledger: Arc<Ledger>,
    console: Caller,
    op: FakeOp,
}

fn person(id: &str) -> Caller {
    Caller {
        principal_id: id.to_owned(),
        fingerprint: String::new(),
        machine: false,
        authenticated: true,
    }
}

/// A call from `caller`. A test person with no fingerprint stands for a
/// session that stepped up a moment ago (a fresh user-verified touch, the
/// identity core's [`PersonSession`]); one with a fingerprint, for one that
/// did not (`a34_10_…`).
fn authed<T>(caller: &Caller, body: T) -> Request<T> {
    let mut r = Request::new(body);
    r.extensions_mut().insert(caller.clone());
    r.extensions_mut().insert(PersonSession {
        stepped_up: caller.fingerprint.is_empty(),
        stepped_up_at_unix_ms: None,
    });
    r
}

/// The browser's relying party, as the console relays it to the core.
fn relay() -> nornir_identity::Relay {
    nornir_identity::Relay {
        rp_id: RP_ID.into(),
        origin: ORIGIN.into(),
        client: None,
    }
}

fn now_ms() -> i64 {
    gunnar_core::now_ms()
}

/// A fresh OpenSSH public key (an SSH key a person adds).
fn session() -> (String, ()) {
    let key =
        gunnar_core::ssh_key::PrivateKey::from_openssh(gunnar_core::mint_ed25519().expect("mint"))
            .expect("parses");
    (key.public_key().to_openssh().expect("openssh"), ())
}

fn query(url: &str, key: &str) -> String {
    url.split_once('?')
        .map(|(_, q)| q)
        .unwrap_or("")
        .split('&')
        .filter_map(|p| p.split_once('='))
        .find(|(k, _)| *k == key)
        .map(|(_, v)| {
            let mut out = Vec::new();
            let b = v.as_bytes();
            let mut i = 0;
            while i < b.len() {
                if b[i] == b'%' && i + 2 < b.len() {
                    out.push(u8::from_str_radix(&v[i + 1..i + 3], 16).unwrap());
                    i += 3;
                } else {
                    out.push(if b[i] == b'+' { b' ' } else { b[i] });
                    i += 1;
                }
            }
            String::from_utf8(out).unwrap()
        })
        .unwrap_or_default()
}

impl Harness {
    fn new() -> Harness {
        let dir = tempfile::tempdir().expect("tempdir");
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
        let control: Arc<dyn ControlPlane> = plane.clone();
        let ledger = Arc::new(Ledger::default());
        let identity = gunnar_server::identity::build(
            dir.path(),
            Arc::clone(&control),
            &ServerConfig::default(),
            false,
        );
        let ctx = ServiceContext::new(
            control,
            Arc::new(EventBus::new(16)),
            ServerConfig::default(),
        )
        .with_audit_ledger(ledger.clone())
        .with_identity(Arc::clone(&identity));
        ctx.sso.set_allow_loopback_http(true);
        ctx.sso
            .set_egress(Arc::new(gunnar_server::sso_egress::UreqEgress::new(true)));
        // The operator's switch (A34-16): this rig dials its IdP.
        ctx.sso.set_egress_on(true);
        plane
            .add_principal("gunnar-ui", "gunnar-ui", PrincipalKind::Machine)
            .expect("console");
        plane
            .grant("gunnar-ui", "", "", GrantLevel::Admin)
            .expect("the relay is a server admin");
        for account in ["acme", "globex"] {
            plane
                .add_principal(account, account, PrincipalKind::User)
                .expect("account");
        }
        let op = FakeOp::start("gunnar-acme", SECRET).expect("fake OP");
        Harness {
            _dir: dir,
            plane,
            identity,
            stores,
            ctx,
            ledger,
            console: Caller {
                principal_id: "gunnar-ui".into(),
                fingerprint: String::new(),
                machine: true,
                authenticated: true,
            },
            op,
        }
    }

    fn sso(&self) -> SsoService {
        SsoService::new(self.ctx.clone())
    }

    fn oidc_provider(
        &self,
        account: &str,
        require_sso: bool,
        require_passkey: bool,
    ) -> SsoProvider {
        SsoProvider {
            account: account.into(),
            protocol: SsoProtocol::Oidc as i32,
            display_name: format!("{account} corp"),
            domains: vec![format!("{account}.example")],
            oidc: Some(OidcSettings {
                issuer: self.op.issuer().to_owned(),
                client_id: "gunnar-acme".into(),
                ..Default::default()
            }),
            saml: None,
            require_sso,
            require_passkey,
            console_origin: ORIGIN.into(),
            ..Default::default()
        }
    }

    async fn configure(&self, provider: SsoProvider) -> ProviderView {
        let account = provider.account.clone();
        self.sso()
            .set_provider(authed(
                &person(&account),
                SetProviderRequest {
                    provider: Some(provider),
                    client_secret: SECRET.into(),
                    clear_client_secret: false,
                    clear_idp_ca: false,
                },
            ))
            .await
            .expect("the account admin configures the IdP")
            .into_inner()
            .view
            .expect("a view")
    }

    async fn begin(&self, account: &str) -> BeginResponse {
        self.sso()
            .begin(authed(
                &self.console,
                BeginRequest {
                    account: account.into(),
                    login_hint: String::new(),
                    console_origin: ORIGIN.into(),
                    client: String::new(),
                },
            ))
            .await
            .expect("begun")
            .into_inner()
    }

    async fn finish_oidc(&self, state: &str, code: &str) -> Result<FinishResponse, tonic::Status> {
        self.sso()
            .finish(authed(
                &self.console,
                FinishRequest {
                    state: state.into(),
                    code: code.into(),
                    console_origin: ORIGIN.into(),
                    ..Default::default()
                },
            ))
            .await
            .map(|r| r.into_inner())
    }

    /// One whole OIDC sign-in as `who`, through the fake: the finish, and the
    /// identity-core session it handed (empty when a second factor is owed).
    async fn oidc_login(
        &self,
        account: &str,
        who: fake_op::Person,
    ) -> (Result<FinishResponse, tonic::Status>, String) {
        self.op.set_person(who);
        let begun = self.begin(account).await;
        let back = self
            .op
            .authorize(&begun.redirect_url)
            .expect("the fake signs in");
        assert_eq!(query(&back, "state"), begun.state, "the state comes back");
        let finished = self.finish_oidc(&begun.state, &query(&back, "code")).await;
        let session = finished
            .as_ref()
            .map(|f| f.session.clone())
            .unwrap_or_default();
        (finished, session)
    }

    fn employees(&self, account: &str) -> Vec<String> {
        self.plane
            .employees(account)
            .expect("employees")
            .into_iter()
            .map(|p| p.id)
            .collect()
    }

    fn audit(&self) -> Vec<String> {
        self.ledger.0.lock().unwrap().clone()
    }

    /// `Sso.ImportUsers`, then `Sso.ImportStatus` until the job has ended
    /// (A34-17: the import is a background job).
    async fn import_as(
        &self,
        caller: &Caller,
        account: &str,
        csv: &str,
        dry_run: bool,
    ) -> Result<ImportUsersResponse, tonic::Status> {
        let mut job = self
            .sso()
            .import_users(authed(
                caller,
                ImportUsersRequest {
                    account: account.into(),
                    csv: csv.into(),
                    dry_run,
                },
            ))
            .await?
            .into_inner();
        for _ in 0..500 {
            if job.state == "done" || job.state == "failed" {
                return Ok(job);
            }
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            job = self
                .sso()
                .import_status(authed(
                    caller,
                    ImportStatusRequest {
                        account: account.into(),
                        job_id: job.job_id.clone(),
                    },
                ))
                .await?
                .into_inner();
        }
        panic!("the import job never ended: {job:?}");
    }
    /// **Prove `domain` for `account`** (A34-5): a DNS-over-HTTPS resolver on this
    /// box's loopback answers the account's TXT record, and the admin asks.
    async fn prove(&self, account: &str, domain: &str) -> Result<ProviderView, tonic::Status> {
        let record = gunnar_grpc::sso::domains::proof_value(account, domain);
        self.ctx.sso.set_dns_resolver(&doh_server(vec![record]));
        self.verify_domain(account, domain).await
    }

    async fn verify_domain(
        &self,
        account: &str,
        domain: &str,
    ) -> Result<ProviderView, tonic::Status> {
        self.sso()
            .verify_domain(authed(
                &person(account),
                VerifyDomainRequest {
                    account: account.into(),
                    domain: domain.into(),
                },
            ))
            .await
            .map(|r| r.into_inner().view.expect("a view"))
    }
}

/// A DNS-over-HTTPS JSON resolver on the loopback, answering `records` as the TXT
/// strings of whatever name is asked. Its URL.
fn doh_server(records: Vec<String>) -> String {
    use std::io::{Read as _, Write as _};
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback port");
    let url = format!("http://{}/resolve", listener.local_addr().unwrap());
    let answers: Vec<serde_json::Value> = records
        .iter()
        .map(|r| serde_json::json!({ "type": 16, "data": format!("\"{r}\"") }))
        .collect();
    let body = serde_json::json!({ "Status": 0, "Answer": answers }).to_string();
    std::thread::spawn(move || {
        for stream in listener.incoming().flatten() {
            let mut stream = stream;
            let mut buf = [0u8; 4096];
            let _ = stream.read(&mut buf);
            let _ = write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/dns-json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
        }
    });
    url
}

/// ★ **Whether ANY account has an identity provider — and nothing more**
/// (round 5.3, owner 2026-10-08: test2's login page offered "Work email or
/// company account" on an estate where no account has an IdP). Discover
/// answers `any_provider` for every hint, the empty one included: false on a
/// box with no provider, true once one account configures one, false again
/// when it is removed. The empty hint still finds nobody and names no account.
#[tokio::test(flavor = "multi_thread")]
async fn discover_says_whether_any_account_has_an_identity_provider() {
    let h = Harness::new();
    let ask = |hint: &str| {
        let req = authed(&h.console, DiscoverRequest { hint: hint.into() });
        let sso = h.sso();
        async move {
            sso.discover(req)
                .await
                .expect("discover answers")
                .into_inner()
        }
    };
    let none = ask("").await;
    assert!(
        !none.any_provider && !none.found && none.account.is_empty(),
        "no provider anywhere: {none:?}"
    );
    assert!(
        !ask("bob@acme.example").await.any_provider,
        "a hint changes nothing while none is configured"
    );

    h.configure(h.oidc_provider("acme", false, false)).await;
    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let any = ask("").await;
    assert!(any.any_provider, "acme has a provider: {any:?}");
    assert!(
        !any.found && any.account.is_empty() && any.display_name.is_empty(),
        "the empty hint names no account: {any:?}"
    );
    let other = ask("someone@elsewhere.example").await;
    assert!(
        other.any_provider && !other.found,
        "a hint that finds nobody still says one exists: {other:?}"
    );
    assert!(ask("bob@acme.example").await.found);

    h.sso()
        .remove_provider(authed(
            &person("acme"),
            RemoveProviderRequest {
                account: "acme".into(),
            },
        ))
        .await
        .expect("the account admin removes it");
    assert!(
        !ask("").await.any_provider,
        "the last provider removed: none any more"
    );
}

fn bob() -> fake_op::Person {
    fake_op::Person::new("00u-bob", "bob@acme.example", "Bob Example")
}

/// ★ **OIDC, the happy path.** gunnar verifies the IdP's answer, creates a NEW
/// employee of acme just in time, and the console's session key is that
/// employee's — it authenticates as them. The second login is the same
/// person. The client secret never comes back.
#[tokio::test(flavor = "multi_thread")]
async fn oidc_first_login_creates_a_new_employee_and_the_session_is_theirs() {
    let h = Harness::new();
    let view = h.configure(h.oidc_provider("acme", false, false)).await;
    let shown = view.provider.expect("provider");
    assert!(shown.has_client_secret);
    assert_eq!(
        view.redirect_uri,
        format!("{ORIGIN}/auth/sso/oidc/callback")
    );
    let got = h
        .sso()
        .get_provider(authed(
            &person("acme"),
            GetProviderRequest {
                account: "acme".into(),
                ..Default::default()
            },
        ))
        .await
        .unwrap()
        .into_inner();
    assert!(
        !format!("{got:?}").contains(SECRET),
        "the client secret came back"
    );

    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let found = h
        .sso()
        .discover(authed(
            &h.console,
            DiscoverRequest {
                hint: "Bob@ACME.example".into(),
            },
        ))
        .await
        .unwrap()
        .into_inner();
    assert!(
        found.found && found.account == "acme" && found.display_name == "acme corp",
        "{found:?}"
    );

    let (first, key) = h.oidc_login("acme", bob()).await;
    let first = first.expect("the first login");
    let employee = first.principal.as_ref().unwrap().id.clone();
    assert!(first.created);
    assert_eq!(employee, "acme.bob");
    assert_eq!(
        h.stores.auth().employer_of(&employee).unwrap().as_deref(),
        Some("acme")
    );
    assert_eq!(first.session, key);
    assert_eq!(
        h.gate_session(&key)
            .expect("the core session is a credential")
            .principal_id,
        "acme.bob",
        "the session is the employee's"
    );

    let (second, _) = h.oidc_login("acme", bob()).await;
    let second = second.expect("the second login");
    assert!(!second.created);
    assert_eq!(second.principal.unwrap().id, "acme.bob");
    assert_eq!(h.employees("acme"), vec!["acme.bob".to_owned()]);

    let trail = h.audit().join("\n");
    assert!(
        trail.contains("Sso.SetProvider principal=acme scope=account:acme allow"),
        "{trail}"
    );
    assert!(
        trail.contains("Sso.Finish principal=acme.bob scope=account:acme allow")
            && trail.contains("created=true"),
        "{trail}"
    );
    assert!(
        !trail.contains(SECRET),
        "the client secret reached the audit trail"
    );
    // ★ The ACCOUNT's own log (the owner's enterprise list, 2026-10-05): the
    // provider its admin set, and each sign-in of its employee through it.
    let log = h
        .stores
        .account_audit()
        .query(
            "acme",
            &gunnar_core::account_audit::AuditQuery {
                limit: 100,
                ..Default::default()
            },
        )
        .expect("acme's log");
    let seen: Vec<String> = log
        .entries
        .iter()
        .map(|e| format!("{}:{}:{}", e.kind, e.actor, e.actor_role))
        .collect();
    assert!(
        seen.iter().any(|s| s == "sso.provider_set:acme:admin"),
        "{seen:#?}"
    );
    assert_eq!(
        seen.iter()
            .filter(|s| *s == "login:acme.bob:member")
            .count(),
        2,
        "{seen:#?}"
    );
    assert!(
        log.entries
            .iter()
            .filter(|e| e.kind == "login")
            .all(|e| e.target == "sso" && !e.detail.contains(SECRET)),
        "{:#?}",
        log.entries
    );
}

/// ★ **Each broken token is refused, and nothing is created or enrolled.**
#[tokio::test(flavor = "multi_thread")]
async fn a_broken_id_token_creates_nobody() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    for (tweak, words) in [
        (fake_op::Tweak::WrongAudience, "audience"),
        (fake_op::Tweak::Expired, "expired"),
        (fake_op::Tweak::ForeignKey, "does not verify"),
        (fake_op::Tweak::HmacConfusion, "not allowed"),
        (fake_op::Tweak::WrongNonce, "nonce"),
        (fake_op::Tweak::WrongIssuer, "issuer"),
    ] {
        h.op.tweak_next(tweak);
        let (answer, _) = h.oidc_login("acme", bob()).await;
        let err = answer.expect_err(&format!("{tweak:?} was accepted"));
        assert_eq!(err.code(), Code::PermissionDenied, "{tweak:?}: {err:?}");
        assert!(
            err.message().contains(words),
            "{tweak:?}: {}",
            err.message()
        );
        assert_eq!(h.sessions(), 0, "{tweak:?} minted a session");
    }
    assert!(
        h.employees("acme").is_empty(),
        "a refused login created an employee"
    );
    assert!(
        h.audit()
            .iter()
            .filter(|l| l.contains("Sso.Finish") && l.contains(" deny "))
            .count()
            >= 6
    );
}

/// ★ **Replay**: the state is single use (a second finish with it is refused
/// even with a fresh code), and the IdP's code is single use too.
#[tokio::test(flavor = "multi_thread")]
async fn a_replayed_state_or_code_is_refused() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    h.op.set_person(bob());
    let begun = h.begin("acme").await;
    let back = h.op.authorize(&begun.redirect_url).unwrap();
    h.finish_oidc(&begun.state, &query(&back, "code"))
        .await
        .expect("once");
    let err = h
        .finish_oidc(&begun.state, &query(&back, "code"))
        .await
        .expect_err("twice");
    assert_eq!(err.code(), Code::PermissionDenied);
    assert!(err.message().contains("already used"), "{}", err.message());
    // A fresh state with the spent code: the IdP refuses the code.
    let begun = h.begin("acme").await;
    let err = h
        .finish_oidc(&begun.state, &query(&back, "code"))
        .await
        .expect_err("a spent code");
    assert!(err.message().contains("invalid_grant"), "{}", err.message());
}

/// ★ **Never across accounts.** A sign-in begun for acme cannot be finished
/// with globex's identity provider's answer (another issuer), and acme's
/// employee is not globex's: the same IdP subject in globex is a NEW person
/// there.
#[tokio::test(flavor = "multi_thread")]
async fn a_subject_from_another_account_is_refused_and_never_shared() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let globex_op = FakeOp::start("gunnar-acme", SECRET).unwrap();
    let mut globex = h.oidc_provider("globex", false, false);
    globex.oidc.as_mut().unwrap().issuer = globex_op.issuer().to_owned();
    h.configure(globex).await;

    // Begun for acme, answered by globex's IdP: its code is not acme's IdP's.
    let begun = h.begin("acme").await;
    let foreign = globex_op
        .authorize(
            &begun
                .redirect_url
                .replace(h.op.issuer(), globex_op.issuer()),
        )
        .unwrap();
    let err = h
        .finish_oidc(&begun.state, &query(&foreign, "code"))
        .await
        .expect_err("globex's code at acme");
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
    assert!(h.employees("acme").is_empty());

    let (a, _) = h.oidc_login("acme", bob()).await;
    let a = a.unwrap().principal.unwrap().id;
    globex_op.set_person(bob());
    let begun = h.begin("globex").await;
    let back = globex_op.authorize(&begun.redirect_url).unwrap();
    let g = h
        .finish_oidc(&begun.state, &query(&back, "code"))
        .await
        .unwrap();
    let g_id = g.principal.unwrap().id;
    assert!(g.created);
    assert_ne!(a, g_id);
    assert_eq!(
        h.stores.auth().employer_of(&g_id).unwrap().as_deref(),
        Some("globex")
    );
}

/// ★ **An existing principal is never attached.** A user `acme.bob` that is
/// not acme's (a stranger on this server) is passed over: the IdP's bob is a
/// NEW employee `acme.bob-2`.
#[tokio::test(flavor = "multi_thread")]
async fn an_existing_principal_is_never_attached() {
    let h = Harness::new();
    h.plane
        .add_principal("acme.bob", "acme.bob", PrincipalKind::User)
        .expect("a stranger");
    h.configure(h.oidc_provider("acme", false, false)).await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let answer = answer.unwrap();
    assert_eq!(answer.principal.unwrap().id, "acme.bob-2");
    assert_eq!(
        h.stores.auth().employer_of("acme.bob").unwrap(),
        None,
        "the stranger became acme's employee"
    );
}

// ── passkeys ────────────────────────────────────────────────────────────────

struct Authenticator {
    key: p256::ecdsa::SigningKey,
    id: Vec<u8>,
}

impl Authenticator {
    fn new(seed: u8, id: &[u8]) -> Authenticator {
        Authenticator {
            key: p256::ecdsa::SigningKey::from_slice(&[seed; 32]).unwrap(),
            id: id.to_vec(),
        }
    }
    fn public(&self) -> Vec<u8> {
        self.key
            .verifying_key()
            .to_sec1_point(false)
            .as_bytes()
            .to_vec()
    }
    fn client_data(kind: &str, challenge: &[u8]) -> Vec<u8> {
        format!(
            r#"{{"type":"{kind}","challenge":"{}","origin":"{ORIGIN}","crossOrigin":false}}"#,
            gunnar_grpc::webauthn::b64url(challenge)
        )
        .into_bytes()
    }
    fn auth_data(counter: u32) -> Vec<u8> {
        let mut d = sha2::Sha256::digest(RP_ID.as_bytes()).to_vec();
        d.push(0x05);
        d.extend_from_slice(&counter.to_be_bytes());
        d
    }
    /// (client data, authenticator data, DER signature) for a get ceremony.
    fn assert(&self, challenge: &[u8], counter: u32) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
        let cd = Self::client_data("webauthn.get", challenge);
        let ad = Self::auth_data(counter);
        let mut msg = ad.clone();
        msg.extend_from_slice(&sha2::Sha256::digest(&cd));
        let sig: p256::ecdsa::Signature = self.key.sign(&msg);
        (cd, ad, sig.to_der().as_bytes().to_vec())
    }
}

impl Harness {
    /// The identity core's sessions on this box.
    fn sessions(&self) -> usize {
        self.identity.host().gunnar_store().tables().sessions.len()
    }

    /// The caller the real gate makes of a core session, or its refusal.
    fn gate_session(&self, token: &str) -> Result<Caller, tonic::Status> {
        let mut request = Request::new(());
        request
            .metadata_mut()
            .insert(SESSION_METADATA_KEY, token.parse().expect("ascii"));
        AuthGate::new(Arc::clone(&self.ctx.control), self.ctx.config.auth)
            .with_identity(Some(Arc::clone(&self.identity)))
            .authenticate_request(&request, now_ms())
            .map_err(|failure| failure.status())
    }

    /// A passkey login through the identity core, as the console relays it.
    fn core_login(
        &self,
        by: &Authenticator,
        counter: u32,
    ) -> Result<nornir_identity::LoggedIn, nornir_identity::Refused> {
        let c = self.identity.login_start(&relay())?;
        let challenge = gunnar_grpc::webauthn::from_b64url(&c.challenge).expect("b64url");
        let (cd, ad, sig) = by.assert(&challenge, counter);
        self.identity.login_finish(nornir_identity::Assertion {
            ceremony_id: c.id,
            credential_id: by.id.clone(),
            client_data_json: cd,
            authenticator_data: ad,
            signature: sig,
            rp_id: RP_ID.into(),
            origin: ORIGIN.into(),
        })
    }

    /// **A passkey login, and what it may then do**: the core's login, then the
    /// session at the real gate — the principal it acts as, or the gate's
    /// refusal (`require_sso`: a passkey alone is no credential).
    fn passkey_login(&self, by: &Authenticator, counter: u32) -> Result<String, tonic::Status> {
        let login = self
            .core_login(by, counter)
            .map_err(nornir_identity::grpc::status)?;
        self.gate_session(&login.token).map(|c| c.principal_id)
    }

    /// Redeem the core's first-passkey `link` with `by`, as the console relays
    /// it: the session the core minted.
    fn redeem(
        &self,
        link: &str,
        by: &Authenticator,
        attestation_object: Vec<u8>,
        public_key: Vec<u8>,
    ) -> Result<nornir_identity::LoggedIn, nornir_identity::Refused> {
        let c = self.identity.redeem_start(link, "", &relay())?;
        let challenge = gunnar_grpc::webauthn::from_b64url(&c.challenge).expect("b64url");
        self.identity.redeem_finish(nornir_identity::Registration {
            ceremony_id: c.id,
            credential_id: by.id.clone(),
            public_key,
            client_data_json: Authenticator::client_data("webauthn.create", &challenge),
            attestation_object,
            rp_id: RP_ID.into(),
            origin: ORIGIN.into(),
        })
    }

    async fn second_factor(
        &self,
        pending: &str,
        session: &str,
    ) -> Result<SecondFactorResponse, tonic::Status> {
        self.sso()
            .second_factor(authed(
                &self.console,
                SecondFactorRequest {
                    pending: pending.into(),
                    session: session.into(),
                },
            ))
            .await
            .map(|r| r.into_inner())
    }
}

/// ★ **`require_sso`**: an employee's passkey alone is refused — they sign in
/// through the company — while the account admin's passkey still logs them
/// in (the break-glass: the admin is not an employee).
#[tokio::test(flavor = "multi_thread")]
async fn require_sso_blocks_an_employees_passkey_but_not_the_admins() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", true, false)).await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let employee = answer.unwrap().principal.unwrap().id;
    let bobs_key = Authenticator::new(3, b"bob-yubikey");
    h.plane
        .add_credential(&employee, &bobs_key.id, &bobs_key.public(), "bob's key", 0)
        .unwrap();
    let admins_key = Authenticator::new(4, b"acme-yubikey");
    h.plane
        .add_credential(
            "acme",
            &admins_key.id,
            &admins_key.public(),
            "acme's YubiKey",
            0,
        )
        .unwrap();

    let refused = h
        .passkey_login(&bobs_key, 1)
        .expect_err("bob's passkey alone");
    assert_eq!(refused.code(), Code::PermissionDenied);
    assert!(
        refused.message().contains("Sign in with acme corp"),
        "{}",
        refused.message()
    );
    let admin = h
        .passkey_login(&admins_key, 1)
        .expect("the admin's break-glass");
    assert_eq!(admin, "acme");

    // Without require_sso the same passkey logs bob in.
    h.configure(h.oidc_provider("acme", false, false)).await;
    let ok = h.passkey_login(&bobs_key, 2).expect("no longer required");
    assert_eq!(ok, employee);
}

/// ★ **`require_passkey`**: after the IdP, a passkey. The first login
/// registers the person's first one; the next asks for an assertion of it;
/// another person's passkey is refused; no session before the passkey.
#[tokio::test(flavor = "multi_thread")]
async fn require_passkey_puts_a_passkey_after_the_idp() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", true, true)).await;
    let (first, session) = h.oidc_login("acme", bob()).await;
    let first = first.unwrap();
    assert_eq!(
        first.second_factor,
        SecondFactorNeed::PasskeyRegistration as i32
    );
    assert!(session.is_empty(), "a session before the passkey");
    assert!(
        !first.enrolment.is_empty(),
        "the core's link for the first passkey"
    );
    assert_eq!(h.sessions(), 0, "a session before the passkey");

    let bobs = Authenticator::new(5, b"bob-first-key");
    // Stinky row 13 (2026-10-10): the key registered is the one bob's
    // authenticator attested, not one the relay swapped in — nor one with no
    // attestation at all. (The core's ceremony; the link stays open.)
    let relays = Authenticator::new(66, b"bob-first-key");
    for (what, attestation_object) in [
        (
            "the attested key is bob's, the relayed one is not",
            gunnar_grpc::webauthn::attestation_none(RP_ID, &bobs.id, &bobs.public(), 0),
        ),
        ("no attestation object", Vec::new()),
    ] {
        let refused = h
            .redeem(
                &first.enrolment,
                &relays,
                attestation_object,
                relays.public(),
            )
            .expect_err(what);
        assert_eq!(
            refused.code,
            nornir_identity::Code::Ceremony,
            "{what}: {refused:?}"
        );
        assert!(
            h.plane.credential(&relays.id).unwrap().is_none(),
            "{what}: the relay's key was stored"
        );
        assert_eq!(h.sessions(), 0, "{what}: a session");
    }
    let first_session = h
        .redeem(
            &first.enrolment,
            &bobs,
            gunnar_grpc::webauthn::attestation_none(RP_ID, &bobs.id, &bobs.public(), 0),
            bobs.public(),
        )
        .expect("the first passkey registers through the core");
    assert_eq!(first_session.session.user_id, "acme.bob");
    // require_sso: until the IdP's ticket takes it, the passkey's session is no credential.
    assert_eq!(
        h.gate_session(&first_session.token)
            .expect_err("not yet admitted")
            .code(),
        Code::PermissionDenied
    );
    let done = h
        .second_factor(&first.pending, &first_session.token)
        .await
        .expect("the first passkey signs in");
    assert_eq!(done.principal.map(|p| p.id).as_deref(), Some("acme.bob"));
    assert_eq!(
        h.gate_session(&first_session.token)
            .expect("admitted")
            .principal_id,
        "acme.bob"
    );
    // The ticket is spent.
    let again = h.second_factor(&first.pending, &first_session.token).await;
    assert_eq!(again.unwrap_err().code(), Code::PermissionDenied);

    let (next, _) = h.oidc_login("acme", bob()).await;
    let next = next.unwrap();
    assert_eq!(next.second_factor, SecondFactorNeed::Passkey as i32);
    assert_eq!(next.credential_ids, vec![bobs.id.clone()]);
    // The account admin's passkey is not bob's.
    let admins = Authenticator::new(6, b"acme-key");
    h.plane
        .add_credential("acme", &admins.id, &admins.public(), "acme", 0)
        .unwrap();
    let theirs = h.core_login(&admins, 1).expect("the admin logs in");
    let wrong = h
        .second_factor(&next.pending, &theirs.token)
        .await
        .expect_err("another person's passkey");
    assert_eq!(wrong.code(), Code::PermissionDenied);

    let (next, _) = h.oidc_login("acme", bob()).await;
    let next = next.unwrap();
    let bob_again = h.core_login(&bobs, 1).expect("bob's passkey");
    h.second_factor(&next.pending, &bob_again.token)
        .await
        .expect("bob's passkey after the IdP");
    assert_eq!(
        h.gate_session(&bob_again.token)
            .expect("admitted")
            .principal_id,
        "acme.bob"
    );
    // A34-24: the passkey's session after the IdP (SecondFactor vouched it) mints bob's own
    // credential; the same passkey's session without the IdP does not even reach the verb.
    h.add_own_key(
        "acme.bob",
        &gunnar_grpc::auth::session_fingerprint(&bob_again.token),
    )
    .await
    .expect("an SSH key from the session the IdP admitted");
    let alone = h
        .core_login(&bobs, 2)
        .expect("bob's passkey alone logs in at the core");
    assert!(
        h.gate_session(&alone.token).is_err(),
        "but that session is no credential under require-SSO"
    );
}

// ── SAML ────────────────────────────────────────────────────────────────────

async fn saml_harness() -> (Harness, FakeIdp) {
    let h = Harness::new();
    let idp = FakeIdp::start().unwrap();
    let provider = SsoProvider {
        account: "acme".into(),
        protocol: SsoProtocol::Saml as i32,
        display_name: "Acme SAML".into(),
        saml: Some(SamlSettings {
            idp_metadata_url: idp.metadata_url(),
            username_attribute: "email".into(),
            ..Default::default()
        }),
        console_origin: ORIGIN.into(),
        ..Default::default()
    };
    let view = h.configure(provider).await;
    let saml = view.provider.unwrap().saml.unwrap();
    assert_eq!(
        saml.idp_entity_id,
        idp.entity_id(),
        "the metadata was read by URL"
    );
    assert!(
        saml.idp_certificates[0].starts_with("SHA256:"),
        "an answer shows fingerprints, never the certificate"
    );
    assert_eq!(view.acs_url, format!("{ORIGIN}/auth/sso/saml/acs"));
    assert!(view.sp_metadata_xml.contains("AssertionConsumerService"));
    (h, idp)
}

impl Harness {
    async fn finish_saml(
        &self,
        state: &str,
        response: &str,
    ) -> Result<FinishResponse, tonic::Status> {
        self.sso()
            .finish(authed(
                &self.console,
                FinishRequest {
                    state: state.into(),
                    saml_response: response.into(),
                    console_origin: ORIGIN.into(),
                    ..Default::default()
                },
            ))
            .await
            .map(|r| r.into_inner())
    }
}

/// ★ **SAML, the happy path**, and each broken response refused: another
/// SP's audience, expired, the attacker's key, unsigned, another IdP — and a
/// replayed response (same assertion, a fresh state) refused.
#[tokio::test(flavor = "multi_thread")]
async fn saml_happy_path_and_every_broken_response_refused() {
    let (h, idp) = saml_harness().await;
    let begun = h.begin("acme").await;
    assert_eq!(begun.protocol, SsoProtocol::Saml as i32);
    let posted = idp.respond(&begun.redirect_url).unwrap();
    assert_eq!(posted.relay_state, begun.state);
    let ok = h
        .finish_saml(&posted.relay_state, &posted.saml_response)
        .await
        .expect("a signed response");
    assert!(ok.created);
    assert_eq!(ok.principal.unwrap().id, "acme.carol");

    // Replay: the same response under a NEW state (InResponseTo differs, and
    // the assertion was consumed).
    let begun2 = h.begin("acme").await;
    let err = h
        .finish_saml(&begun2.state, &posted.saml_response)
        .await
        .expect_err("a replayed response");
    assert_eq!(err.code(), Code::PermissionDenied);

    for (tweak, words) in [
        (fake_idp::Tweak::WrongAudience, "udience"),
        (fake_idp::Tweak::Expired, ""),
        (fake_idp::Tweak::ForeignKey, ""),
        (fake_idp::Tweak::Unsigned, ""),
        (fake_idp::Tweak::WrongIssuer, "issued by"),
    ] {
        idp.tweak_next(tweak);
        let begun = h.begin("acme").await;
        let posted = idp.respond(&begun.redirect_url).unwrap();
        let err = h
            .finish_saml(&posted.relay_state, &posted.saml_response)
            .await
            .expect_err(&format!("{tweak:?} accepted"));
        assert_eq!(err.code(), Code::PermissionDenied, "{tweak:?}: {err:?}");
        assert!(
            err.message().contains(words),
            "{tweak:?}: {}",
            err.message()
        );
    }
    assert_eq!(h.employees("acme"), vec!["acme.carol".to_owned()]);
}

// ── SCIM and the CSV import ─────────────────────────────────────────────────

impl Harness {
    async fn scim(
        &self,
        bearer: &str,
        method: &str,
        path: &str,
        query: &str,
        body: serde_json::Value,
    ) -> (u32, serde_json::Value) {
        let answer = self
            .sso()
            .scim(authed(
                &self.console,
                ScimRequest {
                    bearer: bearer.into(),
                    method: method.into(),
                    path: path.into(),
                    query: query.into(),
                    body: if body.is_null() {
                        Vec::new()
                    } else {
                        serde_json::to_vec(&body).unwrap()
                    },
                    base_url: format!("{ORIGIN}/scim/v2"),
                    ..Default::default()
                },
            ))
            .await
            .expect("relayed")
            .into_inner();
        let json = if answer.body.is_empty() {
            serde_json::Value::Null
        } else {
            serde_json::from_slice(&answer.body).unwrap()
        };
        (answer.status, json)
    }
}

/// ★ **A SCIM deactivation names whose sessions it ended** (cov-A
/// 2026-10-06, MEASURED on the estate: the deactivated employee's browser kept
/// its console session, and its next page asked gunnar with a key gunnar no
/// longer takes — an ERROR about "the console's own credential" and a 503):
/// the answer carries the principal, so the console signs them out too; an
/// update that ends nothing names nobody.
#[tokio::test(flavor = "multi_thread")]
async fn a_scim_deactivation_names_the_sessions_it_ended() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let bearer = h
        .sso()
        .create_scim_token(authed(
            &person("acme"),
            CreateScimTokenRequest {
                account: "acme".into(),
                label: "Entra".into(),
                ..Default::default()
            },
        ))
        .await
        .unwrap()
        .into_inner()
        .secret;
    let raw = |method: &str, path: &str, body: serde_json::Value| ScimRequest {
        bearer: bearer.clone(),
        method: method.into(),
        path: path.into(),
        query: String::new(),
        body: serde_json::to_vec(&body).unwrap(),
        base_url: format!("{ORIGIN}/scim/v2"),
        ..Default::default()
    };
    let made = h
        .sso()
        .scim(authed(&h.console, raw("POST", "/Users", serde_json::json!({
            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "carol@acme.example", "active": true,
        }))))
        .await
        .unwrap()
        .into_inner();
    let id = serde_json::from_slice::<serde_json::Value>(&made.body).unwrap()["id"]
        .as_str()
        .unwrap()
        .to_owned();
    assert!(
        made.ended_sessions.is_empty(),
        "a creation ends nobody's sessions"
    );
    let off = |active: &str| {
        serde_json::json!({
            "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
            "Operations": [{"op": "Replace", "path": "active", "value": active}],
        })
    };
    let deactivated = h
        .sso()
        .scim(authed(
            &h.console,
            raw("PATCH", &format!("/Users/{id}"), off("False")),
        ))
        .await
        .unwrap()
        .into_inner();
    assert_eq!(deactivated.status, 200);
    assert_eq!(
        deactivated.ended_sessions,
        vec![id.clone()],
        "the deactivated employee's sessions are named"
    );
    let again = h
        .sso()
        .scim(authed(
            &h.console,
            raw("PATCH", &format!("/Users/{id}"), off("True")),
        ))
        .await
        .unwrap()
        .into_inner();
    assert!(
        again.ended_sessions.is_empty(),
        "a reactivation ends nothing"
    );
    let deleted = h
        .sso()
        .scim(authed(
            &h.console,
            raw("DELETE", &format!("/Users/{id}"), serde_json::Value::Null),
        ))
        .await
        .unwrap()
        .into_inner();
    assert_eq!(
        deleted.ended_sessions,
        vec![id],
        "a deletion ends the sessions too"
    );
}

/// ★ **SCIM**: a bearer per account (the secret once, never again);
/// create → a NEW employee, unbound; its first SSO login claims it; PATCH
/// active=false (Entra's string form) → its sessions revoked and its push
/// refused, data kept; reactivated; DELETE → gone from the directory, still
/// gated; groups; a wrong bearer is 401.
#[tokio::test(flavor = "multi_thread")]
async fn scim_creates_updates_deactivates_and_deletes_employees() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let made = h
        .sso()
        .create_scim_token(authed(
            &person("acme"),
            CreateScimTokenRequest {
                account: "acme".into(),
                label: "Entra".into(),
                ..Default::default()
            },
        ))
        .await
        .unwrap()
        .into_inner();
    let bearer = made.secret.clone();
    let listed = h
        .sso()
        .list_scim_tokens(authed(
            &person("acme"),
            ListScimTokensRequest {
                account: "acme".into(),
            },
        ))
        .await
        .unwrap()
        .into_inner();
    assert!(
        !format!("{listed:?}").contains(&bearer),
        "a listing shows the bearer"
    );
    assert!(
        h.sso()
            .create_scim_token(authed(
                &person("globex"),
                CreateScimTokenRequest {
                    account: "acme".into(),
                    label: String::new(),
                    ..Default::default()
                }
            ))
            .await
            .is_err(),
        "another account's admin made acme a bearer"
    );

    let (status, _) = h
        .scim("gscim_wrong", "GET", "/Users", "", serde_json::Value::Null)
        .await;
    assert_eq!(status, 401);

    let (status, created) = h
        .scim(
            &bearer,
            "POST",
            "/Users",
            "",
            serde_json::json!({
                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                "userName": "Bob@Acme.example", "externalId": "entra-1",
                "name": {"givenName": "Bob", "familyName": "Example"},
                "emails": [{"value": "bob@acme.example", "primary": true}], "active": true,
            }),
        )
        .await;
    assert_eq!(status, 201, "{created}");
    let id = created["id"].as_str().unwrap().to_owned();
    assert_eq!(id, "acme.bob");
    assert_eq!(
        h.stores.auth().employer_of(&id).unwrap().as_deref(),
        Some("acme")
    );
    let (status, dup) = h
        .scim(
            &bearer,
            "POST",
            "/Users",
            "",
            serde_json::json!({"userName": "bob@acme.example"}),
        )
        .await;
    assert_eq!(
        (status, dup["scimType"].as_str()),
        (409, Some("uniqueness"))
    );
    let (_, found) = h
        .scim(
            &bearer,
            "GET",
            "/Users",
            "filter=userName%20eq%20%22bob%40acme.example%22",
            serde_json::Value::Null,
        )
        .await;
    assert_eq!(found["totalResults"], 1, "{found}");

    // Its first SSO login claims it: the same principal, not a new one. The
    // IdP vouches for bob's address in acme.example (A34-2: a user name alone
    // never claims), a domain acme proved (A34-5).
    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let mut verified_bob = bob();
    verified_bob.email_verified = Some(true);
    let (login, _) = h.oidc_login("acme", verified_bob).await;
    let login = login.unwrap();
    assert!(!login.created);
    assert_eq!(login.principal.unwrap().id, id);
    let session = login.session.clone();
    assert!(
        h.gate_session(&session).is_ok(),
        "the employee's session acts"
    );
    let app = h
        .stores
        .catalog()
        .create(gunnar_core::NewRepo::new(
            gunnar_core::RepoRef::new("acme", "app").unwrap(),
        ))
        .expect("acme/app");
    h.plane
        .grant(&id, "acme", "app", GrantLevel::Write)
        .unwrap();
    assert!(h
        .stores
        .auth()
        .may(&id, &app, gunnar_core::Level::Write)
        .unwrap());

    let (status, patched) = h
        .scim(
            &bearer,
            "PATCH",
            &format!("/Users/{id}"),
            "",
            serde_json::json!({
                "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                "Operations": [{"op": "Replace", "path": "active", "value": "False"}],
            }),
        )
        .await;
    assert_eq!(status, 200, "{patched}");
    assert_eq!(patched["active"], false);
    assert!(
        h.gate_session(&session).is_err(),
        "the session survived the deactivation"
    );
    assert!(
        h.identity.session(&session).is_err(),
        "the core still holds the session"
    );
    assert!(
        !h.stores
            .auth()
            .may(&id, &app, gunnar_core::Level::Write)
            .unwrap(),
        "a deactivated employee can push"
    );
    assert!(
        h.stores.auth().principal(&id).unwrap().is_some(),
        "the data (the employee) is kept"
    );
    let (refused, _) = h.oidc_login("acme", bob()).await;
    assert!(refused.unwrap_err().message().contains("deactivated"));

    let (status, back) = h
        .scim(
            &bearer,
            "PATCH",
            &format!("/Users/{id}"),
            "",
            serde_json::json!({"Operations": [{"op": "replace", "value": {"active": true}}]}),
        )
        .await;
    assert_eq!((status, back["active"].as_bool()), (200, Some(true)));
    assert!(h
        .stores
        .auth()
        .may(&id, &app, gunnar_core::Level::Write)
        .unwrap());

    let (status, group) = h
        .scim(
            &bearer,
            "POST",
            "/Groups",
            "",
            serde_json::json!({"displayName": "Engineering", "members": [{"value": id}]}),
        )
        .await;
    assert_eq!(status, 201, "{group}");
    let gid = group["id"].as_str().unwrap().to_owned();
    let (status, _) = h.scim(&bearer, "PATCH", &format!("/Groups/{gid}"), "", serde_json::json!({"Operations": [{"op": "remove", "path": format!("members[value eq \"{id}\"]")}]})).await;
    assert_eq!(status, 200);
    let (_, g) = h
        .scim(
            &bearer,
            "GET",
            &format!("/Groups/{gid}"),
            "",
            serde_json::Value::Null,
        )
        .await;
    assert_eq!(g["members"].as_array().unwrap().len(), 0, "{g}");
    let (status, _) = h
        .scim(
            &bearer,
            "POST",
            "/Groups",
            "",
            serde_json::json!({"displayName": "Ghosts", "members": [{"value": "globex"}]}),
        )
        .await;
    assert_eq!(status, 400, "a member from outside the directory");

    let (status, _) = h
        .scim(
            &bearer,
            "DELETE",
            &format!("/Users/{id}"),
            "",
            serde_json::Value::Null,
        )
        .await;
    assert_eq!(status, 204);
    let (status, _) = h
        .scim(
            &bearer,
            "GET",
            &format!("/Users/{id}"),
            "",
            serde_json::Value::Null,
        )
        .await;
    assert_eq!(status, 404);
    assert!(
        h.stores.auth().gate_of(&id).unwrap().is_some(),
        "a deleted user is still gated"
    );
    assert!(h.stores.auth().principal(&id).unwrap().is_some());

    h.sso()
        .revoke_scim_token(authed(
            &person("acme"),
            RevokeScimTokenRequest {
                account: "acme".into(),
                token_id: made.token.unwrap().token_id,
            },
        ))
        .await
        .unwrap();
    let (status, _) = h
        .scim(&bearer, "GET", "/Users", "", serde_json::Value::Null)
        .await;
    assert_eq!(status, 401, "a revoked bearer still works");
    let trail = h.audit().join("\n");
    assert!(
        !trail.contains(&bearer),
        "the bearer reached the audit trail"
    );
    assert!(
        trail.contains("Sso.Scim") && trail.contains("deactivated acme.bob"),
        "{trail}"
    );
}

/// ★ **RA5-39: two imports at one moment are never both told "queued" for one job.**
/// `Sso.ImportUsers` asked the account's status and then enqueued without holding the lock;
/// the queue hands back the live job for the account, so the second admin's file was never
/// applied and never refused. Many rounds of two admins at once: an answer that is not a
/// refusal names its own job.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn ra5_39_two_imports_at_once_are_never_one_job() {
    let h = Arc::new(Harness::new());
    for round in 0..200 {
        let barrier = Arc::new(tokio::sync::Barrier::new(2));
        let ask = |n: u32| {
            let (h, barrier) = (Arc::clone(&h), Arc::clone(&barrier));
            tokio::spawn(async move {
                let csv = format!(
                    "user_name,display_name,email,active\nr{round}n{n}@acme.example,R{n},,true\n"
                );
                barrier.wait().await;
                h.sso()
                    .import_users(authed(
                        &person("acme"),
                        ImportUsersRequest {
                            account: "acme".into(),
                            csv,
                            dry_run: true,
                        },
                    ))
                    .await
                    .map(|r| r.into_inner().job_id)
            })
        };
        let (a, b) = (ask(1), ask(2));
        let (a, b) = (a.await.unwrap(), b.await.unwrap());
        if let (Ok(a), Ok(b)) = (&a, &b) {
            assert_ne!(
                a, b,
                "round {round}: both admins were told their file is queued, as one job"
            );
        }
        // Let the round's job end before the next.
        for _ in 0..500 {
            match h.ctx.control.sso_import_status("acme").unwrap() {
                Some(job) if job.state == "queued" || job.state == "running" => {
                    tokio::time::sleep(std::time::Duration::from_millis(2)).await
                }
                _ => break,
            }
        }
    }
}

/// ★ **The CSV import**: NEW employees, an update, a deactivation; a dry run
/// writes nothing; a bad row is named and the rest go in.
#[tokio::test(flavor = "multi_thread")]
async fn the_csv_import_provisions_updates_and_deactivates() {
    let h = Harness::new();
    let csv = "user_name,display_name,email,active\nann@acme.example,Ann,ann@acme.example,true\nben@acme.example,Ben,,true\n,nobody,,\n";
    let dry = h
        .import_as(&person("acme"), "acme", csv, true)
        .await
        .unwrap();
    assert_eq!(dry.state, "done", "{dry:?}");
    assert_eq!((dry.created, dry.errors.len()), (2, 1), "{dry:?}");
    assert!(h.employees("acme").is_empty(), "a dry run wrote");
    let done = h
        .import_as(&person("acme"), "acme", csv, false)
        .await
        .unwrap();
    assert_eq!(done.created, 2, "{done:?}");
    assert_eq!(
        h.employees("acme"),
        vec!["acme.ann".to_owned(), "acme.ben".to_owned()]
    );
    let again =
        "user_name,display_name,active\nann@acme.example,Ann A.,true\nben@acme.example,Ben,false\n";
    let done = h
        .import_as(&person("acme"), "acme", again, false)
        .await
        .unwrap();
    assert_eq!((done.updated, done.deactivated), (1, 1), "{done:?}");
    // ★ A34-9: the deactivated person is named, so the console signs them
    // out as it does for SCIM's deactivation.
    assert_eq!(done.ended_sessions, vec!["acme.ben".to_owned()], "{done:?}");
    assert!(h.stores.auth().gate_of("acme.ben").unwrap().is_some());
    let dir = h
        .sso()
        .list_directory(authed(
            &person("acme"),
            ListDirectoryRequest {
                account: "acme".into(),
            },
        ))
        .await
        .unwrap()
        .into_inner();
    let ann = dir
        .users
        .iter()
        .find(|u| u.principal_id == "acme.ann")
        .unwrap();
    assert_eq!(
        (ann.display_name.as_str(), ann.source.as_str(), ann.bound),
        ("Ann A.", "csv", false)
    );
    // Only the account admin imports.
    let err = h
        .import_as(&person("globex"), "acme", csv, false)
        .await
        .unwrap_err();
    assert_eq!(err.code(), Code::PermissionDenied);
}

/// The provider is the account admin's: another account's admin can neither
/// read nor set it, and an employee cannot either.
#[tokio::test(flavor = "multi_thread")]
async fn only_the_account_admin_configures_its_provider() {
    let h = Harness::new();
    let err = h
        .sso()
        .set_provider(authed(
            &person("globex"),
            SetProviderRequest {
                provider: Some(h.oidc_provider("acme", false, false)),
                client_secret: SECRET.into(),
                clear_client_secret: false,
                clear_idp_ca: false,
            },
        ))
        .await
        .unwrap_err();
    assert_eq!(err.code(), Code::PermissionDenied);
    h.configure(h.oidc_provider("acme", false, false)).await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let employee = answer.unwrap().principal.unwrap().id;
    let err = h
        .sso()
        .get_provider(authed(
            &person(&employee),
            GetProviderRequest {
                account: "acme".into(),
                ..Default::default()
            },
        ))
        .await
        .unwrap_err();
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
    // An https issuer is required (the loopback test door aside).
    h.ctx.sso.set_allow_loopback_http(false);
    let mut plain = h.oidc_provider("acme", false, false);
    plain.oidc.as_mut().unwrap().issuer = "http://idp.example.test".into();
    let err = h
        .sso()
        .set_provider(authed(
            &person("acme"),
            SetProviderRequest {
                provider: Some(plain),
                client_secret: String::new(),
                clear_client_secret: false,
                clear_idp_ca: false,
            },
        ))
        .await
        .unwrap_err();
    assert_eq!(err.code(), Code::InvalidArgument, "{err:?}");
}

// ── A34-2: a provisioned employee is claimed only on a stable IdP id or a
//    verified e-mail ───────────────────────────────────────────────────────

impl Harness {
    /// A SCIM bearer for `account` (its admin asks).
    async fn scim_bearer(&self, account: &str) -> String {
        self.sso()
            .create_scim_token(authed(
                &person(account),
                CreateScimTokenRequest {
                    account: account.into(),
                    label: "a34".into(),
                    ..Default::default()
                },
            ))
            .await
            .expect("a SCIM token")
            .into_inner()
            .secret
    }

    /// SCIM provisions `user_name` in acme with `external_id`; its id.
    async fn provision(&self, user_name: &str, external_id: &str) -> String {
        let bearer = self.scim_bearer("acme").await;
        let (status, made) = self
            .scim(
                &bearer,
                "POST",
                "/Users",
                "",
                serde_json::json!({
                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                    "userName": user_name, "externalId": external_id,
                    "emails": [{"value": user_name, "primary": true}], "active": true,
                }),
            )
            .await;
        assert_eq!(status, 201, "{made}");
        made["id"].as_str().unwrap().to_owned()
    }

    fn bound(&self, id: &str) -> bool {
        self.stores
            .auth()
            .sso_user(id)
            .unwrap()
            .is_some_and(|u| u.subject.is_some())
    }
}

/// ★ A34-2 (OIDC): **an unverified e-mail cannot claim** a SCIM-provisioned
/// employee. The IdP says bob's address but not that it verified it (the
/// fake sends no `email_verified`: self-service sign-up, an edited profile).
/// Refused by name, audited, and nothing is bound or created.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_oidc_an_unverified_email_cannot_claim_a_provisioned_employee() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let id = h.provision("bob@acme.example", "entra-1").await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let refused = answer.expect_err("an unverified e-mail claimed a provisioned employee");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("verified"),
        "refused by name: {}",
        refused.message()
    );
    assert!(!h.bound(&id), "the employee was bound");
    assert_eq!(h.employees("acme"), vec![id], "a second employee was made");
    assert!(
        h.audit()
            .iter()
            .any(|l| l.contains(" deny ") && l.contains("claim")),
        "the refusal is audited by name: {:?}",
        h.audit()
    );
}

/// ★ A34-2 (OIDC): **a matching `preferred_username` cannot claim**: it is
/// editable at many IdPs and is not a claim key.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_oidc_a_matching_preferred_username_cannot_claim() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let id = h.provision("carl@acme.example", "entra-2").await;
    let mut mallory =
        fake_op::Person::new("00u-mallory", "mallory@elsewhere.mock.invalid", "Mallory");
    mallory.preferred_username = "carl@acme.example".into();
    let (answer, _) = h.oidc_login("acme", mallory).await;
    let refused = answer.expect_err("a matching preferred_username claimed carl");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("preferred_username") || refused.message().contains("user name"),
        "refused by name: {}",
        refused.message()
    );
    assert!(!h.bound(&id), "carl was bound to mallory");
}

/// ★ A34-2 (OIDC): **a stable-id binding can claim**: the SCIM `externalId`
/// is the IdP's `sub`, so bob's sign-in binds the provisioned employee.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_oidc_a_stable_id_binding_claims() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let id = h.provision("bob@acme.example", "00u-bob").await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let done = answer.expect("externalId == sub claims");
    assert!(!done.created);
    assert_eq!(done.principal.unwrap().id, id);
    assert!(h.bound(&id));
}

/// ★ A34-2 (SAML): **an e-mail attribute cannot claim**: SAML carries no
/// verification, so carol's address (the mapped user-name attribute) does
/// not bind a provisioned employee whose externalId is another id.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_saml_an_email_attribute_cannot_claim_a_provisioned_employee() {
    let (h, idp) = saml_harness().await;
    let id = h.provision("carol@acme.example", "hr-77").await;
    let begun = h.begin("acme").await;
    let posted = idp.respond(&begun.redirect_url).unwrap();
    let refused = h
        .finish_saml(&posted.relay_state, &posted.saml_response)
        .await
        .expect_err("an e-mail attribute claimed a provisioned employee");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("claim"),
        "refused by name: {}",
        refused.message()
    );
    assert!(!h.bound(&id), "carol's employee was bound");
}

/// ★ A34-2 (SAML): **a persistent NameID binding can claim** (SCIM
/// `externalId` == the persistent NameID).
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_saml_a_persistent_name_id_binding_claims() {
    let (h, idp) = saml_harness().await;
    let id = h
        .provision("carol@acme.example", "carol@acme.example")
        .await;
    let begun = h.begin("acme").await;
    let posted = idp.respond(&begun.redirect_url).unwrap();
    let done = h
        .finish_saml(&posted.relay_state, &posted.saml_response)
        .await
        .expect("externalId == the persistent NameID claims");
    assert!(!done.created);
    assert_eq!(done.principal.unwrap().id, id);
    assert!(h.bound(&id));
}

/// ★ A34-2 (OIDC): **a verified e-mail in one of the account's e-mail
/// domains claims** the provisioned employee whose address it is, and the
/// binding is audited with how it was made.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_oidc_a_verified_email_in_a_claimed_domain_claims() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let id = h.provision("bob@acme.example", "hr-12").await;
    let mut verified = bob();
    verified.email_verified = Some(true);
    // A typed domain proves nothing (A34-5): the claim waits for its DNS proof.
    let (answer, _) = h.oidc_login("acme", verified.clone()).await;
    let refused = answer.expect_err("an unproven domain claimed");
    assert!(
        refused.message().contains("[sso-claim-domain]"),
        "{}",
        refused.message()
    );
    assert!(!h.bound(&id));
    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let (answer, _) = h.oidc_login("acme", verified).await;
    let done = answer.expect("a verified e-mail in acme.example claims");
    assert!(!done.created);
    assert_eq!(done.principal.unwrap().id, id);
    assert!(h.bound(&id));
    assert!(
        h.audit().iter().any(|l| l.contains(" allow ")
            && l.contains("claimed=true")
            && l.contains("verified_email")),
        "the binding is audited with its basis: {:?}",
        h.audit()
    );
}

/// ★ A34-2 (OIDC): **a verified e-mail OUTSIDE the account's e-mail domains
/// cannot claim**: the IdP vouches for the address, but the account never
/// said that domain is its own.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_oidc_a_verified_email_outside_the_claimed_domains_cannot_claim() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let id = h.provision("dan@contractor.mock.invalid", "hr-13").await;
    let mut dan = fake_op::Person::new("00u-dan", "dan@contractor.mock.invalid", "Dan");
    dan.email_verified = Some(true);
    let (answer, _) = h.oidc_login("acme", dan).await;
    let refused = answer.expect_err("a verified e-mail outside acme's domains claimed");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("domain"),
        "refused by name: {}",
        refused.message()
    );
    assert!(!h.bound(&id), "dan was bound");
}

/// ★ A34-2 (OIDC): **a second IdP identity cannot steal a bound employee**,
/// not even with a verified e-mail in the account's domain: the row is bound
/// to the first subject and is never re-pointed.
#[tokio::test(flavor = "multi_thread")]
async fn a34_2_oidc_a_second_identity_cannot_steal_a_bound_employee() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let id = h.provision("bob@acme.example", "00u-bob").await;
    let (first, _) = h.oidc_login("acme", bob()).await;
    assert_eq!(
        first
            .expect("externalId == sub claims")
            .principal
            .unwrap()
            .id,
        id
    );
    let mut thief = fake_op::Person::new("00u-thief", "bob@acme.example", "Not Bob");
    thief.email_verified = Some(true);
    let (answer, _) = h.oidc_login("acme", thief).await;
    let refused = answer.expect_err("a second identity took a bound employee");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("re-pointed"),
        "refused by name: {}",
        refused.message()
    );
    let user = h.stores.auth().sso_user(&id).unwrap().unwrap();
    assert_eq!(
        user.subject,
        Some(gunnar_core::sso::subject_key(h.op.issuer(), "00u-bob")),
        "the employee is still bob's"
    );
}

/// ★ **RA4-2: a refused claim names nobody.** The answer goes to an
/// unauthenticated caller and the reason into the append-only audit chain and
/// the warn log (one `reason`), so none of them may carry the person's e-mail
/// or the employee's id: the refusal's code and a plain sentence only.
#[tokio::test(flavor = "multi_thread")]
async fn ra4_2_a_refused_claim_carries_its_code_and_no_email_or_employee_id() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let check = |refused: tonic::Status, secrets: &[&str], code: &str, seen: usize| {
        let msg = refused.message().to_owned();
        assert!(
            msg.contains(code),
            "the answer carries the code {code}: {msg}"
        );
        let audit = h.audit();
        let new: Vec<&String> = audit.iter().skip(seen).collect();
        assert!(
            new.iter().any(|l| l.contains(code)),
            "the audit row carries the code {code}: {new:?}"
        );
        for s in secrets {
            assert!(!msg.contains(s), "the answer names {s}: {msg}");
            for l in &new {
                assert!(
                    !l.contains(s),
                    "the audit row (and the warn log, the same reason) names {s}: {l}"
                );
            }
        }
    };

    // unverified e-mail
    let id = h.provision("bob@acme.example", "hr-1").await;
    let seen = h.audit().len();
    let (answer, _) = h.oidc_login("acme", bob()).await;
    check(
        answer.expect_err("refused"),
        &["bob@acme.example", &id],
        "[sso-claim-unverified]",
        seen,
    );

    // a verified e-mail outside the account's domains
    let id = h.provision("dan@contractor.mock.invalid", "hr-2").await;
    let mut dan = fake_op::Person::new("00u-dan", "dan@contractor.mock.invalid", "Dan");
    dan.email_verified = Some(true);
    let seen = h.audit().len();
    let (answer, _) = h.oidc_login("acme", dan).await;
    check(
        answer.expect_err("refused"),
        &[
            "dan@contractor.mock.invalid",
            "contractor.mock.invalid",
            &id,
        ],
        "[sso-claim-domain]",
        seen,
    );

    // a bound employee is never re-pointed
    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let id = h.provision("eve@acme.example", "00u-eve").await;
    let (first, _) = h
        .oidc_login(
            "acme",
            fake_op::Person::new("00u-eve", "eve@acme.example", "Eve"),
        )
        .await;
    first.expect("externalId == sub claims");
    let mut thief = fake_op::Person::new("00u-thief", "eve@acme.example", "Not Eve");
    thief.email_verified = Some(true);
    let seen = h.audit().len();
    let (answer, _) = h.oidc_login("acme", thief).await;
    check(
        answer.expect_err("refused"),
        &["eve@acme.example", &id],
        "[sso-claim-bound]",
        seen,
    );
}

/// ★ **A34-10: every SSO write verb needs a fresh, user-verified touch.** A
/// stolen admin session could point the account at a hostile IdP, drop it, or
/// mint a SCIM bearer that never expires. Gunnar refuses SetProvider,
/// RemoveProvider, CreateScimToken and ImportUsers from a session that was not
/// minted by a user-verified passkey login in the last ten minutes, with the
/// code `[stepup]` the console answers with a touch in place.
#[tokio::test(flavor = "multi_thread")]
async fn a34_10_an_sso_write_without_a_fresh_user_verified_touch_is_refused() {
    let h = Harness::new();
    let stale = Caller {
        principal_id: "acme".to_owned(),
        fingerprint: "SHA256:a34-10-no-touch".to_owned(),
        machine: false,
        authenticated: true,
    };
    let refused = h
        .sso()
        .set_provider(authed(
            &stale,
            SetProviderRequest {
                provider: Some(h.oidc_provider("acme", false, false)),
                client_secret: SECRET.into(),
                clear_client_secret: false,
                clear_idp_ca: false,
            },
        ))
        .await
        .expect_err("SetProvider from a session with no fresh touch");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("[stepup]"),
        "the console's code: {}",
        refused.message()
    );
    let refused = h
        .sso()
        .create_scim_token(authed(
            &stale,
            CreateScimTokenRequest {
                account: "acme".into(),
                ..Default::default()
            },
        ))
        .await
        .expect_err("CreateScimToken from a session with no fresh touch");
    assert!(
        refused.message().contains("[stepup]"),
        "{}",
        refused.message()
    );
    let refused = h
        .sso()
        .remove_provider(authed(
            &stale,
            RemoveProviderRequest {
                account: "acme".into(),
            },
        ))
        .await
        .expect_err("RemoveProvider from a session with no fresh touch");
    assert!(
        refused.message().contains("[stepup]"),
        "{}",
        refused.message()
    );

    // A session that stepped up (a user-verified touch, the core's) may; one
    // that did not may not.
    let set = |stepped_up: bool| {
        let mut request = authed(
            &stale,
            SetProviderRequest {
                provider: Some(h.oidc_provider("acme", false, false)),
                client_secret: SECRET.into(),
                clear_client_secret: false,
                clear_idp_ca: false,
            },
        );
        request.extensions_mut().insert(PersonSession {
            stepped_up,
            stepped_up_at_unix_ms: None,
        });
        let sso = h.sso();
        async move { sso.set_provider(request).await }
    };
    set(true)
        .await
        .expect("a fresh user-verified touch configures the IdP");
    let refused = set(false)
        .await
        .expect_err("a session that did not step up");
    assert!(
        refused.message().contains("[stepup]"),
        "{}",
        refused.message()
    );
    // and it is audited by name
    assert!(
        h.audit().iter().any(|l| l.contains("[stepup]")),
        "{:?}",
        h.audit()
    );
}

/// ★ **A34-17: the CSV import is a job.** The call answers at once with a
/// job id; the job runs on its own jera lane and the console polls
/// `Sso.ImportStatus`. (That a refused row leaves the whole file unapplied is
/// the identity table's promise: gunnar-core `tests/sso_directory.rs`.) A
/// look-alike user name is refused (A34-43) and the size limit is one, by
/// name (A34-44).
#[tokio::test(flavor = "multi_thread")]
async fn the_csv_import_is_a_job_and_all_or_nothing() {
    let h = Harness::new();
    let first = h
        .sso()
        .import_users(authed(
            &person("acme"),
            ImportUsersRequest {
                account: "acme".into(),
                csv: "user_name\nann@acme.example\n".into(),
                dry_run: false,
            },
        ))
        .await
        .expect("the import is queued")
        .into_inner();
    assert!(!first.job_id.is_empty(), "{first:?}");
    assert!(
        ["queued", "running", "done"].contains(&first.state.as_str()),
        "{first:?}"
    );
    // One import per account at a time: wait for this one before the next.
    let mut state = first.state.clone();
    for _ in 0..500 {
        if state == "done" {
            break;
        }
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        let now = h
            .sso()
            .import_status(authed(
                &person("acme"),
                ImportStatusRequest {
                    account: "acme".into(),
                    job_id: first.job_id.clone(),
                },
            ))
            .await
            .expect("the job is still readable")
            .into_inner();
        assert_eq!(now.job_id, first.job_id);
        state = now.state;
    }
    assert_eq!(state, "done");
    assert_eq!(h.employees("acme"), vec!["acme.ann".to_owned()]);
    let done = h
        .import_as(
            &person("acme"),
            "acme",
            "user_name\nben@acme.example\n",
            false,
        )
        .await
        .unwrap();
    assert_eq!((done.state.as_str(), done.created), ("done", 1), "{done:?}");
    // A look-alike of `ann` (Cyrillic а) is refused by name, never a second person.
    let odd = h
        .import_as(
            &person("acme"),
            "acme",
            "user_name\n\u{430}nn@acme.example\n",
            false,
        )
        .await
        .unwrap();
    assert_eq!((odd.created, odd.errors.len()), (0, 1), "{odd:?}");
    // The size limit is one, named (A34-44).
    let big = format!(
        "user_name\n{}",
        "x".repeat(gunnar_grpc::sso::directory::CSV_MAX_BYTES)
    );
    let err = h
        .import_as(&person("acme"), "acme", &big, false)
        .await
        .unwrap_err();
    assert!(err.message().contains("[csv_too_large]"), "{err:?}");
    let before = h.employees("acme");
    assert_eq!(before.len(), 2, "{before:?}");
}

/// ★ **A34-27: a SCIM bearer may be read-only or expire, and refusals are
/// rate limited per client.**
#[tokio::test(flavor = "multi_thread")]
async fn scim_bearers_expire_carry_a_scope_and_refusals_are_limited() {
    let h = Harness::new();
    let made = h
        .sso()
        .create_scim_token(authed(
            &person("acme"),
            CreateScimTokenRequest {
                account: "acme".into(),
                label: "audit".into(),
                expires_in_days: 30,
                read_only: true,
            },
        ))
        .await
        .unwrap()
        .into_inner();
    let token = made.token.clone().unwrap();
    assert!(
        token.read_only && token.expires_at_unix_ms > now_ms(),
        "{token:?}"
    );
    let (status, _) = h
        .scim(&made.secret, "GET", "/Users", "", serde_json::Value::Null)
        .await;
    assert_eq!(status, 200);
    let (status, _) = h
        .scim(
            &made.secret,
            "POST",
            "/Users",
            "",
            serde_json::json!({ "userName": "eve" }),
        )
        .await;
    assert_eq!(status, 403, "a read-only bearer wrote");
    assert!(h.employees("acme").is_empty());
    // An expiry past a year is refused by name.
    let err = h
        .sso()
        .create_scim_token(authed(
            &person("acme"),
            CreateScimTokenRequest {
                account: "acme".into(),
                label: "x".into(),
                expires_in_days: 400,
                read_only: false,
            },
        ))
        .await
        .unwrap_err();
    assert_eq!(err.code(), Code::InvalidArgument);
    // An expired bearer is a 401 with its own sentence.
    let secret = "gscim_expired_for_the_test";
    h.stores
        .auth()
        .create_scim_token(gunnar_core::sso::ScimToken {
            id: "old".into(),
            account: "acme".into(),
            secret_hash: hex::encode(sha2::Sha256::digest(secret.as_bytes())),
            label: "old".into(),
            created: 1,
            created_by: "acme".into(),
            last_used: None,
            expires: Some(2),
            read_only: false,
        })
        .unwrap();
    let (status, body) = h
        .scim(secret, "GET", "/Users", "", serde_json::Value::Null)
        .await;
    assert_eq!(status, 401);
    assert!(
        body["detail"]
            .as_str()
            .unwrap_or_default()
            .contains("expired"),
        "{body}"
    );
    // A flood of unknown bearers from one client: a few audit rows, then 429.
    let before = h
        .audit()
        .iter()
        .filter(|l| l.contains("198.51.100.7"))
        .count();
    let mut last = 0;
    for _ in 0..40 {
        let answer = h
            .sso()
            .scim(authed(
                &h.console,
                ScimRequest {
                    bearer: "gscim_nope".into(),
                    method: "GET".into(),
                    path: "/Users".into(),
                    base_url: format!("{ORIGIN}/scim/v2"),
                    client: "198.51.100.7:4711".into(),
                    ..Default::default()
                },
            ))
            .await
            .unwrap()
            .into_inner();
        last = answer.status;
    }
    assert_eq!(last, 429, "a flood of refused bearers is throttled");
    let rows = h
        .audit()
        .iter()
        .filter(|l| l.contains("198.51.100.7"))
        .count()
        - before;
    assert!(
        rows <= gunnar_grpc::sso::scim::bearer::AUDITED_PER_MINUTE as usize,
        "{rows} audit rows"
    );
}

/// ★ **A34-47: the addresses the admin registers at the IdP are gunnar's.**
/// With the console's origin, `GetProvider` answers the redirect URI, the SP
/// entity id, the ACS URL, the sign-in link and the SCIM base, provider or
/// not, every path segment percent-encoded; the console builds none. A
/// non-https origin is refused.
#[tokio::test(flavor = "multi_thread")]
async fn the_console_addresses_are_the_servers() {
    let h = Harness::new();
    let got = h
        .sso()
        .get_provider(authed(
            &person("acme"),
            GetProviderRequest {
                account: "acme".into(),
                console_origin: "https://console.example/".into(),
            },
        ))
        .await
        .expect("read")
        .into_inner();
    assert!(got.view.is_none(), "acme has no provider yet");
    let a = got.addresses.expect("the addresses, provider or not");
    assert_eq!(
        a.redirect_uri,
        "https://console.example/auth/sso/oidc/callback"
    );
    assert_eq!(a.acs_url, "https://console.example/auth/sso/saml/acs");
    assert_eq!(a.sp_entity_id, "https://console.example/auth/sso/saml/acme");
    assert_eq!(a.login_url, "https://console.example/login/sso/acme");
    assert_eq!(a.scim_base_url, "https://console.example/scim/v2");
    assert_eq!(
        gunnar_grpc::sso::oidc::gunnar_core_paths::segment("a b/c"),
        "a%20b%2Fc"
    );
    let none = h
        .sso()
        .get_provider(authed(
            &person("acme"),
            GetProviderRequest {
                account: "acme".into(),
                ..Default::default()
            },
        ))
        .await
        .expect("read")
        .into_inner();
    assert!(none.addresses.is_none());
    h.ctx.sso.set_allow_loopback_http(false);
    let err = h
        .sso()
        .get_provider(authed(
            &person("acme"),
            GetProviderRequest {
                account: "acme".into(),
                console_origin: "http://console.example".into(),
            },
        ))
        .await
        .unwrap_err();
    assert_eq!(err.code(), Code::InvalidArgument);
}

// ── A34-22 / A34-48: re-saving a provider keeps what the page does not resend ──

impl Harness {
    async fn resave(&self, provider: SsoProvider) -> Result<ProviderView, tonic::Status> {
        let account = provider.account.clone();
        self.sso()
            .set_provider(authed(
                &person(&account),
                SetProviderRequest {
                    provider: Some(provider),
                    client_secret: String::new(),
                    clear_client_secret: false,
                    clear_idp_ca: false,
                },
            ))
            .await
            .map(|r| r.into_inner().view.expect("a view"))
    }
}

/// ★ **A34-22: a SAML provider configured from PASTED metadata is saved again without the XML.**
/// The page resends only the metadata URL (empty here), the user-name attribute and the
/// toggles; gunnar keeps the stored entity id, sign-on URL and certificates, so the admin can
/// turn require-SSO on without pasting the XML again.
#[tokio::test(flavor = "multi_thread")]
async fn a34_22_a_pasted_saml_provider_is_saved_again_without_its_metadata() {
    let h = Harness::new();
    let idp = FakeIdp::start().unwrap();
    let first = h
        .configure(SsoProvider {
            account: "acme".into(),
            protocol: SsoProtocol::Saml as i32,
            display_name: "Acme SAML".into(),
            saml: Some(SamlSettings {
                idp_metadata_xml: idp.metadata_xml(),
                username_attribute: "email".into(),
                ..Default::default()
            }),
            console_origin: ORIGIN.into(),
            ..Default::default()
        })
        .await;
    let stored = first.provider.unwrap().saml.unwrap();
    assert_eq!(stored.idp_entity_id, idp.entity_id());
    let again = h
        .resave(SsoProvider {
            account: "acme".into(),
            protocol: SsoProtocol::Saml as i32,
            display_name: "Acme SAML".into(),
            saml: Some(SamlSettings {
                username_attribute: "email".into(),
                ..Default::default()
            }),
            require_sso: true,
            console_origin: ORIGIN.into(),
            ..Default::default()
        })
        .await
        .expect("saved again without the XML");
    let p = again.provider.unwrap();
    assert!(p.require_sso, "the toggle was saved");
    let saml = p.saml.unwrap();
    assert_eq!(
        saml.idp_entity_id, stored.idp_entity_id,
        "the stored entity id is kept"
    );
    assert_eq!(
        saml.idp_sso_url, stored.idp_sso_url,
        "the stored sign-on URL is kept"
    );
    assert_eq!(
        saml.idp_certificates, stored.idp_certificates,
        "the stored certificates are kept"
    );
    // Still a working provider: a sign-in goes through.
    let begun = h.begin("acme").await;
    let posted = idp.respond(&begun.redirect_url).unwrap();
    h.finish_saml(&posted.relay_state, &posted.saml_response)
        .await
        .expect("a signed response after the re-save");
}

/// ★ **A34-48 (gunnar's half): the display-name claim and attribute are kept when not resent.**
/// The page has no field for them and sends them empty; an empty field keeps the stored one.
#[tokio::test(flavor = "multi_thread")]
async fn a34_48_an_unsent_display_name_claim_or_attribute_is_kept() {
    let h = Harness::new();
    let mut oidc = h.oidc_provider("acme", false, false);
    oidc.oidc.as_mut().unwrap().display_name_claim = "nickname".into();
    h.configure(oidc).await;
    let again = h
        .resave(h.oidc_provider("acme", true, false))
        .await
        .expect("saved again");
    assert_eq!(
        again.provider.unwrap().oidc.unwrap().display_name_claim,
        "nickname",
        "the stored claim is kept"
    );

    let idp = FakeIdp::start().unwrap();
    let saml = |attribute: &str| SsoProvider {
        account: "globex".into(),
        protocol: SsoProtocol::Saml as i32,
        display_name: "Globex SAML".into(),
        saml: Some(SamlSettings {
            idp_metadata_xml: idp.metadata_xml(),
            display_name_attribute: attribute.into(),
            ..Default::default()
        }),
        console_origin: ORIGIN.into(),
        ..Default::default()
    };
    h.configure(saml("displayName")).await;
    let again = h.resave(saml("")).await.expect("saved again");
    assert_eq!(
        again.provider.unwrap().saml.unwrap().display_name_attribute,
        "displayName",
        "the stored attribute is kept"
    );
}

// ── A34-24: require_sso over every credential an employee mints ─────────────

impl Harness {
    /// `Access.AddKey` on the employee's own principal, signed by `with` (a key fingerprint).
    async fn add_own_key(
        &self,
        employee: &str,
        with: &str,
    ) -> Result<AddKeyResponse, tonic::Status> {
        let (fresh, _) = session();
        let caller = Caller {
            principal_id: employee.into(),
            fingerprint: with.into(),
            machine: false,
            authenticated: true,
        };
        gunnar_grpc::AccessService::new(self.ctx.clone())
            .add_key(authed(
                &caller,
                AddKeyRequest {
                    principal_id: employee.into(),
                    openssh_public_key: fresh,
                    comment: "new laptop".into(),
                    ..Default::default()
                },
            ))
            .await
            .map(|r| r.into_inner())
    }

    /// `Access.AddCredential` (a passkey) on the employee's own principal, signed by `with`.
    async fn add_own_passkey(
        &self,
        employee: &str,
        with: &str,
        seed: u8,
    ) -> Result<AddCredentialResponse, tonic::Status> {
        let key = Authenticator::new(seed, &[seed; 16]);
        let caller = Caller {
            principal_id: employee.into(),
            fingerprint: with.into(),
            machine: false,
            authenticated: true,
        };
        gunnar_grpc::AccessService::new(self.ctx.clone())
            .add_credential(authed(
                &caller,
                AddCredentialRequest {
                    principal_id: employee.into(),
                    credential_id: key.id.clone(),
                    public_key: key.public(),
                    label: format!("key {seed}"),
                    sign_count: 0,
                },
            ))
            .await
            .map(|r| r.into_inner())
    }
}

/// ★ **A34-24: under require-SSO an employee mints a new credential only from a sign-in
/// through the identity provider.** A key that bypasses the IdP (an SSH key that never
/// expires: added before require-SSO, or by the CLI) cannot add another SSH key or a passkey;
/// the session key of an IdP sign-in can. Without require-SSO the old key can again.
#[tokio::test(flavor = "multi_thread")]
async fn a34_24_require_sso_refuses_a_credential_minted_from_a_key_that_bypasses_the_idp() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", true, false)).await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let answer = answer.expect("bob signs in through the IdP");
    let employee = answer.principal.unwrap().id;
    // The IdP sign-in's identity-core session (Sso.Finish vouched it, A34-24 + IDENTITY-HOST).
    assert!(
        !answer.session.is_empty(),
        "the IdP sign-in minted a core session"
    );
    let through_the_idp = gunnar_grpc::auth::session_fingerprint(&answer.session);
    let (laptop, _) = session();
    let old = h
        .plane
        .add_key(&employee, &laptop, "old laptop", None, &employee, None)
        .expect("an SSH key of bob's");

    let refused = h
        .add_own_key(&employee, &old.fingerprint)
        .await
        .expect_err("an SSH key from an SSH key");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("Sign in with acme corp"),
        "refused by name: {}",
        refused.message()
    );
    let refused = h
        .add_own_passkey(&employee, &old.fingerprint, 21)
        .await
        .expect_err("a passkey from an SSH key");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");

    h.add_own_key(&employee, &through_the_idp)
        .await
        .expect("an SSH key from the IdP sign-in");
    h.add_own_passkey(&employee, &through_the_idp, 22)
        .await
        .expect("a passkey from the IdP sign-in");
    assert!(
        h.audit()
            .iter()
            .any(|l| l.starts_with("Access.AddKey") && l.contains(" deny")),
        "the refusal is audited: {:?}",
        h.audit()
    );

    h.configure(h.oidc_provider("acme", false, false)).await;
    h.add_own_key(&employee, &old.fingerprint)
        .await
        .expect("no longer required");
}

/// ★ **A34-24, the identity core's sessions.** A person may call as their core session
/// (fingerprint `session:<hash start>`, IDENTITY-HOST). Under require-SSO an employee's core
/// session mints a new credential only when a sign-in through the IdP admitted it (the
/// persisted admission, RA5-38); a core session from a passkey login bypasses the IdP and is
/// refused by name, as is an admitted one that has ended.
#[tokio::test(flavor = "multi_thread")]
async fn a34_24_require_sso_takes_a_core_session_only_from_an_idp_sign_in() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", true, false)).await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let employee = answer
        .expect("bob signs in through the IdP")
        .principal
        .unwrap()
        .id;
    let by_passkey = gunnar_grpc::auth::session_fingerprint("nis1.a-passkey-login");
    assert!(by_passkey.starts_with("session:"), "{by_passkey}");
    let admitted = || {
        let s = gunnar_grpc::identity::vouched_session(&h.identity, &employee, false)
            .expect("a core session");
        h.identity
            .host()
            .gunnar_store()
            .admit_sso_session(&s.token)
            .expect("admitted");
        s.token
    };
    let by_the_idp = gunnar_grpc::auth::session_fingerprint(&admitted());
    let old = admitted();
    h.identity
        .logout(&old, false)
        .expect("the old sign-in ends");
    let ran_out = gunnar_grpc::auth::session_fingerprint(&old);

    let refused = h
        .add_own_key(&employee, &by_passkey)
        .await
        .expect_err("an SSH key from a passkey-login core session");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("Sign in with acme corp"),
        "refused by name: {}",
        refused.message()
    );
    let refused = h
        .add_own_passkey(&employee, &ran_out, 23)
        .await
        .expect_err("a passkey from an admitted core session that has ended");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");

    h.add_own_key(&employee, &by_the_idp)
        .await
        .expect("an SSH key from the IdP sign-in's core session");
    h.add_own_passkey(&employee, &by_the_idp, 24)
        .await
        .expect("a passkey from the IdP sign-in's core session");
}

/// ★ **RA5-38: one record of "company sign-on vouched for this core session".** The sign-in's
/// admission is persisted (identity.json, read by the gate under require-SSO), but the vouch the
/// credential mint read lived in memory only: after a restart an employee kept acting (admitted)
/// and was refused minting their own credential (not vouched). Both read the persisted record now.
#[tokio::test(flavor = "multi_thread")]
async fn ra5_38_an_sso_admitted_session_mints_its_own_credential_after_a_restart() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", true, false)).await;
    let (answer, token) = h.oidc_login("acme", bob()).await;
    let employee = answer
        .expect("bob signs in through the IdP")
        .principal
        .unwrap()
        .id;
    assert!(!token.is_empty(), "the IdP sign-in's core session");
    assert!(h.gate_session(&token).is_ok(), "the admitted session acts");
    // The server restarts: a new context over the same control plane and identity core.
    let restarted = ServiceContext::new(
        Arc::clone(&h.ctx.control),
        Arc::new(EventBus::new(16)),
        ServerConfig::default(),
    )
    .with_audit_ledger(h.ledger.clone())
    .with_identity(Arc::clone(&h.identity));
    let (fresh, _) = session();
    let caller = Caller {
        principal_id: employee.clone(),
        fingerprint: gunnar_grpc::auth::session_fingerprint(&token),
        machine: false,
        authenticated: true,
    };
    gunnar_grpc::AccessService::new(restarted)
        .add_key(authed(
            &caller,
            AddKeyRequest {
                principal_id: employee,
                openssh_public_key: fresh,
                comment: "after the restart".into(),
                ..Default::default()
            },
        ))
        .await
        .expect("the admitted session mints its own key after a restart");
}

/// The view's proof for `domain`.
fn proof_of<'a>(view: &'a ProviderView, domain: &str) -> &'a DomainProof {
    view.domain_proofs
        .iter()
        .find(|p| p.domain == domain)
        .unwrap_or_else(|| panic!("no proof for {domain}: {view:?}"))
}

/// ★ **A34-5 + A34-39: an e-mail domain routes a sign-in only after its DNS
/// proof**, and Discover answers proven domains alone (never an account name).
/// The view names the record to publish; a domain another account merely typed
/// blocks nobody; a public mail domain is refused; once proven, the domain is
/// that account's, and another account cannot prove it too.
#[tokio::test(flavor = "multi_thread")]
async fn a34_5_a_domain_routes_only_after_its_dns_proof() {
    let h = Harness::new();
    let ask = |hint: &str| {
        let req = authed(&h.console, DiscoverRequest { hint: hint.into() });
        let sso = h.sso();
        async move {
            sso.discover(req)
                .await
                .expect("discover answers")
                .into_inner()
        }
    };
    let view = h.configure(h.oidc_provider("acme", false, false)).await;
    let proof = proof_of(&view, "acme.example");
    assert!(!proof.verified, "{proof:?}");
    assert_eq!(proof.txt_name, "_sso-proof.acme.example");
    assert_eq!(
        proof.txt_value,
        gunnar_grpc::sso::domains::proof_value("acme", "acme.example")
    );
    assert!(
        !ask("bob@acme.example").await.found,
        "an unproven domain routed a sign-in"
    );
    assert!(!ask("acme.example").await.found);
    assert!(
        !ask("acme").await.found,
        "an account name is no hint (A34-39)"
    );

    // globex merely naming acme's domain blocks nobody.
    let mut squat = h.oidc_provider("globex", false, false);
    squat.domains = vec!["acme.example".into()];
    h.configure(squat).await;

    // The record is not there: refused by code, and nothing routes.
    h.ctx
        .sso
        .set_dns_resolver(&doh_server(vec!["v=spf1 -all".into()]));
    let refused = h
        .verify_domain("acme", "acme.example")
        .await
        .expect_err("no record, no proof");
    assert!(
        refused.message().contains("[sso-domain-unproven]"),
        "{}",
        refused.message()
    );
    // globex's record proves nothing for acme.
    h.ctx
        .sso
        .set_dns_resolver(&doh_server(vec![gunnar_grpc::sso::domains::proof_value(
            "globex",
            "acme.example",
        )]));
    assert!(h.verify_domain("acme", "acme.example").await.is_err());

    let view = h
        .prove("acme", "acme.example")
        .await
        .expect("the record is there");
    assert!(proof_of(&view, "acme.example").verified);
    assert!(view
        .provider
        .as_ref()
        .unwrap()
        .verified_domains
        .contains(&"acme.example".to_owned()));
    let found = ask("Bob@ACME.example").await;
    assert!(found.found && found.account == "acme", "{found:?}");
    assert!(ask("acme.example").await.found);
    assert!(
        !ask("acme").await.found,
        "an account name is still no hint (A34-39)"
    );

    // The proof is acme's now: globex cannot prove it, even with its own record.
    let refused = h
        .prove("globex", "acme.example")
        .await
        .expect_err("a second account proved a taken domain");
    assert!(
        refused.message().contains("[sso-domain-taken]"),
        "{}",
        refused.message()
    );
    assert_eq!(ask("bob@acme.example").await.account, "acme");

    // A public mail domain leads to no company.
    let mut public = h.oidc_provider("acme", false, false);
    public.domains = vec!["gmail.com".into()];
    let refused = h
        .sso()
        .set_provider(authed(
            &person("acme"),
            SetProviderRequest {
                provider: Some(public),
                ..Default::default()
            },
        ))
        .await
        .expect_err("gmail.com was taken as a company domain");
    assert!(
        refused.message().contains("[sso-domain-public]"),
        "{}",
        refused.message()
    );
    assert!(
        h.audit()
            .iter()
            .any(|l| l.contains("Sso.VerifyDomain") && l.contains("allow domain=acme.example")),
        "the proof is audited: {:?}",
        h.audit()
    );
}

/// ★ **A34-11: an IdP migration does not lock the directory out.** Bob is bound
/// to the old IdP's subject; the account moves (a new subject for the same
/// person) and his sign-in is refused, never re-pointed; the admin opens a
/// rebind, and bob's next sign-in binds the SAME employee afresh — on the claim
/// rules — while the old subject no longer signs anybody in.
#[tokio::test(flavor = "multi_thread")]
async fn a34_11_a_rebind_lets_the_new_idp_bind_the_same_employee() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let (first, _) = h.oidc_login("acme", bob()).await;
    let bob_id = first.expect("bob's first sign-in").principal.unwrap().id;
    let mut migrated = fake_op::Person::new("new-idp-bob", "bob@acme.example", "Bob Example");
    migrated.email_verified = Some(true);
    let (answer, _) = h.oidc_login("acme", migrated.clone()).await;
    let refused = answer.expect_err("a new subject took bob");
    assert!(
        refused.message().contains("[sso-claim-bound]"),
        "{}",
        refused.message()
    );

    let rebound = h
        .sso()
        .rebind_users(authed(
            &person("acme"),
            RebindUsersRequest {
                account: "acme".into(),
                principal_ids: vec![bob_id.clone()],
                ..Default::default()
            },
        ))
        .await
        .expect("the account admin opens a rebind")
        .into_inner();
    assert_eq!(rebound.users.len(), 1);
    assert!(
        rebound.users[0].rebind && !rebound.users[0].bound,
        "{:?}",
        rebound.users
    );
    let (answer, _) = h.oidc_login("acme", migrated).await;
    let done = answer.expect("the rebind lets the new IdP bind bob");
    assert!(!done.created, "a new employee was made");
    assert_eq!(done.principal.unwrap().id, bob_id);
    assert_eq!(h.employees("acme"), vec![bob_id.clone()]);
    let user = h.stores.auth().sso_user(&bob_id).unwrap().unwrap();
    assert_eq!(
        user.subject,
        Some(gunnar_core::sso::subject_key(h.op.issuer(), "new-idp-bob"))
    );
    // The old subject signs in nobody now: it would be a new person (JIT), not bob.
    let (old, _) = h.oidc_login("acme", bob()).await;
    assert_ne!(
        old.ok().and_then(|d| d.principal).map(|p| p.id),
        Some(bob_id),
        "the old subject still signs bob in"
    );
    assert!(
        h.audit()
            .iter()
            .any(|l| l.contains("Sso.RebindUsers") && l.contains("allow employees=1")),
        "the rebind is audited: {:?}",
        h.audit()
    );
}

/// ★ **A34-25: JIT is the provider's to limit.** `off`: nobody new is made;
/// `domains`: only a verified e-mail in a proven domain.
#[tokio::test(flavor = "multi_thread")]
async fn a34_25_jit_follows_the_providers_policy() {
    let h = Harness::new();
    let mut off = h.oidc_provider("acme", false, false);
    off.jit = SsoJit::Off as i32;
    let view = h.configure(off).await;
    assert_eq!(view.provider.unwrap().jit, SsoJit::Off as i32);
    h.prove("acme", "acme.example")
        .await
        .expect("acme proves its domain");
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let refused = answer.expect_err("JIT off made an employee");
    assert!(
        refused.message().contains("[sso-jit-off]"),
        "{}",
        refused.message()
    );
    assert!(h.employees("acme").is_empty());

    let mut domains = h.oidc_provider("acme", false, false);
    domains.jit = SsoJit::Domains as i32;
    h.configure(domains).await;
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let refused = answer.expect_err("an unverified e-mail was made an employee");
    assert!(
        refused.message().contains("[sso-jit-domain]"),
        "{}",
        refused.message()
    );
    let mut verified = bob();
    verified.email_verified = Some(true);
    let (answer, _) = h.oidc_login("acme", verified).await;
    assert!(
        answer
            .expect("a verified e-mail in a proven domain")
            .created
    );
}

/// ★ **A34-46: a refusal carries its code and names nobody.** A deactivated
/// person's sign-in says `[sso-deactivated]`, never their id.
#[tokio::test(flavor = "multi_thread")]
async fn a34_46_a_deactivated_sign_in_is_refused_by_code() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let (first, _) = h.oidc_login("acme", bob()).await;
    let id = first.expect("bob's first sign-in").principal.unwrap().id;
    h.sso()
        .set_user_active(authed(
            &person("acme"),
            SetUserActiveRequest {
                account: "acme".into(),
                principal_id: id.clone(),
                active: false,
            },
        ))
        .await
        .expect("the admin deactivates bob");
    let (answer, _) = h.oidc_login("acme", bob()).await;
    let refused = answer.expect_err("a deactivated person signed in");
    assert!(
        refused.message().contains("[sso-deactivated]"),
        "{}",
        refused.message()
    );
    assert!(
        !refused.message().contains(&id),
        "the refusal names the employee: {}",
        refused.message()
    );
}

/// An egress that dials nothing and writes down every URL it was asked for.
#[derive(Default)]
struct Dialled(Mutex<Vec<String>>);

impl gunnar_grpc::sso::Egress for Dialled {
    fn get(&self, url: &str) -> Result<(u16, String), String> {
        self.0.lock().unwrap().push(url.to_owned());
        Err("this test dials nothing".into())
    }
    fn post_form(&self, url: &str, _: &str, _: &str) -> Result<(u16, String), String> {
        self.0.lock().unwrap().push(url.to_owned());
        Err("this test dials nothing".into())
    }
}

/// ★ **RA5-24: no default public resolver.** With SSO egress on and no
/// `sso_dns_resolver` fed, a domain proof is refused by name
/// (`[sso-domain-resolver]`, "feed sso_dns_resolver") and nothing is dialled:
/// before, the customer's domain and proof name went to https://dns.google.
#[tokio::test(flavor = "multi_thread")]
async fn ra5_24_an_unfed_resolver_refuses_the_domain_proof_and_dials_nobody() {
    let h = Harness::new();
    h.configure(h.oidc_provider("acme", false, false)).await;
    let dialled = Arc::new(Dialled::default());
    h.ctx.sso.set_egress(dialled.clone());
    h.ctx.sso.set_dns_resolver("");
    let refused = h
        .verify_domain("acme", "acme.example")
        .await
        .expect_err("no resolver, no proof");
    assert!(
        refused.message().contains("[sso-domain-resolver]"),
        "{}",
        refused.message()
    );
    assert!(
        refused.message().contains("feed sso_dns_resolver"),
        "{}",
        refused.message()
    );
    assert_eq!(
        *dialled.0.lock().unwrap(),
        Vec::<String>::new(),
        "a resolver nobody configured was dialled"
    );
}