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

nordisk/gunnar-ui

crates/gunnar-ui-server/src/tenant.rs

//! **The credential a logged-in person's READS are answered with — theirs, not
//! this console's and not the grantless guest's.**
//!
//! # The hole this closes, and the one it does not reopen
//!
//! `api::Reader` used to hand `App::gunnar` — the console's server-wide Admin
//! key — to every request that carried a session, whatever its role. A viewer
//! is the tenant role: the lowest rung, minted by payment, and it read every
//! private repository on the box, because gunnar was answering the credential
//! and the credential was the console's (GUI-002).
//!
//! Narrowing a viewer to `App::guest` stopped that, and took something with it
//! that the console is paid to provide: a tenant's own private repositories.
//! The guest principal holds no grants, so a paying customer signed in and saw
//! the public subset — the same thing a stranger sees.
//!
//! Neither credential is the right one, because neither one is *theirs*. So
//! this module mints a third kind: one ed25519 key per console user, enrolled
//! on that person's own gunnar principal. Their reads are signed with it,
//! gunnar applies **their** grants, and the answer is narrowed on gunnar's side
//! of the wire — where a mistake in this crate cannot reach it. The console's
//! admin key goes back to what it is for: administration.
//!
//! # Why this is [`crate::reviewer`]'s shape and not [`crate::reviewer`]
//!
//! The two look alike and answer different questions, and merging them would
//! lose the difference:
//!
//! * a REVIEWER key exists so a forge approval **names a human**. It is
//!   enrolled on a principal this console creates for the purpose
//!   (`console-<user>`) and is granted server-wide Write, because signing a
//!   review is a write a console operator could already make.
//! * a TENANT key exists so a read is **narrowed to what that person may see**.
//!   It is enrolled on the principal that person already is, and is granted
//!   **nothing at all** — the grants that principal holds are the whole point,
//!   and a grant handed out here would be the console deciding what a tenant
//!   may read, which is the confused deputy again with extra steps.
//!
//! They share the minting and the sealing, and that much IS shared:
//! [`crate::reviewer::mint_ed25519`] and the same [`StateStore`] the session
//! key lives in, at mode 0600 (see [`crate::state`]). No second crypto scheme
//! and no second file.
//!
//! # What custody means here, stated rather than hidden
//!
//! The console holds the private half. A compromised console can therefore read
//! as any tenant — but a compromised console already holds a server-wide Admin
//! key, so this adds no reach it did not have. What it removes is the reach an
//! **uncompromised** console hands out by accident, which is what every finding
//! in this file's history has been about.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tokio::sync::Mutex;

use crate::grpc::Gunnar;
use crate::pb;
use crate::reviewer::mint_ed25519;
use crate::signer::Signer;
use crate::state::{StateError, StateStore};

/// A console user's gunnar identity: the principal their reads are answered as,
/// and a client that signs with the key enrolled on it.
pub struct TenantIdentity {
    /// The identity-table principal this session acts for.
    pub principal_id: String,
    /// The `SHA256:…` fingerprint of the key this console enrolled for them.
    ///
    /// Read by [`crate::api::account`], which must not offer a person a button
    /// that revokes the credential their own console session is reading with.
    pub fingerprint: String,
    /// A client that acts AS the tenant on the control plane.
    pub gunnar: Gunnar,
    /// **The key itself**, for the one call that needs a signature rather than
    /// a connection: `Access.AddKey`'s attestation, where a person's own key
    /// has to vouch for the next one
    /// (`crate::signer::Signer::sign_key_attestation`). Every other use of this
    /// credential goes through [`TenantIdentity::gunnar`], which signs a
    /// per-call credential itself — this is the exception, and it is here
    /// rather than behind a method because the caller needs the key and not a
    /// client.
    pub signer: Arc<Signer>,
}

/// **A minted key waiting on its owner's word**, as the console's own account
/// surface shows it.
///
/// No private half: this crosses into [`crate::api::account`] and from there
/// into a browser, and the private half never leaves this process.
pub struct PendingKey {
    /// The principal it was minted for, so a record left over from a rebinding
    /// is not offered as this session's business.
    pub principal_id: String,
    /// The OpenSSH one-liner to attest — what `--key` takes.
    pub public_key: String,
    /// `SHA256:…`, the same fingerprint gunnar's refusal names.
    pub fingerprint: String,
}

/// What gunnar said about a key this console holds.
///
/// Three answers and not two, because the difference between "gone" and "I
/// could not ask" is the difference between dropping a private key and keeping
/// it through an outage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Enrolment {
    /// gunnar's key table holds it.
    Held,
    /// gunnar answered, and it is not there.
    Gone,
    /// gunnar did not answer.
    Unknown,
}

/// Something went wrong establishing a tenant's credential.
#[derive(Debug)]
pub enum TenantError {
    /// The state store could not be read or written.
    State(StateError),
    /// A generated or stored key was not usable.
    Signer(crate::signer::SignerError),
    /// gunnar refused a provisioning call.
    Gunnar(String),
    /// A per-user control-plane client could not be built.
    Client(String),
    /// A seed the ladder laid for this principal could not be used: it exists
    /// and cannot be read, or gunnar could not be asked whether it holds it.
    /// Never "there is no seed" — that is the ordinary path, not an error.
    Seed(String),
    /// A stored credential named a different principal than the passkey is
    /// pinned to. The record is discarded first and this is returned second, so
    /// the next call re-provisions; the refusal exists because a discard that
    /// answered `None` was indistinguishable from having no credential yet, and
    /// a seed the ladder enrolled would have been given up without a word.
    Rebound(String),
}

impl std::fmt::Display for TenantError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TenantError::State(e) => write!(f, "the tenant key store: {e}"),
            TenantError::Signer(e) => write!(f, "the tenant's signing key: {e}"),
            TenantError::Gunnar(m) => write!(f, "Gunnar refused to enrol the tenant's key: {m}"),
            TenantError::Client(m) => write!(f, "the tenant's control-plane client: {m}"),
            TenantError::Seed(m) => write!(f, "the ladder's seed for this principal: {m}"),
            TenantError::Rebound(m) => write!(f, "the stored credential was discarded: {m}"),
        }
    }
}

impl std::error::Error for TenantError {}

/// Mints, holds and retires per-tenant credentials.
///
/// One per console, shared behind an [`Arc`]: the store is a handle and the
/// endpoint is a string.
pub struct TenantKeys {
    /// gunnar's control-plane endpoint, to build a per-user client from.
    endpoint: String,
    /// Where the private keys and the pinned principal ids live, in the same
    /// 0600 store as the session key.
    store: StateStore,
    /// Serialises provisioning, so two requests racing a tenant's first read
    /// enrol exactly one key rather than two.
    provisioning: Mutex<()>,
    /// So a warm tenant does not re-read the store and rebuild a client on
    /// every request. Keyed by console user name.
    warm: Mutex<HashMap<String, Arc<Held>>>,
    /// **ONE channel to gunnar for every person this console signs as**
    /// (`Gunnar::over`), made at the first use (a lazy channel wants the
    /// runtime). Until 2026-10-06 each person's client dialled its own: a TCP
    /// connection, kept alive while idle, per session key — MEASURED on the KVM
    /// mock: ~900 logins after vm1 booted, the front ran out of file
    /// descriptors and its web, its forge door and monetize stopped answering.
    channel: std::sync::OnceLock<Result<tonic::transport::Channel, String>>,
    /// **Where the ladder lays a tenant key before anybody has logged in.**
    ///
    /// `GUNNAR_UI_TENANT_SEEDS`. `cargo xtask estate rebuild` (private-gunnar-ops,
    /// `rebuild::tenant_seed`) mints one ed25519 key per operator principal at
    /// `<dir>/<principal_id>` on this box at its `pair` rung, enrols the public
    /// half on that principal, and has the operator's OWN key vouch for it. Until
    /// 2026-09-20 this console minted its own key at the first read, gunnar
    /// refused it (the operator's key had spent the one free slot), and the
    /// operator was asked to paste an attestation — on every fresh estate. With a
    /// seed here, the key already exists and is already held: `ensure` adopts it
    /// and asks gunnar for nothing. `None` is the console as it was.
    seeds: Option<PathBuf>,
}

/// What [`TenantKeys::sweep`] did: keys revoked at gunnar and forgotten, and
/// keys kept for the next sweep.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Swept {
    pub revoked: usize,
    pub kept: usize,
}

struct Held {
    principal_id: String,
    fingerprint: String,
    signer: Arc<Signer>,
}

/// The state-store key a tenant's credential is filed under.
///
/// Its own namespace, distinct from `reviewer-identity:` — the two keys belong
/// to the same human and to different principals, and a console that confused
/// them would sign a tenant's read with a server-wide Write credential.
fn store_key(user: &str) -> String {
    format!("tenant-identity:{user}")
}

/// **The key this console minted and gunnar has not accepted yet.**
///
/// Its own namespace beside `tenant-identity:`, and the difference between the
/// two is the whole point: a record under THIS key is a private half the
/// console holds and cannot yet sign with, and a reader that confused it for an
/// enrolled credential would build a client whose every call gunnar refuses.
///
/// # Why a pending record exists at all
///
/// It is the other half of gunnar's attestation rule. `Access.AddKey` is
/// refused with `FAILED_PRECONDITION` when the target principal already holds a
/// signing key and the caller is not the target — correctly: that rule is what
/// stops this console's server-wide admin grant from minting itself into
/// somebody's identity. The refusal names the fingerprint to attest.
///
/// Until 2026-09-18 the console minted a key, was refused, and dropped it. The
/// next request minted a DIFFERENT one and was refused naming a different
/// fingerprint, so the operator was asked to attest a key that no longer
/// existed by the time they read the line. MEASURED on the front the same day:
/// five distinct fingerprints in two page loads, and no way out of it for
/// anybody — attesting needs a key the principal holds, and the key needing
/// attestation was gone.
///
/// So the mint is persisted BEFORE gunnar is asked. The fingerprint then stops
/// moving, which is what makes it a thing a human can sign over; see
/// [`TenantKeys::pending`] for what is shown to them and
/// [`TenantKeys::attest`] for the way back in.
fn pending_key(user: &str) -> String {
    format!("tenant-pending:{user}")
}

impl TenantKeys {
    /// A client signed as `signer`, over the console's one channel to gunnar.
    fn client(&self, signer: Arc<Signer>) -> Result<Gunnar, TenantError> {
        let channel = self
            .channel
            .get_or_init(|| Gunnar::lazy_channel(&self.endpoint))
            .clone()
            .map_err(TenantError::Client)?;
        Ok(Gunnar::over(channel, signer))
    }

    /// Build over gunnar's endpoint and the console's state store.
    pub fn new(endpoint: impl Into<String>, store: StateStore) -> TenantKeys {
        TenantKeys {
            endpoint: endpoint.into(),
            store,
            provisioning: Mutex::new(()),
            warm: Mutex::new(HashMap::new()),
            channel: std::sync::OnceLock::new(),
            seeds: None,
        }
    }

    /// Read seeds from `dir` — see [`TenantKeys::seeds`]. `None` leaves the
    /// console minting its own keys, as before.
    pub fn with_seeds(mut self, dir: Option<PathBuf>) -> TenantKeys {
        self.seeds = dir;
        self
    }

    /// The credential this user's reads are answered with, minting one on first
    /// use.
    ///
    /// **`principal_id` is required, and there is no lookup here.** It is the
    /// id the passkey was bound to (`auth::StoredCred::principal_id`), pinned
    /// at registration or at the first login after this console learned how —
    /// the moments when the name is somebody's to give. A read path that
    /// resolved a name would be GUI-001 living on in a quieter place, and it
    /// would also put an `Access.ListPrincipals` on the console's ADMIN key in
    /// front of every GET a viewer makes, which is measurable: the fingerprint
    /// log for one viewer's read showed the admin key interleaved with every
    /// call.
    ///
    /// # Errors
    ///
    /// The store could not be read or written, a key could not be minted, or
    /// gunnar refused the enrolment. Every one of them is a refusal to hand
    /// back a credential — never a fallback to the console's own.
    pub async fn ensure(
        &self,
        user: &str,
        principal_id: &str,
        admin: &Gunnar,
    ) -> Result<TenantIdentity, TenantError> {
        // A credential this process has already used is taken at its word: it
        // was checked against gunnar when it came off the disk (below), and
        // re-asking on every read would put an `Access.ListKeys` in front of
        // every request a tenant makes.
        if let Some(identity) = self.warm(user, principal_id).await? {
            return Ok(identity);
        }
        let _guard = self.provisioning.lock().await;
        // Re-check under the lock: another request may have provisioned this
        // tenant while we waited for it.
        if let Some(identity) = self.warm(user, principal_id).await? {
            return Ok(identity);
        }

        // **A stored key gunnar has never heard of is not a credential.**
        //
        // The two sides keep their halves in different places: the private half
        // lives in this console's state store, the public half in gunnar's key
        // table. A RE-IMAGE resets gunnar's and leaves this one untouched — and
        // then the console signs every call with a key the appliance does not
        // know, which comes back as `Unauthenticated` on every read, for ever,
        // with a stored record that looks perfectly healthy from here.
        //
        // So the record is believed only once per process: when it is read off
        // the disk it is checked against gunnar's table, and a key that is not
        // there is DROPPED rather than used. What follows is the ordinary
        // first-run path — mint, ask, and if the principal has since grown a
        // key of its own, the attestation panel.
        if let Some(identity) = self.existing(user, Some(principal_id)).await? {
            match self.still_enrolled(&identity, admin).await {
                Enrolment::Held => return Ok(identity),
                // Not a verdict. Keep the key and let the read that follows be
                // answered — or refused — by gunnar itself.
                Enrolment::Unknown => return Ok(identity),
                Enrolment::Gone => {}
            }
            tracing::warn!(
                target: crate::auth::AUDIT_TARGET,
                event = "tenant.key_unknown_to_gunnar",
                user = %user,
                principal = %principal_id,
                fingerprint = %identity.fingerprint,
                "the key this console holds for a tenant is not in gunnar's table — a \
                 re-imaged or restored appliance — so it is dropped and a new one asked for"
            );
            self.warm.lock().await.remove(user);
            self.store
                .remove(&store_key(user))
                .map_err(TenantError::State)?;
        }

        let principal_id = principal_id.to_owned();

        // **A seed the ladder laid** — see [`TenantKeys::seeds`]. Three answers
        // and not two, the way `still_enrolled` keeps them apart: gunnar HOLDS
        // it, so it is adopted and nothing is asked for; gunnar says it is GONE
        // (an appliance re-imaged since the ladder paired it), so it is the key
        // this console asks for — as the first enrolment, or into the pending
        // slot where the panel names ITS fingerprint — never a fresh mint
        // beside it; gunnar could not be asked, so this refuses, because a
        // credential handed out on no evidence is the confused deputy again.
        let seeded = self.seeded_private(&principal_id)?;
        if let Some(seed) = &seeded {
            let signer = Signer::from_openssh(seed).map_err(TenantError::Signer)?;
            let fingerprint = signer.fingerprint().to_owned();
            match self.held_at_gunnar(&principal_id, &fingerprint, admin).await {
                Enrolment::Held => {
                    self.store
                        .put(&store_key(user), format!("{principal_id}\n{seed}").as_bytes())
                        .map_err(TenantError::State)?;
                    tracing::info!(
                        target: crate::auth::AUDIT_TARGET,
                        event = "tenant.seed_adopted",
                        user = %user,
                        principal = %principal_id,
                        fingerprint = %fingerprint,
                        "the ladder's seed on this principal is held by gunnar and is now this \
                         console's credential for them; no key was minted and nothing was asked for"
                    );
                    let held = Arc::new(Held {
                        principal_id: principal_id.clone(),
                        fingerprint,
                        signer: Arc::new(signer),
                    });
                    self.warm.lock().await.insert(user.to_owned(), held.clone());
                    return self.identity_from(&held);
                }
                Enrolment::Gone => {
                    tracing::warn!(
                        target: crate::auth::AUDIT_TARGET,
                        event = "tenant.seed_not_enrolled",
                        user = %user,
                        principal = %principal_id,
                        fingerprint = %fingerprint,
                        "a seed lies here for this principal and gunnar does not hold it — an \
                         appliance re-imaged since the ladder paired it; it is the key asked for now"
                    );
                }
                Enrolment::Unknown => {
                    return Err(TenantError::Seed(format!(
                        "Gunnar could not be asked whether it holds {fingerprint} on {principal_id}, \
                         so the seed is neither adopted nor replaced"
                    )));
                }
            }
        }

        // **The same key every time, until gunnar takes it.** A mint here would
        // be a new fingerprint on every request, and gunnar's refusal names the
        // fingerprint the principal is asked to sign over — so a moving one is
        // an instruction nobody can carry out. See [`pending_key`].
        let private = match self.pending_private(user, &principal_id)? {
            Some(private) => private,
            None => {
                let private = match seeded {
                    Some(seed) => seed,
                    None => mint_ed25519().map_err(TenantError::Signer)?,
                };
                // Written BEFORE gunnar is asked. A crash between here and the
                // call leaves a key nothing has accepted, which is exactly what
                // the next `ensure` is built to pick up.
                self.store
                    .put(
                        &pending_key(user),
                        format!("{principal_id}\n{private}").as_bytes(),
                    )
                    .map_err(TenantError::State)?;
                private
            }
        };
        self.enrol(user, &principal_id, &private, None, admin).await
    }

    /// **Enrol a minted key on its principal, with or without the owner's
    /// word**, and promote it on acceptance.
    ///
    /// The one place `Access.AddKey` is called for a tenant, so the two ways in
    /// — the unattested first enrolment and the attested retry — cannot drift
    /// on what they store or what they log.
    async fn enrol(
        &self,
        user: &str,
        principal_id: &str,
        private: &str,
        attestation: Option<pb::SignedKeyAttestation>,
        admin: &Gunnar,
    ) -> Result<TenantIdentity, TenantError> {
        let signer = Signer::from_openssh(private).map_err(TenantError::Signer)?;
        let fingerprint = signer.fingerprint().to_owned();
        let attested = attestation.is_some();

        // **`Access.AddKey` and nothing else.** No `AddPrincipal`: the
        // principal is whose it already is. No `Grant`: what this credential
        // may read is what that principal was already allowed to read, and a
        // grant issued here would be this console deciding that instead of
        // gunnar.
        let added = admin
            .access()
            .add_key(pb::AddKeyRequest {
                principal_id: principal_id.to_owned(),
                openssh_public_key: signer.public_key_openssh(),
                comment: format!("gunnar-ui console session for {user}"),
                // Absent on a first enrolment, because the console holds no key
                // of the tenant's to attest with and a principal with no
                // signing key has nobody to ask. Present on the retry, where it
                // is the tenant's own sshsig, pasted by them — see
                // [`TenantKeys::attest`] and `api::access::add_key`.
                attestation,
                // No expiry. The console cannot renew one unattended, and an
                // expired key would take a tenant's whole console dark with no
                // way back in; revocation is the lever, and it is
                // [`TenantKeys::forget`].
                expires_at_unix_ms: None,
            })
            .await;
        match added {
            Ok(_) => {}
            // **gunnar already holds this very key on this principal** — its
            // documented idempotent answer ("no new key, so no attestation is
            // owed — ALREADY_EXISTS, which callers tolerate"): a restored main,
            // a retried call, an enrolment this console lost track of. That IS
            // an enrolment. Believed only after gunnar lists the fingerprint on
            // THIS principal: an `AlreadyExists` for a key filed under anybody
            // else stays a refusal, because signing with it would be signing as
            // them. Until 2026-10-04 every `AlreadyExists` was a failure, the
            // key never left the pending slot, and the person's reads were the
            // guest's for ever.
            Err(status) if status.code() == tonic::Code::AlreadyExists => {
                match self.held_at_gunnar(principal_id, &fingerprint, admin).await {
                    Enrolment::Held => tracing::info!(
                        target: crate::auth::AUDIT_TARGET,
                        event = "tenant.key_already_held",
                        user = %user,
                        principal = %principal_id,
                        fingerprint = %fingerprint,
                        "gunnar already held this console's key on the tenant's principal; it is \
                         their credential"
                    ),
                    Enrolment::Gone | Enrolment::Unknown => {
                        return Err(TenantError::Gunnar(status.to_string()))
                    }
                }
            }
            Err(status) => return Err(TenantError::Gunnar(status.to_string())),
        }

        // Promoted only after gunnar accepted it, so a half-enrolled tenant is
        // retried rather than remembered. `principal_id + '\n' + the multi-line
        // OpenSSH private key`, read back by splitting on the first newline —
        // the same record shape `reviewer` uses.
        let record = format!("{principal_id}\n{private}");
        self.store
            .put(&store_key(user), record.as_bytes())
            .map_err(TenantError::State)?;
        // The pending copy goes only now, and its removal is not allowed to
        // fail the call: the credential is live at gunnar and in the store, and
        // a leftover pending record is read back as "already enrolled" by
        // [`TenantKeys::pending`] rather than as an instruction.
        if let Err(why) = self.store.remove(&pending_key(user)) {
            tracing::warn!(
                target: crate::auth::AUDIT_TARGET,
                event = "tenant.pending_not_cleared",
                user = %user,
                reason = %why,
                "the enrolled key could not be cleared from the pending slot; it is \
                 superseded, not live"
            );
        }
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event = "tenant.key_enrolled",
            user = %user,
            principal = %principal_id,
            fingerprint = %fingerprint,
            attested = attested,
            "this console enrolled a key of its own on a tenant's principal, so \
             their reads are answered with their grants"
        );

        let held = Arc::new(Held {
            principal_id: principal_id.to_owned(),
            fingerprint,
            signer: Arc::new(signer),
        });
        self.warm.lock().await.insert(user.to_owned(), held.clone());
        self.identity_from(&held)
    }

    /// The private half waiting in the pending slot for THIS principal, if any.
    ///
    /// A record naming a different principal is dropped rather than reused: the
    /// pin is somebody's decision about whose account this session is, and a
    /// key minted before it must not be enrolled onto the new one. Same rule as
    /// [`TenantKeys::existing`], same reason.
    fn pending_private(
        &self,
        user: &str,
        principal_id: &str,
    ) -> Result<Option<String>, TenantError> {
        let Some(bytes) = self
            .store
            .get(&pending_key(user))
            .map_err(TenantError::State)?
        else {
            return Ok(None);
        };
        let text = String::from_utf8_lossy(&bytes).into_owned();
        let Some((held, private)) = text.split_once('\n') else {
            self.store
                .remove(&pending_key(user))
                .map_err(TenantError::State)?;
            return Ok(None);
        };
        if held != principal_id {
            self.store
                .remove(&pending_key(user))
                .map_err(TenantError::State)?;
            return Ok(None);
        }
        Ok(Some(private.to_owned()))
    }

    /// **What this person is being asked to vouch for**, or `None` when nothing
    /// is waiting.
    ///
    /// `None` is also the answer when the credential is already enrolled, even
    /// if a pending record survived: an enrolled tenant has nothing to do, and
    /// showing them a key to attest would be an instruction with no effect.
    pub async fn pending(
        &self,
        user: &str,
        principal_id: &str,
    ) -> Result<Option<PendingKey>, TenantError> {
        if self.existing(user, Some(principal_id)).await?.is_some() {
            return Ok(None);
        }
        let Some(private) = self.pending_private(user, principal_id)? else {
            return Ok(None);
        };
        let signer = Signer::from_openssh(&private).map_err(TenantError::Signer)?;
        Ok(Some(PendingKey {
            principal_id: principal_id.to_owned(),
            public_key: signer.public_key_openssh(),
            fingerprint: signer.fingerprint().to_owned(),
        }))
    }

    /// **The tenant's own word, carried to gunnar.**
    ///
    /// The attestation is an sshsig the person made with a key they already
    /// hold, over the fingerprint of the key in the pending slot. This console
    /// composes none of it and verifies none of it: gunnar checks the signature
    /// against its own key table, and a wrong one comes back as
    /// `INVALID_ARGUMENT` in gunnar's words. That is deliberate — a console
    /// that judged the signature would be a second opinion about the thing the
    /// appliance is the authority on.
    ///
    /// # Errors
    ///
    /// Nothing is pending (there is nothing to attest), the stored key is
    /// unusable, or gunnar refused. A refusal leaves the pending record where
    /// it is, so a corrected paste can be tried against the same fingerprint.
    pub async fn attest(
        &self,
        user: &str,
        principal_id: &str,
        attestation: pb::SignedKeyAttestation,
        admin: &Gunnar,
    ) -> Result<TenantIdentity, TenantError> {
        let _guard = self.provisioning.lock().await;
        if let Some(identity) = self.existing(user, Some(principal_id)).await? {
            return Ok(identity);
        }
        let Some(private) = self.pending_private(user, principal_id)? else {
            return Err(TenantError::Gunnar(
                "no key is waiting to be attested for this account".into(),
            ));
        };
        self.enrol(user, principal_id, &private, Some(attestation), admin)
            .await
    }

    /// The credential already in this process's memory, or `None`.
    ///
    /// Split out of [`TenantKeys::existing`] so that `ensure` can tell a
    /// credential it has already used from one it has just read off the disk:
    /// only the second needs checking against gunnar, and putting that check on
    /// the first would be an `Access.ListKeys` in front of every tenant read.
    async fn warm(&self, user: &str, pinned: &str) -> Result<Option<TenantIdentity>, TenantError> {
        let held = self.warm.lock().await.get(user).cloned();
        match held {
            Some(held) if held.principal_id == pinned => Ok(Some(self.identity_from(&held)?)),
            _ => Ok(None),
        }
    }

    /// **Does gunnar still hold this key?** — and the third answer matters.
    ///
    /// A stored credential is dropped only when gunnar POSITIVELY says the key
    /// is not in its table. "I could not ask" is not that answer, and treating
    /// it as one was a real mistake caught by
    /// `a_viewers_read_is_not_signed_with_the_consoles_admin_key`: a momentary
    /// refusal from the control plane would discard a perfectly good private
    /// key and then try to re-provision INTO THE SAME OUTAGE, turning a blink
    /// into a tenant with no credential at all and a fresh one waiting to be
    /// attested by hand.
    ///
    /// Leaving the record alone costs nothing that is not already lost: the
    /// read that follows is signed and gunnar answers it, or does not, in its
    /// own words. The only state this check exists to end is the DURABLE one —
    /// a key gunnar has genuinely forgotten, which it will go on genuinely
    /// forgetting on the next request, and the next.
    async fn still_enrolled(&self, identity: &TenantIdentity, admin: &Gunnar) -> Enrolment {
        self.held_at_gunnar(&identity.principal_id, &identity.fingerprint, admin)
            .await
    }

    /// The one `Access.ListKeys` behind both [`TenantKeys::still_enrolled`]
    /// and the seed's adoption, so the two cannot read one table two ways.
    async fn held_at_gunnar(&self, principal_id: &str, fingerprint: &str, admin: &Gunnar) -> Enrolment {
        match admin
            .access()
            .list_keys(pb::ListKeysRequest {
                principal_id: principal_id.to_owned(),
            })
            .await
        {
            Ok(answer) => {
                let held = answer
                    .into_inner()
                    .keys
                    .iter()
                    .any(|k| k.fingerprint == fingerprint && k.principal_id == principal_id);
                if held {
                    Enrolment::Held
                } else {
                    Enrolment::Gone
                }
            }
            Err(_) => Enrolment::Unknown,
        }
    }

    /// The credential this user already has, or `None`.
    ///
    /// A stored record whose principal disagrees with `pinned` does not outrank
    /// the pin: the pin is the binding somebody with authority made. The record
    /// is dropped so the next call re-provisions, and the drop is answered as
    /// [`TenantError::Rebound`] — NOT as `Ok(None)`. Until 2026-09-20 it was
    /// `Ok(None)`, which reads the same as "this user has no credential yet",
    /// so a credential the estate ladder had enrolled and vouched for could be
    /// thrown away with nothing but a log line to say it happened. The refusal
    /// is not fatal: the record is already gone when it is returned, so the
    /// caller's next read provisions for the pinned principal.
    pub async fn existing(
        &self,
        user: &str,
        pinned: Option<&str>,
    ) -> Result<Option<TenantIdentity>, TenantError> {
        if let Some(held) = self.warm.lock().await.get(user).cloned() {
            if pinned.is_none_or(|want| want == held.principal_id) {
                return Ok(Some(self.identity_from(&held)?));
            }
        }
        let Some(bytes) = self
            .store
            .get(&store_key(user))
            .map_err(TenantError::State)?
        else {
            return Ok(None);
        };
        let held = Arc::new(self.decode(&bytes)?);
        if pinned.is_some_and(|want| want != held.principal_id) {
            tracing::warn!(
                target: crate::auth::AUDIT_TARGET,
                event = "tenant.rebound",
                user = %user,
                held = %held.principal_id,
                pinned = %pinned.unwrap_or_default(),
                "a stored tenant credential names a different principal than the \
                 passkey is bound to; it is discarded rather than trusted"
            );
            self.warm.lock().await.remove(user);
            self.store
                .remove(&store_key(user))
                .map_err(TenantError::State)?;
            // Named, not `None`. The record is already gone, so the next call
            // provisions from the ladder's seed or mints; this answer exists so
            // the discard is READ somewhere instead of only logged.
            return Err(TenantError::Rebound(format!(
                "console user `{user}` held a credential on principal `{}` ({}) and the passkey is \
                 pinned to `{}`; the credential is discarded and the next read provisions for the \
                 pinned principal",
                held.principal_id,
                held.fingerprint,
                pinned.unwrap_or_default()
            )));
        }
        self.warm.lock().await.insert(user.to_owned(), held.clone());
        Ok(Some(self.identity_from(&held)?))
    }

    /// **Retire this user's credential: revoke the key at gunnar, then forget
    /// it here.**
    ///
    /// Called when the last passkey behind a console user is revoked. In that
    /// order on purpose: a console that forgot the key first would leave a live
    /// credential in gunnar's key table with nothing left in this process that
    /// knows its fingerprint, which is a key nobody can find to revoke.
    ///
    /// A gunnar that refuses is reported and the local halves are dropped
    /// anyway — the console must not go on holding a private key for somebody
    /// it has just revoked, and an operator can see the fingerprint in the
    /// audit line and revoke it by hand.
    ///
    /// # Errors
    ///
    /// The store could not be read or written. gunnar's refusal is returned
    /// too, after the local state is dropped.
    pub async fn forget(&self, user: &str, admin: &Gunnar) -> Result<Option<String>, TenantError> {
        let Some(bytes) = self
            .store
            .get(&store_key(user))
            .map_err(TenantError::State)?
        else {
            return Ok(None);
        };
        let held = self.decode(&bytes)?;
        let refusal = admin
            .access()
            .revoke_key(pb::RevokeKeyRequest {
                fingerprint: held.fingerprint.clone(),
            })
            .await
            .err();
        self.warm.lock().await.remove(user);
        self.store
            .remove(&store_key(user))
            .map_err(TenantError::State)?;
        match refusal {
            None => {
                tracing::info!(
                    target: crate::auth::AUDIT_TARGET,
                    event = "tenant.key_revoked",
                    user = %user,
                    principal = %held.principal_id,
                    fingerprint = %held.fingerprint,
                    "the console's key on a tenant's principal was revoked with their last passkey"
                );
                Ok(Some(held.fingerprint))
            }
            Some(status) => {
                tracing::error!(
                    target: crate::auth::AUDIT_TARGET,
                    event = "tenant.key_revoke_failed",
                    user = %user,
                    principal = %held.principal_id,
                    fingerprint = %held.fingerprint,
                    message = %status.message(),
                    "gunnar would not revoke the console's key on a tenant's principal; \
                     it is still live there and must be revoked by hand"
                );
                Err(TenantError::Gunnar(status.to_string()))
            }
        }
    }

    /// **RA5-13: retire every key this console no longer signs with.** Since
    /// the identity core (IDENTITY-HOST) a session holds no key at gunnar and
    /// nothing in production signs as a tenant key, so each one still in the
    /// store — `tenant-identity:*`, `tenant-pending:*` — and each seed the
    /// ladder laid ([`TenantKeys::seeds`]) is a private key enrolled on its
    /// person and used by nobody: a copy of the store would sign as each of
    /// them. Each is revoked at gunnar (the console's admin key), then
    /// forgotten here; a key gunnar no longer holds (`NotFound`) is forgotten
    /// too. One gunnar would not revoke now is KEPT, and said, so the next
    /// sweep tries again: forgetting it first would leave a live key nobody
    /// can find to revoke. Nothing else in the store is touched.
    pub async fn sweep(&self, admin: &Gunnar) -> Swept {
        let mut swept = Swept::default();
        let records = match self.store.auth_keys() {
            Ok(keys) => keys,
            Err(e) => {
                tracing::error!(target: crate::auth::AUDIT_TARGET, event = "tenant.sweep_failed", error = %e, "the console's store could not be listed: no tenant key was retired");
                swept.kept += 1;
                return swept;
            }
        };
        for record in records.into_iter().filter(|k| k.starts_with("tenant-identity:") || k.starts_with("tenant-pending:")) {
            let private = match self.store.get(&record) {
                Ok(Some(bytes)) => String::from_utf8_lossy(&bytes).split_once('\n').map(|(_, key)| key.to_owned()),
                Ok(None) => continue,
                Err(e) => {
                    tracing::error!(target: crate::auth::AUDIT_TARGET, event = "tenant.sweep_failed", record = %record, error = %e, "a stored tenant key could not be read: kept");
                    swept.kept += 1;
                    continue;
                }
            };
            let fingerprint = private.as_deref().and_then(|p| Signer::from_openssh(p).ok()).map(|s| s.fingerprint().to_owned());
            let gone = match &fingerprint {
                Some(fp) => self.revoked_at_gunnar(admin, fp, &record).await,
                // No key in it: nothing to revoke, and nothing to keep.
                None => true,
            };
            if gone && self.store.remove(&record).is_ok() {
                self.warm.lock().await.remove(record.split_once(':').map(|(_, user)| user).unwrap_or_default());
                swept.revoked += 1;
            } else {
                swept.kept += 1;
            }
        }
        if let Some(dir) = &self.seeds {
            let files = match std::fs::read_dir(dir) {
                Ok(files) => files.flatten().collect::<Vec<_>>(),
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
                Err(e) => {
                    tracing::error!(target: crate::auth::AUDIT_TARGET, event = "tenant.sweep_failed", dir = %dir.display(), error = %e, "the tenant seeds could not be listed: kept");
                    swept.kept += 1;
                    Vec::new()
                }
            };
            for file in files {
                let path = file.path();
                let name = file.file_name().to_string_lossy().into_owned();
                if !plain_file_name(&name) || !path.is_file() {
                    continue;
                }
                let Ok(text) = std::fs::read_to_string(&path) else {
                    swept.kept += 1;
                    continue;
                };
                let Ok(signer) = Signer::from_openssh(&text) else { continue };
                if self.revoked_at_gunnar(admin, signer.fingerprint(), &format!("seed {name}")).await && std::fs::remove_file(&path).is_ok() {
                    swept.revoked += 1;
                } else {
                    swept.kept += 1;
                }
            }
        }
        tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "tenant.swept", revoked = swept.revoked, kept = swept.kept, "the per-person keys this console no longer signs with: revoked at gunnar and forgotten");
        swept
    }

    /// Revoke `fingerprint` at gunnar: `true` when gunnar holds it no more.
    async fn revoked_at_gunnar(&self, admin: &Gunnar, fingerprint: &str, what: &str) -> bool {
        match admin.access().revoke_key(pb::RevokeKeyRequest { fingerprint: fingerprint.to_owned() }).await {
            Ok(_) => {
                tracing::info!(target: crate::auth::AUDIT_TARGET, event = "tenant.key_revoked", record = %what, fingerprint = %fingerprint, "a tenant key this console no longer signs with was revoked at gunnar");
                true
            }
            Err(status) if status.code() == tonic::Code::NotFound => true,
            Err(status) => {
                tracing::error!(target: crate::auth::AUDIT_TARGET, event = "tenant.key_revoke_failed", record = %what, fingerprint = %fingerprint, message = %status.message(), "gunnar would not revoke a tenant key this console no longer signs with: kept, the next sweep tries again");
                false
            }
        }
    }

    /// **The private half the ladder laid for `principal_id`**, or `None`.
    ///
    /// `None` when no directory is configured, when nothing lies there for this
    /// principal, or when the id could not be a plain file name — a `/`, a
    /// leading `.`, anything outside ASCII letters, digits, `-`, `_` and `.`:
    /// a principal id is a value off the wire and this is a path, so it is
    /// refused as a name before it is joined to one. A file that EXISTS and
    /// cannot be read is an error and not `None`: a seed this console cannot
    /// open is the defect this door ends, wearing a permission bit, and a
    /// quiet fresh mint would report it as health.
    fn seeded_private(&self, principal_id: &str) -> Result<Option<String>, TenantError> {
        let Some(dir) = &self.seeds else {
            return Ok(None);
        };
        if !plain_file_name(principal_id) {
            tracing::warn!(
                target: crate::auth::AUDIT_TARGET,
                event = "tenant.seed_name_refused",
                principal = %principal_id,
                "a principal id that cannot be a plain file name is not looked up as a seed"
            );
            return Ok(None);
        }
        let path: PathBuf = Path::new(dir).join(principal_id);
        match std::fs::read_to_string(&path) {
            Ok(text) => Ok(Some(text)),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(TenantError::Seed(format!(
                "{} exists but could not be read ({e})",
                path.display()
            ))),
        }
    }

    fn decode(&self, bytes: &[u8]) -> Result<Held, TenantError> {
        let text = String::from_utf8_lossy(bytes);
        let (principal_id, private) = text
            .split_once('\n')
            .ok_or_else(|| TenantError::Gunnar("a stored tenant record has no key".into()))?;
        let signer = Signer::from_openssh(private).map_err(TenantError::Signer)?;
        Ok(Held {
            principal_id: principal_id.to_owned(),
            fingerprint: signer.fingerprint().to_owned(),
            signer: Arc::new(signer),
        })
    }

    fn identity_from(&self, held: &Held) -> Result<TenantIdentity, TenantError> {
        let gunnar = self.client(held.signer.clone())?;
        Ok(TenantIdentity {
            principal_id: held.principal_id.clone(),
            fingerprint: held.fingerprint.clone(),
            gunnar,
            signer: held.signer.clone(),
        })
    }
}

/// A principal id that is safe to join to a directory: ASCII letters, digits,
/// `-`, `_`, `.`; not empty; not beginning with `.`. The same rule the ladder
/// applies before it lays a seed (`rebuild::tenant_seed::checked_name`), so a
/// name the ladder would refuse is a name this console never looks up.
fn plain_file_name(principal_id: &str) -> bool {
    !principal_id.is_empty()
        && !principal_id.starts_with('.')
        && principal_id
            .bytes()
            .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.')
}

#[cfg(test)]
mod tests {
    use super::plain_file_name;
    use crate::pb;
    use std::sync::{Arc, Mutex};

    /// A gunnar that ALREADY holds whatever key it is asked to enrol on `kim`
    /// — what a restored main, a retried call or an earlier enrolment the
    /// console lost track of all look like — and answers `AlreadyExists`, its
    /// documented idempotent answer ("callers tolerate"). `foreign` makes it
    /// claim the key belongs to somebody else instead.
    #[derive(Clone, Default)]
    struct Holds {
        asked: Arc<Mutex<Vec<String>>>,
        foreign: bool,
    }

    #[tonic::async_trait]
    impl pb::access_server::Access for Holds {
        async fn add_key(
            &self,
            request: tonic::Request<pb::AddKeyRequest>,
        ) -> Result<tonic::Response<pb::AddKeyResponse>, tonic::Status> {
            let asked = request.into_inner();
            let parsed = ssh_key::PublicKey::from_openssh(&asked.openssh_public_key)
                .map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
            let fingerprint = parsed.fingerprint(ssh_key::HashAlg::Sha256).to_string();
            self.asked.lock().expect("asked").push(fingerprint.clone());
            Err(tonic::Status::already_exists(format!(
                "principal {} already holds key {fingerprint}",
                asked.principal_id
            )))
        }
        async fn list_keys(
            &self,
            request: tonic::Request<pb::ListKeysRequest>,
        ) -> Result<tonic::Response<pb::ListKeysResponse>, tonic::Status> {
            let principal = request.into_inner().principal_id;
            let keys = self
                .asked
                .lock()
                .expect("asked")
                .iter()
                .map(|fingerprint| pb::PublicKey {
                    fingerprint: fingerprint.clone(),
                    algo: "ssh-ed25519".to_owned(),
                    principal_id: if self.foreign { "somebody-else".to_owned() } else { principal.clone() },
                    ..Default::default()
                })
                .filter(|k| k.principal_id == principal)
                .collect();
            Ok(tonic::Response::new(pb::ListKeysResponse { keys }))
        }
    }

    async fn serve(holds: Holds) -> String {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
            .await
            .expect("a loopback port");
        let addr = listener.local_addr().expect("its address");
        tokio::spawn(async move {
            tonic::transport::Server::builder()
                .add_service(pb::access_server::AccessServer::new(holds))
                .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
                .await
        });
        format!("http://{addr}")
    }

    fn console(endpoint: &str) -> crate::grpc::Gunnar {
        crate::grpc::Gunnar::connect_lazy(
            endpoint,
            Arc::new(
                crate::signer::Signer::new(ssh_key::PrivateKey::from(
                    ssh_key::private::Ed25519Keypair::from_seed(&[71u8; 32]),
                ))
                .expect("a signer"),
            ),
        )
        .expect("a handle")
    }

    /// ★ **ONE connection to gunnar for every person this console signs as.**
    ///
    /// MEASURED on the KVM mock, 2026-10-06 (private-gunnar-ops `bench load`):
    /// after about 900 logins since vm1 booted, the front answered nothing —
    /// `web: accept on :443 error=No file descriptors available (os error 24)`,
    /// the forge forward refused `:2222` with the same words, monetize could
    /// not read its own config, and the console told every person "tcp open
    /// error". Every login mints a session key, and [`super::TenantKeys::session`]
    /// built a client — a tonic channel, one HTTP/2 connection kept alive
    /// while idle — per session key and kept it: a file descriptor on vm1 per
    /// login, for ever. gunnar-front runs the console in its own process, so
    /// the web, the forge's door and monetize went down with it.
    ///
    /// A listener that answers nothing and counts the connections made to it:
    /// five people, five sessions, one call each — one connection.
    #[tokio::test]
    async fn every_persons_session_rides_the_one_connection() {
        use std::sync::atomic::{AtomicUsize, Ordering};
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port");
        let addr = listener.local_addr().expect("its address");
        let accepted = Arc::new(AtomicUsize::new(0));
        let counted = accepted.clone();
        tokio::spawn(async move {
            let mut held = Vec::new();
            while let Ok((socket, _)) = listener.accept().await {
                counted.fetch_add(1, Ordering::SeqCst);
                held.push(socket);
            }
        });
        // Every person's client is the console's one channel carrying THEIR
        // identity-core session (IDENTITY-HOST): five sessions, one connection.
        let console = crate::grpc::Gunnar::connect_lazy(
            &format!("http://{addr}"),
            Arc::new(crate::signer::Signer::new(ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(&[1; 32]))).expect("a signer")),
        )
        .expect("a lazy channel");
        for n in 0..5u8 {
            let person = console.as_session(&format!("nis1.person-{n}"));
            let mut access = person.access();
            // The listener never answers: the call is given up, the connection stays.
            let _ = tokio::time::timeout(std::time::Duration::from_millis(300), access.list_keys(pb::ListKeysRequest::default())).await;
        }
        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
        assert_eq!(
            accepted.load(Ordering::SeqCst),
            1,
            "a connection per session is a file descriptor on vm1 per login, kept for ever"
        );
    }

    /// ★ **gunnar already holding the very key the console asks it to enrol is
    /// an enrolment, not a refusal.** Red before this row: `AlreadyExists` was
    /// a failure, the key never left the pending slot, and every read the
    /// person made was answered by the grantless guest for ever — the uitest
    /// rig's viewer `kim` (the console's own log: "principal kim already holds
    /// key SHA256:…"), whose own repositories 404'd and whose room said
    /// "principal guest has no read grant on namespace:kim".
    #[tokio::test]
    async fn a_key_gunnar_already_holds_on_the_principal_is_enrolled() {
        let holds = Holds::default();
        let endpoint = serve(holds.clone()).await;
        let dir = tempfile::tempdir().expect("a temp dir");
        let store = crate::state::StateStore::open(dir.path().join("state.redb")).expect("a store");
        let keys = super::TenantKeys::new(endpoint.clone(), store);
        let identity = keys
            .ensure("kim", "kim", &console(&endpoint))
            .await
            .expect("a key gunnar holds on kim is kim's credential");
        assert_eq!(
            holds.asked.lock().expect("asked").clone(),
            vec![identity.fingerprint.clone()],
            "the credential handed back is the key gunnar said it holds"
        );
        // …and it is remembered: the next read asks gunnar nothing.
        let again = keys.ensure("kim", "kim", &console(&endpoint)).await.expect("warm");
        assert_eq!(again.fingerprint, identity.fingerprint);
        assert_eq!(holds.asked.lock().expect("asked").len(), 1);
    }

    /// The other half: `AlreadyExists` for a key gunnar does NOT list on this
    /// principal is still a refusal — the console never signs as a principal
    /// with a key gunnar files under somebody else.
    #[tokio::test]
    async fn already_exists_for_a_key_the_principal_does_not_hold_is_still_refused() {
        let holds = Holds {
            foreign: true,
            ..Holds::default()
        };
        let endpoint = serve(holds.clone()).await;
        let dir = tempfile::tempdir().expect("a temp dir");
        let store = crate::state::StateStore::open(dir.path().join("state.redb")).expect("a store");
        let keys = super::TenantKeys::new(endpoint.clone(), store);
        assert!(keys.ensure("kim", "kim", &console(&endpoint)).await.is_err());
    }

    /// A gunnar that revokes whatever key it is asked to, except `refuse`, and
    /// says which it was asked for.
    #[derive(Clone, Default)]
    struct Revokes {
        asked: Arc<Mutex<Vec<String>>>,
        refuse: Option<String>,
    }

    #[tonic::async_trait]
    impl pb::access_server::Access for Revokes {
        async fn revoke_key(
            &self,
            request: tonic::Request<pb::RevokeKeyRequest>,
        ) -> Result<tonic::Response<pb::RevokeKeyResponse>, tonic::Status> {
            let fingerprint = request.into_inner().fingerprint;
            self.asked.lock().expect("asked").push(fingerprint.clone());
            if self.refuse.as_deref() == Some(fingerprint.as_str()) {
                return Err(tonic::Status::unavailable("gunnar is busy"));
            }
            if fingerprint == "SHA256:gone" {
                return Err(tonic::Status::not_found("no such key"));
            }
            Ok(tonic::Response::new(pb::RevokeKeyResponse {}))
        }
    }

    fn fingerprint_of(private: &str) -> String {
        crate::signer::Signer::from_openssh(private).expect("a key").fingerprint().to_owned()
    }

    /// ★ **RA5-13: the per-person keys this console no longer signs with are
    /// revoked at gunnar and removed here.** Since the identity core, no
    /// session holds a key at gunnar; every tenant key minted before (and every
    /// seed the ladder laid) stays in the store and enrolled on its person,
    /// unused — a copy of the store signs as each of them. The sweep revokes
    /// each at gunnar, then forgets it (the record, the pending record, the
    /// seed file); a key gunnar no longer holds is forgotten too; one gunnar
    /// would not revoke now is KEPT, so the next sweep tries again.
    #[tokio::test]
    async fn the_sweep_revokes_every_stored_tenant_key_at_gunnar_and_forgets_it() {
        let (anna, bob, kim, seed) = (
            crate::reviewer::mint_ed25519().unwrap(),
            crate::reviewer::mint_ed25519().unwrap(),
            crate::reviewer::mint_ed25519().unwrap(),
            crate::reviewer::mint_ed25519().unwrap(),
        );
        let revokes = Revokes { refuse: Some(fingerprint_of(&kim)), ..Revokes::default() };
        let endpoint = {
            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port");
            let addr = listener.local_addr().expect("its address");
            let served = revokes.clone();
            tokio::spawn(async move {
                tonic::transport::Server::builder()
                    .add_service(pb::access_server::AccessServer::new(served))
                    .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
                    .await
            });
            format!("http://{addr}")
        };
        let dir = tempfile::tempdir().expect("a temp dir");
        let store = crate::state::StateStore::open(dir.path().join("state.redb")).expect("a store");
        store.put("tenant-identity:anna", format!("anna\n{anna}").as_bytes()).unwrap();
        store.put("tenant-pending:bob", format!("bob\n{bob}").as_bytes()).unwrap();
        store.put("tenant-identity:kim", format!("kim\n{kim}").as_bytes()).unwrap();
        store.put("reviewer-identity:anna", b"not a tenant key: untouched").unwrap();
        let seeds = dir.path().join("seeds");
        std::fs::create_dir_all(&seeds).unwrap();
        std::fs::write(seeds.join("rickard"), &seed).unwrap();

        let keys = super::TenantKeys::new(endpoint.clone(), store.clone()).with_seeds(Some(seeds.clone()));
        let swept = keys.sweep(&console(&endpoint)).await;
        let asked = revokes.asked.lock().unwrap().clone();
        for (who, key) in [("anna", &anna), ("bob", &bob), ("kim", &kim), ("the seed", &seed)] {
            assert!(asked.contains(&fingerprint_of(key)), "{who}'s key was revoked at gunnar: {asked:?}");
        }
        assert_eq!(store.get("tenant-identity:anna").unwrap(), None, "forgotten once revoked");
        assert_eq!(store.get("tenant-pending:bob").unwrap(), None);
        assert!(!seeds.join("rickard").exists(), "the seed's private half is gone");
        assert!(store.get("tenant-identity:kim").unwrap().is_some(), "kept: gunnar did not revoke it yet");
        assert_eq!(store.get("reviewer-identity:anna").unwrap().as_deref(), Some(&b"not a tenant key: untouched"[..]));
        assert_eq!((swept.revoked, swept.kept), (3, 1), "{swept:?}");
    }

    /// A path, a hidden file, a shell word: none of them is a seed's name.
    #[test]
    fn a_seed_is_looked_up_by_a_plain_name_and_nothing_else() {
        for fine in ["rickard", "p-anna", "a.b_c-9"] {
            assert!(plain_file_name(fine), "{fine}");
        }
        for bad in ["", ".", "..", "../console_key", "a/b", ".hidden", "rick'; rm -rf / #", "a b", "räv"] {
            assert!(!plain_file_name(bad), "{bad:?} would be joined to the seeds directory");
        }
    }
}