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_keycloak.rs

//! ★ **Company SSO against a REAL identity provider** (owner 2026-10-26, lane T-OWNERFLOW:
//! "add tests for this, maybe container-based IdM"). `tests/sso.rs` holds every rule against
//! edda's in-process fakes; this file holds what only a real IdP can show: Keycloak's own
//! discovery document, key set, ID tokens, login form, SAML metadata, signatures and
//! assertions, reached through gunnar's REAL egress (`UreqEgress`, the pinned resolver, an
//! https IdP on the operator's internal network behind the account's own CA).
//!
//! The IdP is Keycloak in a container (`KEYCLOAK_IMAGE` in `xtask/src/idp.rs`), started by
//! **`cargo xtask idp`**: a private podman network, Keycloak on it as `idp.acme.test:8443`
//! over https with a certificate from a CA minted for the run, and this test binary in the
//! toolchain container on the same network. Every test here is `#[ignore]`d so a plain
//! `cargo test` (no IdP) skips it by name; the xtask runs them with `--ignored`.
//!
//! The environment the xtask hands in (all required; a test without it fails by name):
//! * `GUNNAR_IDP_URL` — `https://idp.acme.test:8443`;
//! * `GUNNAR_IDP_CA` — the run CA's PEM (a FILE path);
//! * `GUNNAR_IDP_ADMIN_PASSWORD_FILE` — Keycloak's bootstrap admin password (a FILE path,
//!   never printed);
//! * `GUNNAR_IDP_NETS` — the podman network's subnet (the operator's internal network).
//!
//! Each test makes its OWN realm through Keycloak's admin REST API, so the tests share the
//! container and nothing else. People are `*@acme.mock.invalid`.
//!
//! Tests that are RED on today's code for a row another lane fixes carry their own ignore
//! reason (`red until <row> merges`), and the xtask skips them by name until the row merges
//! (`xtask/src/idp.rs` `RED_UNTIL`).
#![cfg(feature = "sso-egress")] // A32-18: the airgap default carries no HTTP client; the appliance rows and CI name `sso-egress`.

use std::collections::BTreeMap;
use std::io::Read as _;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use gunnar_grpc::auth::{AuditLedger, AuditRecord, AuthGate, SESSION_METADATA_KEY};
use gunnar_grpc::sso::PrivateNets;
use gunnar_grpc::{Caller, ControlPlane, EventBus, ServerConfig, ServiceContext, SsoService};
use gunnar_proto::sso_server::Sso as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use p256::ecdsa::signature::Signer as _;
use sha2::Digest as _;
use tonic::{Code, Request};

const ORIGIN: &str = "https://gunnar.acme.test";
const RP_ID: &str = "gunnar.acme.test";
const SECRET: &str = "keycloak-client-secret-NEVER-LOGGED";
const DOMAIN: &str = "acme.mock.invalid";
const PASSWORD: &str = "a-test-password-for-a-test-realm";

// ── the IdP, from the xtask's environment ───────────────────────────────────

#[derive(Clone)]
struct Idp {
    base: String,
    ca_pem: String,
    admin_password: String,
    nets: String,
}

fn env(name: &str) -> String {
    std::env::var(name).unwrap_or_else(|_| {
        panic!("[idp-env] {name} is not set: these tests need a real Keycloak, run them with `cargo xtask idp`")
    })
}

impl Idp {
    fn from_env() -> Idp {
        let read = |var: &str| {
            let path = env(var);
            std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("[idp-env] {var}={path}: {e}"))
        };
        Idp {
            base: env("GUNNAR_IDP_URL").trim_end_matches('/').to_owned(),
            ca_pem: read("GUNNAR_IDP_CA"),
            admin_password: read("GUNNAR_IDP_ADMIN_PASSWORD_FILE").trim().to_owned(),
            nets: env("GUNNAR_IDP_NETS"),
        }
    }

    fn issuer(&self, realm: &str) -> String {
        format!("{}/realms/{realm}", self.base)
    }

    /// A client that trusts the run's CA, follows NO redirect and returns every status.
    fn agent(&self) -> ureq::Agent {
        let ca =
            ureq::tls::Certificate::from_pem(self.ca_pem.as_bytes()).expect("the run CA parses");
        let tls = ureq::tls::TlsConfig::builder()
            .root_certs(ureq::tls::RootCerts::Specific(Arc::new(vec![ca])))
            .build();
        ureq::Agent::config_builder()
            .tls_config(tls)
            .max_redirects(0)
            .max_redirects_will_error(false)
            .http_status_as_error(false)
            .timeout_global(Some(Duration::from_secs(30)))
            .build()
            .into()
    }

    fn admin_token(&self) -> String {
        let mut r = self
            .agent()
            .post(&format!(
                "{}/realms/master/protocol/openid-connect/token",
                self.base
            ))
            .send_form([
                ("grant_type", "password"),
                ("client_id", "admin-cli"),
                ("username", "admin"),
                ("password", self.admin_password.as_str()),
            ])
            .expect("Keycloak's token door answers");
        let body = r.body_mut().read_to_string().unwrap_or_default();
        assert_eq!(
            r.status().as_u16(),
            200,
            "the Keycloak admin sign-in was refused (body withheld)"
        );
        let v: serde_json::Value = serde_json::from_str(&body).unwrap();
        v["access_token"].as_str().unwrap().to_owned()
    }

    /// One admin REST call: (status, body, Location).
    fn admin(
        &self,
        method: &str,
        path: &str,
        body: Option<serde_json::Value>,
    ) -> (u16, String, String) {
        let url = format!("{}/admin/realms{path}", self.base);
        let auth = format!("Bearer {}", self.admin_token());
        let agent = self.agent();
        let json = body.unwrap_or(serde_json::Value::Null).to_string();
        let resp = match method {
            "GET" => agent.get(&url).header("Authorization", &auth).call(),
            "DELETE" => agent.delete(&url).header("Authorization", &auth).call(),
            "POST" => agent
                .post(&url)
                .header("Authorization", &auth)
                .header("Content-Type", "application/json")
                .send(json),
            "PUT" => agent
                .put(&url)
                .header("Authorization", &auth)
                .header("Content-Type", "application/json")
                .send(json),
            m => panic!("{m}"),
        };
        let mut resp = resp.unwrap_or_else(|e| panic!("{method} {path}: {e}"));
        let location = resp
            .headers()
            .get("location")
            .and_then(|v| v.to_str().ok())
            .unwrap_or_default()
            .to_owned();
        let status = resp.status().as_u16();
        (
            status,
            resp.body_mut().read_to_string().unwrap_or_default(),
            location,
        )
    }

    fn admin_ok(
        &self,
        method: &str,
        path: &str,
        body: Option<serde_json::Value>,
    ) -> (String, String) {
        let (status, text, location) = self.admin(method, path, body);
        assert!(
            (200..300).contains(&status),
            "Keycloak admin {method} {path}: {status} {text}"
        );
        (text, location)
    }

    /// A fresh realm (a leftover of an earlier run is deleted first).
    fn realm(&self, name: &str) {
        let _ = self.admin("DELETE", &format!("/{name}"), None);
        self.admin_ok(
            "POST",
            "",
            Some(serde_json::json!({"realm": name, "enabled": true, "sslRequired": "external"})),
        );
    }

    /// A confidential OIDC client for gunnar's callback.
    fn oidc_client(&self, realm: &str, client_id: &str) {
        self.admin_ok(
            "POST",
            &format!("/{realm}/clients"),
            Some(serde_json::json!({
                "clientId": client_id, "secret": SECRET, "publicClient": false, "protocol": "openid-connect",
                "standardFlowEnabled": true, "directAccessGrantsEnabled": true,
                "redirectUris": [format!("{ORIGIN}/auth/sso/oidc/callback")],
            })),
        );
    }

    /// A person in the realm, with a password; `verified` is Keycloak's emailVerified. Returns
    /// Keycloak's id for them (the OIDC `sub`).
    fn user(&self, realm: &str, local: &str, first: &str, verified: bool) -> String {
        let (_, location) = self.admin_ok(
            "POST",
            &format!("/{realm}/users"),
            Some(serde_json::json!({
                "username": local, "email": format!("{local}@{DOMAIN}"), "firstName": first, "lastName": "Example",
                "enabled": true, "emailVerified": verified,
                "credentials": [{"type": "password", "value": PASSWORD, "temporary": false}],
            })),
        );
        location.rsplit('/').next().unwrap_or_default().to_owned()
    }

    fn client_uuid(&self, realm: &str, client_id: &str) -> String {
        let (text, _) = self.admin_ok(
            "GET",
            &format!("/{realm}/clients?clientId={}", pct(client_id)),
            None,
        );
        let v: serde_json::Value = serde_json::from_str(&text).unwrap();
        v[0]["id"]
            .as_str()
            .unwrap_or_else(|| panic!("no client {client_id}: {text}"))
            .to_owned()
    }

    /// A SAML client: `entity_id` (what the AuthnRequest's Issuer names) answering at gunnar's
    /// ACS, NameID = the e-mail, documents and assertions signed, requests unsigned.
    fn saml_client(&self, realm: &str, entity_id: &str) {
        self.admin_ok(
            "POST",
            &format!("/{realm}/clients"),
            Some(serde_json::json!({
                "clientId": entity_id, "protocol": "saml", "enabled": true, "frontchannelLogout": false,
                "redirectUris": [format!("{ORIGIN}/auth/sso/saml/acs")],
                "attributes": {
                    "saml.client.signature": "false",
                    "saml.server.signature": "true",
                    "saml.assertion.signature": "true",
                    "saml.signature.algorithm": "RSA_SHA256",
                    "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
                    "saml_name_id_format": "email",
                    "saml_force_name_id_format": "true",
                    "saml_assertion_consumer_url_post": format!("{ORIGIN}/auth/sso/saml/acs"),
                    "saml.authnstatement": "true",
                },
            })),
        );
    }

    fn saml_metadata(&self, realm: &str) -> String {
        let mut r = self
            .agent()
            .get(&format!(
                "{}/realms/{realm}/protocol/saml/descriptor",
                self.base
            ))
            .call()
            .expect("the SAML descriptor");
        assert_eq!(r.status().as_u16(), 200);
        r.body_mut().read_to_string().unwrap()
    }

    /// **Keycloak's own login form, driven as a browser drives it**, in a FRESH cookie jar:
    /// GET the start URL, POST the form `#kc-form-login` with the person's password. The answer:
    /// a redirect's Location (OIDC: back to gunnar with the code), or the page Keycloak drew
    /// (SAML: the auto-post form carrying the response).
    fn sign_in(&self, start: &str, user: &str) -> Answer {
        let agent = self.agent();
        let mut jar: BTreeMap<String, String> = BTreeMap::new();
        let mut first = agent
            .get(start)
            .call()
            .unwrap_or_else(|e| panic!("GET {start}: {e}"));
        keep_cookies(&mut jar, &first);
        let status = first.status().as_u16();
        let html = first.body_mut().read_to_string().unwrap_or_default();
        assert_eq!(
            status,
            200,
            "Keycloak did not draw its login form: {status} {}",
            cut(&html, 400)
        );
        let action = form_action(&html, "kc-form-login")
            .unwrap_or_else(|| panic!("no #kc-form-login: {}", cut(&html, 600)));
        let mut done = agent
            .post(&action)
            .header("Cookie", &cookie_header(&jar))
            .send_form([
                ("username", user),
                ("password", PASSWORD),
                ("credentialId", ""),
            ])
            .unwrap_or_else(|e| panic!("POST the login form: {e}"));
        let status = done.status().as_u16();
        let location = done
            .headers()
            .get("location")
            .and_then(|v| v.to_str().ok())
            .unwrap_or_default()
            .to_owned();
        let page = done.body_mut().read_to_string().unwrap_or_default();
        if (300..400).contains(&status) {
            Answer::Redirect(location)
        } else {
            Answer::Page(status, page)
        }
    }
}

enum Answer {
    Redirect(String),
    Page(u16, String),
}

fn keep_cookies(jar: &mut BTreeMap<String, String>, resp: &ureq::http::Response<ureq::Body>) {
    for v in resp.headers().get_all("set-cookie") {
        let pair = v
            .to_str()
            .unwrap_or_default()
            .split(';')
            .next()
            .unwrap_or_default();
        if let Some((n, val)) = pair.split_once('=') {
            jar.insert(n.trim().to_owned(), val.trim().to_owned());
        }
    }
}

fn cookie_header(jar: &BTreeMap<String, String>) -> String {
    jar.iter()
        .map(|(k, v)| format!("{k}={v}"))
        .collect::<Vec<_>>()
        .join("; ")
}

fn unescape(s: &str) -> String {
    s.replace("&amp;", "&")
        .replace("&quot;", "\"")
        .replace("&#x2F;", "/")
        .replace("&#x3D;", "=")
        .replace("&#43;", "+")
        .replace("&#x2B;", "+")
}

/// The `action` of the form whose id is `id`.
fn form_action(html: &str, id: &str) -> Option<String> {
    let at = html.find(&format!("id=\"{id}\""))?;
    let start = html[..at].rfind("<form")?;
    let tag = &html[start..start + html[start..].find('>')?];
    let a = tag.find("action=\"")? + 8;
    Some(unescape(&tag[a..a + tag[a..].find('"')?]))
}

/// The value of the hidden input named `name` (Keycloak's SAML auto-post page).
fn hidden(html: &str, name: &str) -> Option<String> {
    let at = html.find(&format!("name=\"{name}\""))?;
    let rest = &html[at..];
    let v = rest.find("value=\"")? + 7;
    Some(unescape(&rest[v..v + rest[v..].find('"')?]))
}

fn cut(s: &str, n: usize) -> String {
    s.chars().take(n).collect()
}

fn pct(s: &str) -> String {
    s.bytes()
        .map(|b| {
            if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) {
                (b as char).to_string()
            } else {
                format!("%{b:02X}")
            }
        })
        .collect()
}

fn unpct(v: &str) -> String {
    let b = v.as_bytes();
    let mut out = Vec::new();
    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()
}

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)| unpct(v))
        .unwrap_or_default()
}

// ── base64 (standard), for the SAML redirect binding ─────────────────────────

const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

fn b64(bytes: &[u8]) -> String {
    let mut out = String::new();
    for c in bytes.chunks(3) {
        let n = (u32::from(c[0]) << 16)
            | (u32::from(*c.get(1).unwrap_or(&0)) << 8)
            | u32::from(*c.get(2).unwrap_or(&0));
        for i in 0..4 {
            if i <= c.len() {
                out.push(B64[(n >> (18 - 6 * i) & 63) as usize] as char);
            } else {
                out.push('=');
            }
        }
    }
    out
}

fn unb64(s: &str) -> Vec<u8> {
    let mut out = Vec::new();
    let (mut acc, mut bits) = (0u32, 0);
    for ch in s.bytes().filter(|c| !c.is_ascii_whitespace() && *c != b'=') {
        let v = B64.iter().position(|b| *b == ch).expect("base64") as u32;
        acc = (acc << 6) | v;
        bits += 6;
        if bits >= 8 {
            bits -= 8;
            out.push((acc >> bits) as u8);
        }
    }
    out
}

/// **The same AuthnRequest, naming ANOTHER service provider as its issuer** (the same request
/// ID, so the answer is InResponseTo this flow): Keycloak answers it for that SP, so the
/// assertion's audience is not gunnar's.
fn reissued(redirect_url: &str, other_entity: &str, own_entity: &str) -> String {
    use std::io::Write as _;
    let raw = unb64(&query(redirect_url, "SAMLRequest"));
    let mut xml = String::new();
    flate2::read::DeflateDecoder::new(&raw[..])
        .read_to_string(&mut xml)
        .expect("a deflated AuthnRequest");
    assert!(
        xml.contains(own_entity),
        "gunnar's AuthnRequest names its own entity id: {xml}"
    );
    let xml = xml.replace(own_entity, other_entity);
    let mut z = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
    z.write_all(xml.as_bytes()).unwrap();
    let base = redirect_url.split_once('?').unwrap().0;
    format!(
        "{base}?SAMLRequest={}&RelayState={}",
        pct(&b64(&z.finish().unwrap())),
        pct(&query(redirect_url, "RelayState"))
    )
}

// ── the harness: the real plane and stores, gunnar's real egress ────────────

#[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<gunnar_grpc::identity::GunnarIdentity>,
    stores: Arc<ServerStores>,
    ctx: ServiceContext,
    ledger: Arc<Ledger>,
    console: Caller,
    idp: Idp,
    realm: String,
}

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

/// A call from `caller`'s session, stepped up a moment ago (the identity
/// core's fresh user-verified touch).
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(gunnar_grpc::auth::PersonSession {
        stepped_up: true,
        stepped_up_at_unix_ms: None,
    });
    r
}

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

/// 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,
    }
}

impl Harness {
    /// `realm`: this test's own realm at the IdP. The box's egress is gunnar's own; the
    /// operator lists the IdP's network (`GUNNAR_IDP_NETS`) unless `with_nets` is false.
    fn new(realm: &str, with_nets: bool) -> Harness {
        let idp = Idp::from_env();
        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));
        if with_nets {
            ctx.sso.set_private_nets(
                PrivateNets::parse(&idp.nets).expect("the IdP's network is a private one"),
            );
        }
        ctx.sso
            .set_egress(Arc::new(gunnar_server::sso_egress::UreqEgress::with_nets(
                false,
                gunnar_server::sso_egress::system_lookup(),
                ctx.sso.private_nets_handle(),
            )));
        // 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");
        }
        idp.realm(realm);
        Harness {
            _dir: dir,
            plane,
            identity,
            stores,
            ctx,
            ledger,
            console: Caller {
                principal_id: "gunnar-ui".into(),
                fingerprint: String::new(),
                machine: true,
                authenticated: true,
            },
            idp,
            realm: realm.to_owned(),
        }
    }

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

    fn client_id(&self) -> String {
        format!("gunnar-{}", self.realm)
    }

    /// acme's OIDC provider at this realm: the user-name claim is the e-mail, the domain is
    /// acme's, the IdP CA is the run's (or none).
    fn oidc_provider(
        &self,
        require_sso: bool,
        require_passkey: bool,
        with_ca: bool,
    ) -> SsoProvider {
        SsoProvider {
            account: "acme".into(),
            protocol: SsoProtocol::Oidc as i32,
            display_name: "Acme Keycloak".into(),
            domains: vec![DOMAIN.into()],
            oidc: Some(OidcSettings {
                issuer: self.idp.issuer(&self.realm),
                client_id: self.client_id(),
                username_claim: "email".into(),
                idp_ca_pem: if with_ca {
                    self.idp.ca_pem.clone()
                } else {
                    String::new()
                },
                ..Default::default()
            }),
            require_sso,
            require_passkey,
            console_origin: ORIGIN.into(),
            ..Default::default()
        }
    }

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

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

    /// One whole OIDC sign-in of `local` through Keycloak's login form: (Finish, session key).
    async fn oidc_login(&self, local: &str) -> (Result<FinishResponse, tonic::Status>, String) {
        let begun = self.begin().await;
        assert!(
            begun
                .redirect_url
                .starts_with(&self.idp.issuer(&self.realm)),
            "{}",
            begun.redirect_url
        );
        let back = match self.idp.sign_in(&begun.redirect_url, local) {
            Answer::Redirect(l) => l,
            Answer::Page(s, p) => {
                panic!("Keycloak did not send {local} back: {s} {}", cut(&p, 600))
            }
        };
        assert!(
            back.starts_with(&format!("{ORIGIN}/auth/sso/oidc/callback")),
            "{back}"
        );
        assert_eq!(query(&back, "state"), begun.state, "the state comes back");
        let finished = self
            .sso()
            .finish(authed(
                &self.console,
                FinishRequest {
                    state: begun.state.clone(),
                    code: query(&back, "code"),
                    console_origin: ORIGIN.into(),
                    ..Default::default()
                },
            ))
            .await
            .map(|r| r.into_inner());
        let session = finished
            .as_ref()
            .map(|f| f.session.clone())
            .unwrap_or_default();
        (finished, session)
    }

    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())
    }

    /// Through Keycloak's SAML login: (RelayState, SAMLResponse) from its auto-post page.
    fn saml_post(&self, redirect_url: &str, local: &str) -> (String, String) {
        match self.idp.sign_in(redirect_url, local) {
            Answer::Page(200, page) => (
                hidden(&page, "RelayState").unwrap_or_default(),
                hidden(&page, "SAMLResponse").unwrap_or_else(|| {
                    panic!("no SAMLResponse on Keycloak's page: {}", cut(&page, 600))
                }),
            ),
            Answer::Page(s, page) => panic!(
                "Keycloak answered the SAML sign-in {s}: {}",
                cut(&page, 600)
            ),
            Answer::Redirect(l) => panic!("a SAML sign-in redirected to {l}"),
        }
    }

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

    async fn directory(&self) -> Vec<DirectoryUser> {
        self.sso()
            .list_directory(authed(
                &person("acme"),
                ListDirectoryRequest {
                    account: "acme".into(),
                },
            ))
            .await
            .expect("acme's directory")
            .into_inner()
            .users
    }

    fn audit(&self) -> String {
        self.ledger.0.lock().unwrap().join("\n")
    }

    fn account_log(&self) -> Vec<String> {
        let log = self
            .stores
            .account_audit()
            .query(
                "acme",
                &gunnar_core::account_audit::AuditQuery {
                    limit: 200,
                    ..Default::default()
                },
            )
            .expect("acme's log");
        log.entries
            .iter()
            .map(|e| format!("{}:{}:{}:{}", e.kind, e.actor, e.target, e.detail))
            .collect()
    }

    async fn scim_token(&self) -> (String, String) {
        let made = self
            .sso()
            .create_scim_token(authed(
                &person("acme"),
                CreateScimTokenRequest {
                    account: "acme".into(),
                    label: "Keycloak test".into(),
                    ..Default::default()
                },
            ))
            .await
            .expect("a New SCIM token")
            .into_inner();
        (made.secret, made.token.expect("its row").token_id)
    }

    /// One SCIM call as a directory client sends it: (status, JSON, the sessions it ended).
    async fn scim_full(
        &self,
        bearer: &str,
        method: &str,
        path: &str,
        body: serde_json::Value,
    ) -> (u32, serde_json::Value, Vec<String>) {
        let answer = self
            .sso()
            .scim(authed(
                &self.console,
                ScimRequest {
                    bearer: bearer.into(),
                    method: method.into(),
                    path: path.into(),
                    query: String::new(),
                    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, answer.ended_sessions)
    }

    async fn scim(
        &self,
        bearer: &str,
        method: &str,
        path: &str,
        body: serde_json::Value,
    ) -> (u32, serde_json::Value) {
        let (s, j, _) = self.scim_full(bearer, method, path, body).await;
        (s, j)
    }

    /// `Sso.ImportUsers`, then `Sso.ImportStatus` until the job ended (A34-17).
    async fn import(&self, csv: &str, dry_run: bool) -> ImportUsersResponse {
        let mut job = self
            .sso()
            .import_users(authed(
                &person("acme"),
                ImportUsersRequest {
                    account: "acme".into(),
                    csv: csv.into(),
                    dry_run,
                },
            ))
            .await
            .expect("the CSV import answers")
            .into_inner();
        for _ in 0..500 {
            if job.state == "done" || job.state == "failed" {
                return job;
            }
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            job = self
                .sso()
                .import_status(authed(
                    &person("acme"),
                    ImportStatusRequest {
                        account: "acme".into(),
                        job_id: job.job_id.clone(),
                    },
                ))
                .await
                .expect("the import job answers")
                .into_inner();
        }
        panic!("the import job never ended: {job:?}");
    }
}

// ── passkeys (the same software authenticator as tests/sso.rs) ──────────────

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 assert(&self, challenge: &[u8], counter: u32) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
        let cd = Self::client_data("webauthn.get", challenge);
        let mut ad = sha2::Sha256::digest(RP_ID.as_bytes()).to_vec();
        ad.push(0x05);
        ad.extend_from_slice(&counter.to_be_bytes());
        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())
    }
}

// ── OIDC ───────────────────────────────────────────────────────────────────

/// ★ **OIDC through Keycloak, behind the account's own CA, on the operator's network.** The
/// account admin's Identity provider settings (issuer, client id and secret, the user-name
/// claim, the e-mail domain, the IdP CA PEM): without the CA the issuer is refused by name
/// (gunnar reads Keycloak's discovery at once), without the operator's network listed it is
/// refused by name too; with both, Keycloak's own login form signs bob in and he is a NEW
/// employee of acme, the session key his; the second sign-in is the same person. The client
/// secret is in no answer and no audit record; the change and both sign-ins are in acme's log.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn keycloak_oidc_signs_in_a_new_employee_behind_the_accounts_idp_ca() {
    let h = Harness::new("oidc", true);
    h.idp.oidc_client("oidc", &h.client_id());
    h.idp.user("oidc", "bob", "Bob", true);

    let no_ca = h
        .set(h.oidc_provider(false, false, false))
        .await
        .expect_err("an issuer behind a CA nobody trusts");
    assert_eq!(no_ca.code(), Code::FailedPrecondition, "{no_ca:?}");
    assert!(
        no_ca.message().contains("the issuer could not be read")
            && no_ca.message().to_lowercase().contains("certificate"),
        "refused by name: {}",
        no_ca.message()
    );

    let elsewhere = Harness::new("oidc-nets", false);
    elsewhere
        .idp
        .oidc_client("oidc-nets", &elsewhere.client_id());
    let no_net = elsewhere
        .set(elsewhere.oidc_provider(false, false, true))
        .await
        .expect_err("an IdP on a network the operator did not list");
    assert_eq!(no_net.code(), Code::FailedPrecondition, "{no_net:?}");
    assert!(
        no_net.message().contains("the issuer could not be read")
            && no_net.message().contains("private"),
        "refused by name: {}",
        no_net.message()
    );

    let view = h
        .set(h.oidc_provider(false, false, true))
        .await
        .expect("the account admin configures Keycloak");
    let shown = view.provider.clone().expect("provider");
    assert!(shown.has_client_secret);
    // A typed domain routes nothing until proven (A34-5): the rig proves it in the
    // table (tests/sso.rs reads a DNS proof).
    h.stores
        .auth()
        .verify_sso_domain("acme", DOMAIN)
        .expect("the rig proves acme's domain");
    let oidc = shown.oidc.clone().unwrap();
    assert!(
        oidc.idp_ca_pem.is_empty() && oidc.idp_ca_fingerprints.len() == 1,
        "the CA is shown by fingerprint only: {oidc:?}"
    );
    assert_eq!(
        view.redirect_uri,
        format!("{ORIGIN}/auth/sso/oidc/callback")
    );
    assert!(
        !format!("{view:?}").contains(SECRET),
        "the client secret came back"
    );

    let found = h
        .sso()
        .discover(authed(
            &h.console,
            DiscoverRequest {
                hint: format!("Bob@{}", DOMAIN.to_uppercase()),
            },
        ))
        .await
        .unwrap()
        .into_inner();
    assert!(
        found.found && found.account == "acme" && found.display_name == "Acme Keycloak",
        "{found:?}"
    );

    let (first, key) = h.oidc_login("bob").await;
    let first = first.expect("Keycloak's ID token verified by gunnar");
    assert!(first.created, "{first:?}");
    let bob = first.principal.as_ref().unwrap().id.clone();
    assert_eq!(bob, "acme.bob");
    assert_eq!(
        h.stores.auth().employer_of(&bob).unwrap().as_deref(),
        Some("acme")
    );
    assert_eq!(first.session, key);
    assert_eq!(
        h.gate_session(&key).expect("the session acts").principal_id,
        "acme.bob",
        "the session is the employee's"
    );

    let (second, _) = h.oidc_login("bob").await;
    let second = second.expect("the second sign-in");
    assert!(!second.created);
    assert_eq!(second.principal.unwrap().id, "acme.bob");
    assert_eq!(h.employees(), vec!["acme.bob".to_owned()]);
    let bob_row = h
        .directory()
        .await
        .into_iter()
        .find(|u| u.principal_id == "acme.bob")
        .expect("bob in the Directory");
    assert!(
        bob_row.bound && bob_row.active && bob_row.source == "jit",
        "{bob_row:?}"
    );
    assert_eq!(bob_row.email, format!("bob@{DOMAIN}"));

    let trail = h.audit();
    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"
    );
    let log = h.account_log();
    assert!(
        log.iter().any(|e| e.starts_with("sso.provider_set:acme:")),
        "{log:#?}"
    );
    assert_eq!(
        log.iter()
            .filter(|e| e.starts_with("login:acme.bob:sso"))
            .count(),
        2,
        "{log:#?}"
    );
    assert!(log.iter().all(|e| !e.contains(SECRET)), "{log:#?}");
}

/// ★ **"Require single sign-on" and "Also require a passkey after the IdP", with Keycloak.**
/// Required: an employee's passkey alone is refused by name, the account admin's passkey
/// still signs in (the break-glass). Then a passkey after the IdP: Keycloak's sign-in gives no
/// session until the first passkey is registered; the next sign-in asks for that passkey.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn keycloak_require_sso_keeps_the_admins_passkey_and_puts_a_passkey_after_the_idp() {
    let h = Harness::new("required", true);
    h.idp.oidc_client("required", &h.client_id());
    h.idp.user("required", "cleo", "Cleo", true);
    h.set(h.oidc_provider(true, false, true))
        .await
        .expect("require SSO");
    let (answer, _) = h.oidc_login("cleo").await;
    let cleo = answer.expect("cleo through Keycloak").principal.unwrap().id;
    let cleos = Authenticator::new(3, b"cleo-key");
    h.plane
        .add_credential(&cleo, &cleos.id, &cleos.public(), "cleo's key", 0)
        .unwrap();
    let admins = Authenticator::new(4, b"acme-admin-key");
    h.plane
        .add_credential("acme", &admins.id, &admins.public(), "acme's key", 0)
        .unwrap();
    let refused = h
        .passkey_login(&cleos, 1)
        .expect_err("an employee's passkey alone");
    assert_eq!(refused.code(), Code::PermissionDenied);
    assert!(
        refused.message().contains("Sign in with Acme Keycloak"),
        "{}",
        refused.message()
    );
    assert_eq!(
        h.passkey_login(&admins, 1)
            .expect("the admin keeps the passkey"),
        "acme"
    );

    h.set(h.oidc_provider(true, true, true))
        .await
        .expect("a passkey after the IdP");
    h.idp.user("required", "dora", "Dora", true);
    // GHUNT-8: the core keeps every session in one table since IDENTITY-HOST (cleo's and
    // the admin's sign-ins above are in it), so what is asserted is that dora's Keycloak
    // sign-in adds none before her first passkey.
    let before = h.sessions();
    let (first, key) = h.oidc_login("dora").await;
    let first = first.expect("dora through Keycloak");
    assert_eq!(
        first.second_factor,
        SecondFactorNeed::PasskeyRegistration as i32,
        "{first:?}"
    );
    assert!(key.is_empty(), "a session before the passkey");
    assert_eq!(h.sessions(), before, "a session before the passkey");
    let doras = Authenticator::new(5, b"dora-first-key");
    let made = h
        .redeem(
            &first.enrolment,
            &doras,
            gunnar_grpc::webauthn::attestation_none(RP_ID, &doras.id, &doras.public(), 0),
            doras.public(),
        )
        .expect("the first passkey registers through the core");
    h.second_factor(&first.pending, &made.token)
        .await
        .expect("the first passkey signs in");
    assert_eq!(
        h.gate_session(&made.token).expect("admitted").principal_id,
        made.session.user_id
    );
    let (next, _) = h.oidc_login("dora").await;
    let next = next.expect("dora again");
    assert_eq!(next.second_factor, SecondFactorNeed::Passkey as i32);
    assert_eq!(next.credential_ids, vec![doras.id.clone()]);
    let again = h.core_login(&doras, 1).expect("dora's passkey");
    h.second_factor(&next.pending, &again.token)
        .await
        .expect("dora's passkey after Keycloak");
    let trail = h.audit();
    assert!(
        trail
            .matches("Sso.SetProvider principal=acme scope=account:acme allow")
            .count()
            >= 2,
        "{trail}"
    );
}

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

impl Harness {
    fn sp_entity(&self) -> String {
        format!("{ORIGIN}/auth/sso/saml/acme")
    }

    /// acme's SAML provider from Keycloak's metadata (pasted: see the A34-41 test for the URL).
    async fn saml_configure(&self) -> ProviderView {
        let metadata = self.idp.saml_metadata(&self.realm);
        let view = self
            .set(SsoProvider {
                account: "acme".into(),
                protocol: SsoProtocol::Saml as i32,
                display_name: "Acme Keycloak SAML".into(),
                domains: vec![DOMAIN.into()],
                saml: Some(SamlSettings {
                    idp_metadata_xml: metadata,
                    ..Default::default()
                }),
                console_origin: ORIGIN.into(),
                ..Default::default()
            })
            .await
            .expect("Keycloak's SAML metadata is taken");
        let saml = view.provider.clone().unwrap().saml.unwrap();
        assert_eq!(saml.idp_entity_id, self.idp.issuer(&self.realm), "{saml:?}");
        assert!(
            saml.idp_sso_url.starts_with(&self.idp.issuer(&self.realm)),
            "{saml:?}"
        );
        assert!(
            !saml.idp_certificates.is_empty()
                && saml
                    .idp_certificates
                    .iter()
                    .all(|c| c.starts_with("SHA256:")),
            "{saml:?}"
        );
        assert_eq!(view.acs_url, format!("{ORIGIN}/auth/sso/saml/acs"));
        view
    }
}

/// ★ **SAML through Keycloak**: gunnar's AuthnRequest, Keycloak's login form, its signed
/// response posted back: a NEW employee. Then each refusal by name: the same response replayed;
/// an assertion for ANOTHER service provider (the same request reissued under another SP's
/// entity id, so only the audience differs); an assertion past its NotOnOrAfter (a one-second
/// lifespan on Keycloak's client, posted after gunnar's skew).
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn keycloak_saml_signs_in_and_refuses_a_replay_another_audience_and_an_expired_assertion() {
    let h = Harness::new("saml", true);
    let other = "https://other-sp.acme.test/saml";
    h.idp.saml_client("saml", &h.sp_entity());
    h.idp.saml_client("saml", other);
    h.idp.user("saml", "carol", "Carol", true);
    h.saml_configure().await;

    let begun = h.begin().await;
    assert_eq!(begun.protocol, SsoProtocol::Saml as i32);
    let (relay, response) = h.saml_post(&begun.redirect_url, "carol");
    assert_eq!(relay, begun.state, "the RelayState comes back");
    let ok = h
        .finish_saml(&relay, &response)
        .await
        .expect("Keycloak's signed response");
    assert!(ok.created, "{ok:?}");
    assert_eq!(ok.principal.unwrap().id, "acme.carol");

    let again = h.begin().await;
    let replay = h
        .finish_saml(&again.state, &response)
        .await
        .expect_err("a replayed response");
    assert_eq!(replay.code(), Code::PermissionDenied, "{replay:?}");

    let begun = h.begin().await;
    let (relay, response) = h.saml_post(
        &reissued(&begun.redirect_url, other, &h.sp_entity()),
        "carol",
    );
    let wrong = h
        .finish_saml(&relay, &response)
        .await
        .expect_err("an assertion for another SP");
    assert_eq!(wrong.code(), Code::PermissionDenied, "{wrong:?}");
    assert!(
        wrong.message().contains("udience"),
        "refused by name: {}",
        wrong.message()
    );

    // A one-second assertion, posted after the skew gunnar allows.
    let uuid = h.idp.client_uuid("saml", &h.sp_entity());
    let (text, _) = h
        .idp
        .admin_ok("GET", &format!("/saml/clients/{uuid}"), None);
    let mut client: serde_json::Value = serde_json::from_str(&text).unwrap();
    client["attributes"]["saml.assertion.lifespan"] = serde_json::json!("1");
    h.idp
        .admin_ok("PUT", &format!("/saml/clients/{uuid}"), Some(client));
    let begun = h.begin().await;
    let (relay, response) = h.saml_post(&begun.redirect_url, "carol");
    tokio::time::sleep(Duration::from_secs(gunnar_grpc::sso::SKEW_SECS as u64 + 5)).await;
    let expired = h
        .finish_saml(&relay, &response)
        .await
        .expect_err("an expired assertion");
    assert_eq!(expired.code(), Code::PermissionDenied, "{expired:?}");
    let words = expired.message().to_lowercase();
    assert!(
        words.contains("expired") || words.contains("notonorafter") || words.contains("no longer"),
        "refused by name: {}",
        expired.message()
    );
    assert_eq!(
        h.employees(),
        vec!["acme.carol".to_owned()],
        "a refused response made someone"
    );
    let trail = h.audit();
    assert!(
        trail.contains("Sso.Finish principal=acme.carol scope=account:acme allow"),
        "{trail}"
    );
    assert!(
        trail
            .lines()
            .filter(|l| l.starts_with("Sso.Finish") && l.contains(" deny"))
            .count()
            >= 3,
        "each refusal is audited: {trail}"
    );
}

/// ★ **A34-41: the SAML metadata by its https URL, behind the account's IdP CA.** An OIDC
/// provider's calls trust the account's own CA; the SAML metadata fetch does not, so a company
/// IdP behind its own CA could only be configured by pasting the XML (it was RED: "the IdP's
/// metadata: io: invalid peer certificate: UnknownIssuer"). The account's CA is the one CA field,
/// `OidcSettings.idp_ca_pem`, for a SAML provider too.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn keycloak_saml_metadata_by_url_is_read_behind_the_accounts_idp_ca() {
    let h = Harness::new("saml-url", true);
    h.idp.saml_client("saml-url", &h.sp_entity());
    let url = format!("{}/protocol/saml/descriptor", h.idp.issuer("saml-url"));
    let view = h
        .set(SsoProvider {
            account: "acme".into(),
            protocol: SsoProtocol::Saml as i32,
            display_name: "Acme Keycloak SAML".into(),
            saml: Some(SamlSettings {
                idp_metadata_url: url,
                ..Default::default()
            }),
            oidc: Some(OidcSettings {
                idp_ca_pem: h.idp.ca_pem.clone(),
                ..Default::default()
            }),
            console_origin: ORIGIN.into(),
            ..Default::default()
        })
        .await
        .expect("the metadata URL is read trusting acme's IdP CA");
    assert_eq!(
        view.provider.unwrap().saml.unwrap().idp_entity_id,
        h.idp.issuer("saml-url")
    );
}

// ── SCIM and the CSV import, claimed by a Keycloak sign-in ──────────────────

/// ★ **SCIM 2.0, then the person signs in through Keycloak.** A New SCIM token; a directory
/// client (Entra's and Okta's shapes) creates erik and a group, updates and deactivates him;
/// the Directory shows him (`scim`, not bound); his Keycloak sign-in CLAIMS that employee (no
/// new one); deactivated, his Keycloak sign-in is refused by name and his session ended;
/// reactivated, he signs in again; the group loses him; a revoked token is refused (401).
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn scim_provisions_employees_and_groups_that_keycloak_sign_ins_claim() {
    let h = Harness::new("scim", true);
    h.idp.oidc_client("scim", &h.client_id());
    let kc_id = h.idp.user("scim", "erik", "Erik", true);
    h.set(h.oidc_provider(false, false, true))
        .await
        .expect("Keycloak configured");
    let (bearer, token_id) = h.scim_token().await;
    let email = format!("erik@{DOMAIN}");

    let (status, made) = h
        .scim(&bearer, "POST", "/Users", serde_json::json!({
            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
            "userName": email, "externalId": kc_id, "name": {"givenName": "Erik", "familyName": "Example"},
            "emails": [{"value": email, "primary": true, "type": "work"}], "active": true,
        }))
        .await;
    assert_eq!(status, 201, "{made}");
    let id = made["id"].as_str().unwrap().to_owned();
    assert_eq!(id, "acme.erik");
    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": "displayName", "value": "Erik E."}],
            }),
        )
        .await;
    assert_eq!(status, 200, "{patched}");
    let (status, group) = h.scim(&bearer, "POST", "/Groups", serde_json::json!({"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "Engineering", "members": [{"value": id}]})).await;
    assert_eq!(status, 201, "{group}");
    let gid = group["id"].as_str().unwrap().to_owned();
    let row = h
        .directory()
        .await
        .into_iter()
        .find(|u| u.principal_id == id)
        .expect("erik in the Directory");
    assert!(
        row.source == "scim" && !row.bound && row.active && row.display_name == "Erik E.",
        "{row:?}"
    );

    let (login, _) = h.oidc_login("erik").await;
    let login = login.expect("erik signs in through Keycloak");
    assert!(!login.created, "a SCIM employee was made twice: {login:?}");
    assert_eq!(login.principal.as_ref().unwrap().id, id);
    assert!(
        h.directory()
            .await
            .into_iter()
            .find(|u| u.principal_id == id)
            .unwrap()
            .bound,
        "the sign-in bound him"
    );

    let off = serde_json::json!({"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{"op": "Replace", "path": "active", "value": "False"}]});
    let (status, _, ended) = h
        .scim_full(&bearer, "PATCH", &format!("/Users/{id}"), off)
        .await;
    assert_eq!(status, 200);
    assert_eq!(
        ended,
        vec![id.clone()],
        "the deactivation names the sessions it ended"
    );
    assert!(
        h.gate_session(&login.session).is_err(),
        "the session survived the deactivation"
    );
    let (refused, _) = h.oidc_login("erik").await;
    let refused = refused.expect_err("a deactivated person through Keycloak");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("deactivated"),
        "refused by name: {}",
        refused.message()
    );

    let (status, _) = h
        .scim(
            &bearer,
            "PATCH",
            &format!("/Users/{id}"),
            serde_json::json!({"Operations": [{"op": "replace", "value": {"active": true}}]}),
        )
        .await;
    assert_eq!(status, 200);
    // RA5-31: a sign-in in the very second of the deactivation's "signed out everywhere" is
    // refused as busy (whole-second floor), and the console signs in again a moment later.
    let mut again = h.oidc_login("erik").await.0;
    if matches!(&again, Err(s) if s.code() == Code::ResourceExhausted && s.message().contains("this very second"))
    {
        tokio::time::sleep(Duration::from_millis(1100)).await;
        again = h.oidc_login("erik").await.0;
    }
    assert!(again.is_ok(), "reactivated, erik signs in: {again:?}");
    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().map(Vec::len).unwrap_or(0), 0, "{g}");

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

/// ★ **The CSV import, then Keycloak sign-ins.** Check (a dry run) names the bad row and
/// writes nothing; Import makes the people; fay's Keycloak sign-in claims her imported
/// employee; a second import with active=false deactivates gus, whose Keycloak sign-in is then
/// refused by name; the Directory shows the source.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn the_csv_import_provisions_employees_that_keycloak_sign_ins_claim() {
    let h = Harness::new("csv", true);
    h.idp.oidc_client("csv", &h.client_id());
    h.idp.user("csv", "fay", "Fay", true);
    h.idp.user("csv", "gus", "Gus", true);
    h.set(h.oidc_provider(false, false, true))
        .await
        .expect("Keycloak configured");
    // GHUNT-8: an e-mail claims a provisioned employee only on a PROVEN domain (A34-5,
    // `sso-claim-domain`); the rig proves acme's after the provider is set, as the
    // domain-routing test does.
    h.stores
        .auth()
        .verify_sso_domain("acme", DOMAIN)
        .expect("the rig proves acme's domain");
    let csv = format!("user_name,display_name,email,active\nfay@{DOMAIN},Fay,fay@{DOMAIN},true\ngus@{DOMAIN},Gus,gus@{DOMAIN},true\n,Nobody,,true\n");
    let check = h.import(&csv, true).await;
    assert!(check.dry_run);
    assert_eq!((check.created, check.errors.len()), (2, 1), "{check:?}");
    assert!(
        check.errors[0].contains('4') || check.errors[0].contains("Nobody"),
        "the bad row is named: {:?}",
        check.errors
    );
    assert!(h.employees().is_empty(), "Check wrote");
    let done = h.import(&csv, false).await;
    assert_eq!((done.created, done.errors.len()), (2, 1), "{done:?}");
    assert_eq!(
        h.employees(),
        vec!["acme.fay".to_owned(), "acme.gus".to_owned()]
    );

    let (fay, _) = h.oidc_login("fay").await;
    let fay = fay.expect("fay through Keycloak");
    assert!(!fay.created, "{fay:?}");
    assert_eq!(fay.principal.unwrap().id, "acme.fay");

    let off = h
        .import(&format!("user_name,active\ngus@{DOMAIN},false\n"), false)
        .await;
    assert_eq!(off.deactivated, 1, "{off:?}");
    let (gus, _) = h.oidc_login("gus").await;
    let gus = gus.expect_err("gus deactivated by the import");
    assert_eq!(gus.code(), Code::PermissionDenied);
    assert!(
        gus.message().contains("deactivated"),
        "refused by name: {}",
        gus.message()
    );
    let dir = h.directory().await;
    let row = |id: &str| dir.iter().find(|u| u.principal_id == id).cloned().unwrap();
    assert!(
        row("acme.fay").source == "csv" && row("acme.fay").bound && row("acme.fay").active,
        "{:?}",
        row("acme.fay")
    );
    assert!(
        row("acme.gus").source == "csv" && !row("acme.gus").active,
        "{:?}",
        row("acme.gus")
    );
    let trail = h.audit();
    assert!(
        trail.contains("Sso.ImportUsers principal=acme scope=account:acme allow"),
        "{trail}"
    );
}

/// ★ **A34-2: an unverified e-mail never claims a provisioned employee.** SCIM provisions
/// hana; a Keycloak person whose e-mail is hana's but NOT verified (`email_verified=false`:
/// self-service sign-up, an edited profile) signs in: gunnar must refuse by name and attach
/// nothing. RED before A34-2 (the first matching user name claimed the employee).
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn an_unverified_keycloak_email_never_claims_a_provisioned_employee() {
    let h = Harness::new("unverified", true);
    h.idp.oidc_client("unverified", &h.client_id());
    h.idp.user("unverified", "hana", "Hana", false);
    h.set(h.oidc_provider(false, false, true))
        .await
        .expect("Keycloak configured");
    let (bearer, _) = h.scim_token().await;
    let email = format!("hana@{DOMAIN}");
    let (status, made) = h.scim(&bearer, "POST", "/Users", serde_json::json!({"userName": email, "emails": [{"value": email, "primary": true}], "active": true})).await;
    assert_eq!(status, 201, "{made}");
    let (claim, _) = h.oidc_login("hana").await;
    let refused = claim.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()
    );
    let row = h
        .directory()
        .await
        .into_iter()
        .find(|u| u.principal_id == "acme.hana")
        .expect("hana in the Directory");
    assert!(!row.bound, "the employee was attached: {row:?}");
}

// ── through the real router and gate (OX-WIRED4) ────────────────────────────
//
// Every test above calls `SsoService` with a `Caller` put in by hand, so the gate in front of
// a box (the console's signed key, an admin's passkey session, an anonymous call) is never
// asked. `tests/sso_signin_rpc.rs` drives the router against edda's fakes; this drives it
// against Keycloak: the `Sso` service a box serves, behind its gate, with gunnar's real
// egress behind the account's IdP CA on the operator's network.

fn free_addr() -> std::net::SocketAddr {
    let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
    l.local_addr().expect("addr")
}

/// A call carrying a person's identity-core session, as the console sends it.
fn with_session<T>(token: &str, body: T) -> Request<T> {
    let mut r = Request::new(body);
    r.metadata_mut()
        .insert(SESSION_METADATA_KEY, token.parse().expect("ascii"));
    r
}

/// A user-verified passkey login through the identity core: the session token.
fn passkey_session(identity: &gunnar_grpc::identity::GunnarIdentity, by: &Authenticator) -> String {
    let c = identity.login_start(&relay()).expect("login start");
    let challenge = gunnar_grpc::webauthn::from_b64url(&c.challenge).expect("b64url");
    let (cd, ad, sig) = by.assert(&challenge, 1);
    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(),
        })
        .expect("the passkey logs in")
        .token
}

/// **One box serving the real router**: its gate in front of the real `CoreControlPlane`
/// on a real store; the console is the box's first admin by its signed key; acme's and
/// globex's admins hold stepped-up passkey sessions. The egress is the Harness's.
struct Served {
    _dir: tempfile::TempDir,
    stores: Arc<ServerStores>,
    plane: Arc<CoreControlPlane>,
    ledger: Arc<Ledger>,
    idp: Idp,
    realm: String,
    at: String,
    console: gunnar_core::ssh_key::PrivateKey,
    acme: String,
    globex: String,
    stop: Option<tokio::sync::oneshot::Sender<()>>,
}

impl Drop for Served {
    fn drop(&mut self) {
        if let Some(stop) = self.stop.take() {
            let _ = stop.send(());
        }
    }
}

impl Served {
    async fn start(realm: &str) -> Served {
        let idp = Idp::from_env();
        let dir = tempfile::tempdir().expect("tempdir");
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        let pem = gunnar_core::mint_ed25519().expect("mint");
        let console =
            gunnar_core::ssh_key::PrivateKey::from_openssh(std::str::from_utf8(&pem).unwrap())
                .expect("parse");
        let line = console.public_key().to_openssh().expect("line");
        stores
            .auth()
            .bootstrap_admin(&line)
            .expect("the console is the box's first admin");
        let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
        let control: Arc<dyn ControlPlane> = plane.clone();
        let acme_key = Authenticator::new(6, b"acme-key");
        let globex_key = Authenticator::new(7, b"globex-key");
        for (account, key) in [("acme", &acme_key), ("globex", &globex_key)] {
            plane
                .add_principal(account, account, PrincipalKind::User)
                .expect("account");
            plane
                .add_credential(account, &key.id, &key.public(), account, 0)
                .expect("the admin's passkey");
        }
        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_private_nets(
            PrivateNets::parse(&idp.nets).expect("the IdP's network is a private one"),
        );
        ctx.sso
            .set_egress(Arc::new(gunnar_server::sso_egress::UreqEgress::with_nets(
                false,
                gunnar_server::sso_egress::system_lookup(),
                ctx.sso.private_nets_handle(),
            )));
        ctx.sso.set_egress_on(true);
        idp.realm(realm);
        let addr = free_addr();
        let (tx, rx) = tokio::sync::oneshot::channel::<()>();
        tokio::spawn(async move {
            let _ = gunnar_grpc::router(ctx, || 0)
                .serve_with_shutdown(addr, async {
                    let _ = rx.await;
                })
                .await;
        });
        let t0 = std::time::Instant::now();
        while std::net::TcpStream::connect(addr).is_err() {
            assert!(t0.elapsed() < Duration::from_secs(10), "the box listens");
            tokio::time::sleep(Duration::from_millis(25)).await;
        }
        let acme = passkey_session(&identity, &acme_key);
        let globex = passkey_session(&identity, &globex_key);
        Served {
            _dir: dir,
            stores,
            plane,
            ledger,
            idp,
            realm: realm.to_owned(),
            at: addr.to_string(),
            console,
            acme,
            globex,
            stop: Some(tx),
        }
    }

    fn sso(
        &self,
    ) -> gunnar_proto::sso_client::SsoClient<gunnar_grpc::client_signer::ControlChannel> {
        gunnar_proto::sso_client::SsoClient::new(
            gunnar_grpc::client_signer::plain_channel(&self.at).expect("a channel"),
        )
    }

    /// `Access.WhoAmI` through the router: who the gate takes `session` for.
    async fn who(&self, session: &str) -> Result<String, tonic::Status> {
        gunnar_proto::access_client::AccessClient::new(
            gunnar_grpc::client_signer::plain_channel(&self.at).expect("a channel"),
        )
        .who_am_i(with_session(session, WhoAmIRequest {}))
        .await
        .map(|r| r.into_inner().principal.map(|p| p.id).unwrap_or_default())
    }

    fn signed<T>(&self, body: T) -> Request<T> {
        gunnar_grpc::client_signer::authed(&self.console, body)
    }

    async fn set(
        &self,
        session: &str,
        provider: SsoProvider,
    ) -> Result<ProviderView, tonic::Status> {
        self.sso()
            .set_provider(with_session(
                session,
                SetProviderRequest {
                    provider: Some(provider),
                    client_secret: SECRET.into(),
                    clear_client_secret: false,
                    clear_idp_ca: false,
                },
            ))
            .await
            .map(|r| r.into_inner().view.expect("a view"))
    }

    async fn begin(&self, account: &str) -> BeginResponse {
        self.sso()
            .begin(self.signed(BeginRequest {
                account: account.into(),
                login_hint: String::new(),
                console_origin: ORIGIN.into(),
                client: String::new(),
            }))
            .await
            .expect("Sso.Begin through the gate")
            .into_inner()
    }

    async fn finish(&self, request: FinishRequest) -> Result<FinishResponse, tonic::Status> {
        self.sso()
            .finish(self.signed(request))
            .await
            .map(|r| r.into_inner())
    }

    /// One OIDC sign-in of `local` through Keycloak's login form, relayed by the console.
    async fn oidc_login(&self, local: &str) -> Result<FinishResponse, tonic::Status> {
        let begun = self.begin("acme").await;
        assert_eq!(begun.protocol, SsoProtocol::Oidc as i32);
        assert!(
            begun
                .redirect_url
                .starts_with(&self.idp.issuer(&self.realm)),
            "{}",
            begun.redirect_url
        );
        let back = match self.idp.sign_in(&begun.redirect_url, local) {
            Answer::Redirect(l) => l,
            Answer::Page(s, p) => {
                panic!("Keycloak did not send {local} back: {s} {}", cut(&p, 600))
            }
        };
        assert!(
            back.starts_with(&format!("{ORIGIN}/auth/sso/oidc/callback")),
            "{back}"
        );
        assert_eq!(query(&back, "state"), begun.state, "the state comes back");
        self.finish(FinishRequest {
            state: begun.state,
            code: query(&back, "code"),
            console_origin: ORIGIN.into(),
            ..Default::default()
        })
        .await
    }

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

    fn audit(&self) -> String {
        self.ledger.0.lock().unwrap().join("\n")
    }
}

/// ★ **Keycloak sign-ins through the real router and gate** (OX-WIRED4). OIDC: only acme's
/// admin, by a stepped-up passkey session, sets acme's provider (globex's admin and an
/// anonymous call are refused); the console (the box's first admin, its signed key)
/// discovers, begins and finishes; Keycloak's own login form signs bob in, he is a NEW
/// employee of acme just in time, and the session the finish hands is a credential the gate
/// takes as acme.bob (`Access.WhoAmI`); the second sign-in is the same person; the account
/// admin's session is no relay. SAML: globex's admin sets Keycloak's metadata by URL behind
/// the IdP CA; carol signs in through Keycloak's form, her session is a credential, a
/// replayed response is refused. The client secret is in no answer and no audit record.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs a real Keycloak: cargo xtask idp"]
async fn keycloak_sign_ins_go_through_the_real_router_and_gate() {
    let b = Served::start("router").await;
    let client_id = "gunnar-router".to_owned();
    b.idp.oidc_client("router", &client_id);
    b.idp.user("router", "bob", "Bob", true);
    b.idp.user("router", "carol", "Carol", true);
    let oidc = SsoProvider {
        account: "acme".into(),
        protocol: SsoProtocol::Oidc as i32,
        display_name: "Acme Keycloak".into(),
        domains: vec![DOMAIN.into()],
        oidc: Some(OidcSettings {
            issuer: b.idp.issuer("router"),
            client_id,
            username_claim: "email".into(),
            idp_ca_pem: b.idp.ca_pem.clone(),
            ..Default::default()
        }),
        console_origin: ORIGIN.into(),
        ..Default::default()
    };

    // Only acme's admin sets acme's provider.
    let outsider = b.set(&b.globex, oidc.clone()).await.unwrap_err();
    assert_eq!(outsider.code(), Code::PermissionDenied, "{outsider:?}");
    let anon = b
        .sso()
        .set_provider(Request::new(SetProviderRequest {
            provider: Some(oidc.clone()),
            client_secret: SECRET.into(),
            ..Default::default()
        }))
        .await
        .unwrap_err();
    assert_eq!(anon.code(), Code::Unauthenticated, "{anon:?}");
    let view = b
        .set(&b.acme, oidc)
        .await
        .expect("Sso.SetProvider through the gate reads Keycloak behind acme's CA");
    assert!(view.provider.as_ref().expect("provider").has_client_secret);
    assert!(
        !format!("{view:?}").contains(SECRET),
        "the client secret came back"
    );
    // The domain proof through the router is `tests/sso_signin_rpc.rs`'s (a DNS answer);
    // here the rig proves it in the table, as the Harness does.
    b.stores
        .auth()
        .verify_sso_domain("acme", DOMAIN)
        .expect("the rig proves acme's domain");

    let found = b
        .sso()
        .discover(b.signed(DiscoverRequest {
            hint: format!("Bob@{}", DOMAIN.to_uppercase()),
        }))
        .await
        .expect("Sso.Discover through the gate")
        .into_inner();
    assert!(
        found.found && found.account == "acme" && found.display_name == "Acme Keycloak",
        "{found:?}"
    );

    // The relay's door is the console's: the account admin's session is no relay.
    let not_relay = b
        .sso()
        .begin(with_session(
            &b.acme,
            BeginRequest {
                account: "acme".into(),
                console_origin: ORIGIN.into(),
                ..Default::default()
            },
        ))
        .await
        .unwrap_err();
    assert_eq!(not_relay.code(), Code::PermissionDenied, "{not_relay:?}");

    let first = b
        .oidc_login("bob")
        .await
        .expect("Keycloak's ID token, verified through the router");
    assert!(first.created, "{first:?}");
    let bob = first.principal.as_ref().expect("principal").id.clone();
    assert_eq!(bob, "acme.bob");
    assert_eq!(first.account, "acme");
    assert_eq!(
        b.stores.auth().employer_of(&bob).unwrap().as_deref(),
        Some("acme")
    );
    assert_eq!(
        b.who(&first.session)
            .await
            .expect("the session is a credential at the gate"),
        "acme.bob"
    );
    assert_eq!(
        b.who("not-a-session").await.unwrap_err().code(),
        Code::Unauthenticated
    );
    let second = b.oidc_login("bob").await.expect("the second sign-in");
    assert!(!second.created);
    assert_eq!(second.principal.unwrap().id, bob);
    assert_eq!(b.employees("acme"), vec![bob.clone()]);

    // SAML: globex's provider is Keycloak's SAML client in the same realm.
    let entity = format!("{ORIGIN}/auth/sso/saml/globex");
    b.idp.saml_client("router", &entity);
    let saml = SsoProvider {
        account: "globex".into(),
        protocol: SsoProtocol::Saml as i32,
        display_name: "Globex Keycloak SAML".into(),
        saml: Some(SamlSettings {
            idp_metadata_url: format!("{}/protocol/saml/descriptor", b.idp.issuer("router")),
            ..Default::default()
        }),
        oidc: Some(OidcSettings {
            idp_ca_pem: b.idp.ca_pem.clone(),
            ..Default::default()
        }),
        console_origin: ORIGIN.into(),
        ..Default::default()
    };
    let outsider = b.set(&b.acme, saml.clone()).await.unwrap_err();
    assert_eq!(
        outsider.code(),
        Code::PermissionDenied,
        "acme's admin set globex's provider: {outsider:?}"
    );
    let view = b
        .set(&b.globex, saml)
        .await
        .expect("the metadata by URL, through the gate");
    assert_eq!(
        view.provider.unwrap().saml.unwrap().idp_entity_id,
        b.idp.issuer("router")
    );
    let begun = b.begin("globex").await;
    assert_eq!(begun.protocol, SsoProtocol::Saml as i32);
    let (relay_state, response) = match b.idp.sign_in(&begun.redirect_url, "carol") {
        Answer::Page(200, page) => (
            hidden(&page, "RelayState").unwrap_or_default(),
            hidden(&page, "SAMLResponse").unwrap_or_else(|| {
                panic!("no SAMLResponse on Keycloak's page: {}", cut(&page, 600))
            }),
        ),
        Answer::Page(s, page) => panic!(
            "Keycloak answered the SAML sign-in {s}: {}",
            cut(&page, 600)
        ),
        Answer::Redirect(l) => panic!("a SAML sign-in redirected to {l}"),
    };
    assert_eq!(relay_state, begun.state, "the RelayState comes back");
    let saml_finish = |state: &str| FinishRequest {
        state: state.into(),
        saml_response: response.clone(),
        console_origin: ORIGIN.into(),
        ..Default::default()
    };
    let not_relay = b
        .sso()
        .finish(with_session(&b.globex, saml_finish(&relay_state)))
        .await
        .unwrap_err();
    assert_eq!(not_relay.code(), Code::PermissionDenied, "{not_relay:?}");
    let ok = b
        .finish(saml_finish(&relay_state))
        .await
        .expect("Keycloak's signed response, through the router");
    assert!(ok.created, "{ok:?}");
    let carol = ok.principal.expect("principal").id;
    assert_eq!(ok.account, "globex");
    assert_eq!(
        b.who(&ok.session)
            .await
            .expect("the session is a credential at the gate"),
        carol
    );
    let again = b.begin("globex").await;
    let replay = b
        .finish(saml_finish(&again.state))
        .await
        .expect_err("a replayed response");
    assert_eq!(replay.code(), Code::PermissionDenied, "{replay:?}");
    assert_eq!(b.employees("globex"), vec![carol.clone()]);

    let trail = b.audit();
    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}"
    );
    assert!(
        trail.contains(&format!(
            "Sso.Finish principal={carol} scope=account:globex allow"
        )),
        "{trail}"
    );
    assert!(
        !trail.contains(SECRET),
        "the client secret reached the audit trail"
    );
}

// ── the run's certificate (a step of `cargo xtask idp`, not a test of gunnar) ─

/// **The run's CA and Keycloak's certificate**, written to `GUNNAR_IDP_MINT_DIR` (`ca.pem`,
/// `tls/kc.crt`, `tls/kc.key`) before Keycloak starts: the xtask is dependency-free, and rcgen
/// is already this crate's dev-dependency. The key is a throwaway, for one run's IdP.
#[test]
#[ignore = "a step of cargo xtask idp: mints the run's IdP certificate"]
fn mint_the_runs_idp_certificate() {
    let dir = std::path::PathBuf::from(env("GUNNAR_IDP_MINT_DIR"));
    let ca_key = rcgen::KeyPair::generate().expect("a key");
    let mut ca = rcgen::CertificateParams::new(Vec::<String>::new()).expect("params");
    ca.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
    ca.key_usages = vec![
        rcgen::KeyUsagePurpose::KeyCertSign,
        rcgen::KeyUsagePurpose::CrlSign,
    ];
    ca.distinguished_name.push(
        rcgen::DnType::CommonName,
        "acme's own CA (a cargo xtask idp run)",
    );
    let ca_cert = ca.self_signed(&ca_key).expect("the CA");
    let issuer = rcgen::Issuer::new(ca, ca_key);
    let leaf_key = rcgen::KeyPair::generate().expect("a key");
    let leaf = rcgen::CertificateParams::new(vec!["idp.acme.test".to_owned()]).expect("params");
    let leaf_cert = leaf.signed_by(&leaf_key, &issuer).expect("the leaf");
    std::fs::create_dir_all(dir.join("tls")).unwrap();
    std::fs::write(dir.join("ca.pem"), ca_cert.pem()).unwrap();
    std::fs::write(
        dir.join("tls/kc.crt"),
        format!("{}{}", leaf_cert.pem(), ca_cert.pem()),
    )
    .unwrap();
    std::fs::write(dir.join("tls/kc.key"), leaf_key.serialize_pem()).unwrap();
}