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

nordisk/gunnar

crates/gunnar-grpc/src/identity.rs

//! **Gunnar's identity host** (IDENTITY-HOST, 2026-10-27): the ONE
//! product-specific part of sign-up, login, invites, sessions and step-up.
//! The flows are edda's `nornir_identity::Identity`; Gunnar answers only:
//!
//! * **site**: Root (gunnar.rs sells) or Private (an estate), data from the config;
//! * **store**: people and passkeys are Gunnar's principals and web credentials
//!   in the control plane (one table, the one every other verb reads); the
//!   core's sessions, invites, floors and the sign-up state live in
//!   [`IdentityTables`], persisted by the server beside the control state;
//! * **roles**: the server-wide grant ([`SERVER_ADMIN`], [`SERVER_WRITE`]);
//! * **audit**: the audit target, and the server's ledger sink;
//! * **mailer**: the server's postman, or [`nornir_identity::NoMail`] (airgap).

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

use gunnar_proto::{GrantLevel, PrincipalKind};
use nornir_identity::{
    Credential, Event, IdentityHost, Invite, Mailer, Revocations, Roles, Session, Site, StoreError,
    User, UserState, UserStore,
};
use serde::{Deserialize, Serialize};

use crate::auth::Access;
use crate::seam::ControlPlane;

/// The group a server-wide Admin grant reads as.
pub const SERVER_ADMIN: &str = "server-admin";
/// The group a server-wide Write grant reads as.
pub const SERVER_WRITE: &str = "server-write";

/// What a passkey the core registered is labelled in the console.
pub const PASSKEY_LABEL: &str = "passkey";

/// The stable code of a twin's (or browse replica's) refusal of an identity write (RA5-10).
pub const READ_ONLY: &str = "identity-read-only";

/// A session, as Gunnar persists it (the core's [`Session`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionRow {
    pub id: String,
    pub user: String,
    pub auth_time: i64,
    pub expires: i64,
    #[serde(default)]
    pub stepped_up_at: Option<i64>,
}

impl From<&Session> for SessionRow {
    fn from(s: &Session) -> Self {
        SessionRow {
            id: s.id.clone(),
            user: s.user.clone(),
            auth_time: s.auth_time,
            expires: s.expires,
            stepped_up_at: s.stepped_up_at,
        }
    }
}

impl From<&SessionRow> for Session {
    fn from(s: &SessionRow) -> Self {
        Session {
            id: s.id.clone(),
            user: s.user.clone(),
            auth_time: s.auth_time,
            expires: s.expires,
            stepped_up_at: s.stepped_up_at,
        }
    }
}

/// Where one person stands, beside their principal.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PersonRow {
    #[serde(default)]
    pub email: Option<String>,
    /// `pending`, `active` or `deactivated`.
    pub state: String,
}

fn state_name(s: UserState) -> &'static str {
    match s {
        UserState::Pending => "pending",
        UserState::Active => "active",
        UserState::Deactivated => "deactivated",
    }
}

fn state_of(name: &str) -> UserState {
    match name {
        "pending" => UserState::Pending,
        "deactivated" => UserState::Deactivated,
        _ => UserState::Active,
    }
}

/// **The identity core's own records in Gunnar**: sessions (hashes only),
/// invites (hashes only), the revocation floors and the people's sign-up state.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityTables {
    #[serde(default)]
    pub sessions: Vec<SessionRow>,
    #[serde(default)]
    pub invites: Vec<Invite>,
    #[serde(default)]
    pub revocations: Revocations,
    #[serde(default)]
    pub people: BTreeMap<String, PersonRow>,
    /// **The sessions company sign-on admitted** (their ids, the token's hash):
    /// an employee of an account that requires its identity provider acts only
    /// from one of these (`require_sso`, [`GunnarStore::sso_admitted`]).
    #[serde(default)]
    pub sso_admitted: Vec<String>,
    /// **The same admissions by the session's fingerprint** (`session:<hash start>`,
    /// [`crate::auth::session_fingerprint`]) → its id: a credential mint sees a caller's
    /// fingerprint, never the token, and reads this persisted record, as the gate reads
    /// [`Self::sso_admitted`] (RA5-38: one record, written in one change).
    #[serde(default)]
    pub sso_admitted_by_fingerprint: BTreeMap<String, String>,
}

/// How the server keeps [`IdentityTables`]: handed the whole JSON after every change.
pub type Persist = Box<dyn Fn(&str) -> Result<(), String> + Send + Sync>;

/// A clock, Unix milliseconds.
pub type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;

/// The system clock.
pub fn system_clock() -> Clock {
    Arc::new(crate::bus::now_unix_ms)
}

fn store_err(e: impl std::fmt::Display) -> StoreError {
    StoreError(e.to_string())
}

/// **Gunnar's [`UserStore`]**: the control plane's principals and passkeys,
/// and [`IdentityTables`].
pub struct GunnarStore {
    plane: Arc<dyn ControlPlane>,
    tables: Mutex<IdentityTables>,
    persist: Option<Persist>,
    clock: Clock,
}

impl std::fmt::Debug for GunnarStore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("GunnarStore").finish_non_exhaustive()
    }
}

impl GunnarStore {
    /// Over `plane`, starting from `tables` (the persisted JSON, read by the server).
    pub fn new(
        plane: Arc<dyn ControlPlane>,
        tables: IdentityTables,
        persist: Option<Persist>,
        clock: Clock,
    ) -> Self {
        GunnarStore {
            plane,
            tables: Mutex::new(tables),
            persist,
            clock,
        }
    }

    /// Read persisted tables; an empty text is no tables.
    pub fn read_tables(json: &str) -> Result<IdentityTables, String> {
        if json.trim().is_empty() {
            return Ok(IdentityTables::default());
        }
        serde_json::from_str(json).map_err(|e| format!("the identity tables do not read: {e}"))
    }

    fn lock(&self) -> std::sync::MutexGuard<'_, IdentityTables> {
        self.tables.lock().unwrap_or_else(|p| p.into_inner())
    }

    /// Change the tables and persist them, under one lock.
    /// **This box takes identity writes** (RA5-10): a twin or a browse replica
    /// does not. The core is mounted outside the gate, so the gate's browse/twin
    /// refusal is asked here, before every write verb: refused by name, nothing
    /// written, on the control plane or in `identity.json`.
    fn writable(&self) -> Result<(), StoreError> {
        match self.plane.frozen() {
            None => Ok(()),
            Some(why) => Err(StoreError(format!(
                "[{READ_ONLY}] {why}: it takes no identity write (no login, redeem, passkey, invite or \
                 logout here); its identity records follow the primary's. Do it at the primary"
            ))),
        }
    }

    /// **The tables, as the primary's document has them now** (a twin following
    /// its main, RA5-10): replaces what this box loaded at its boot.
    pub fn reload(&self, tables: IdentityTables) {
        *self.lock() = tables;
    }

    /// RA5-36: the change is made on a copy, persisted, and only then swapped in, so a failed
    /// write changes nothing (memory and `identity.json` never disagree).
    fn change<R>(&self, f: impl FnOnce(&mut IdentityTables) -> R) -> Result<R, StoreError> {
        self.writable()?;
        let mut tables = self.lock();
        let mut next = tables.clone();
        let r = f(&mut next);
        if let Some(persist) = &self.persist {
            let json = serde_json::to_string(&next).map_err(store_err)?;
            persist(&json).map_err(StoreError)?;
        }
        *tables = next;
        Ok(r)
    }

    /// A copy of the tables (tests, the server's backup).
    pub fn tables(&self) -> IdentityTables {
        self.lock().clone()
    }

    /// **Company sign-on admitted this session** (`Sso.Finish` vouched for its
    /// person, or `Sso.SecondFactor` took the passkey's session after the
    /// IdP): kept by the token's hash, beside the session it names; an admitted
    /// session that ended is forgotten on the next admission.
    pub fn admit_sso_session(&self, token: &str) -> Result<(), StoreError> {
        let id = nornir_identity_session::invites::hash_secret(token);
        let fingerprint = crate::auth::session_fingerprint(token);
        self.change(|t| {
            let live: std::collections::BTreeSet<String> =
                t.sessions.iter().map(|s| s.id.clone()).collect();
            let mut kept: Vec<String> = t
                .sso_admitted
                .iter()
                .filter(|s| live.contains(s.as_str()) && **s != id)
                .cloned()
                .collect();
            kept.push(id.clone());
            t.sso_admitted = kept;
            t.sso_admitted_by_fingerprint
                .retain(|fp, sid| live.contains(sid.as_str()) && *fp != fingerprint);
            t.sso_admitted_by_fingerprint
                .insert(fingerprint.clone(), id.clone());
        })
    }

    /// **Did company sign-on admit the live core session whose fingerprint is
    /// `fingerprint`** (RA5-38: the persisted record, the one the gate reads by token)?
    pub fn sso_admitted_fingerprint(&self, fingerprint: &str) -> bool {
        let now = (self.clock)().div_euclid(1000);
        let t = self.lock();
        t.sso_admitted_by_fingerprint
            .get(fingerprint)
            .is_some_and(|id| {
                t.sso_admitted.contains(id)
                    && t.sessions.iter().any(|s| s.id == *id && s.expires > now)
            })
    }

    /// **When the session of `token` last stepped up** (a user-verified touch),
    /// Unix seconds as the core keeps it; `None` for a session that never did,
    /// or one that is gone.
    pub fn stepped_up_at(&self, token: &str) -> Option<i64> {
        let id = nornir_identity_session::invites::hash_secret(token);
        self.lock()
            .sessions
            .iter()
            .find(|s| s.id == id)
            .and_then(|s| s.stepped_up_at)
    }

    /// Did company sign-on admit the session of `token`?
    pub fn sso_admitted(&self, token: &str) -> bool {
        let id = nornir_identity_session::invites::hash_secret(token);
        self.lock().sso_admitted.contains(&id)
    }

    fn groups_of(&self, id: &str) -> Result<Vec<String>, StoreError> {
        if self
            .plane
            .may(id, &Access::Server, GrantLevel::Admin)
            .map_err(store_err)?
        {
            return Ok(vec![SERVER_ADMIN.to_owned()]);
        }
        if self
            .plane
            .may(id, &Access::Server, GrantLevel::Write)
            .map_err(store_err)?
        {
            return Ok(vec![SERVER_WRITE.to_owned()]);
        }
        Ok(Vec::new())
    }

    /// **Where the company directory stands on an employee** (SSO): `None`
    /// for no row (not a directory person); `Some(true)` active — the IdP
    /// vouches for them, so they are an active person to the core even before
    /// a passkey; `Some(false)` deactivated or deleted (SCIM, the CSV import):
    /// no session of theirs stands and no login of theirs mints one, while the
    /// directory says so.
    fn directory_active(&self, id: &str) -> Result<Option<bool>, StoreError> {
        let Some(plane) = self.plane.sso() else {
            return Ok(None);
        };
        let Some(account) = self.plane.employer_of(id).map_err(store_err)? else {
            return Ok(None);
        };
        Ok(plane
            .directory(&account)
            .map_err(store_err)?
            .iter()
            .find(|row| row.user.principal_id == id)
            .map(|row| !row.deleted && row.user.active))
    }

    fn person(&self, p: gunnar_proto::Principal) -> Result<User, StoreError> {
        let row = self.lock().people.get(&p.id).cloned();
        let directory = self.directory_active(&p.id)?;
        let state = match &row {
            _ if directory == Some(false) => UserState::Deactivated,
            Some(row) if directory == Some(true) && state_of(&row.state) == UserState::Pending => {
                UserState::Active
            }
            Some(row) => state_of(&row.state),
            None if directory == Some(true) => UserState::Active,
            // A principal made outside the core: active once it holds a passkey.
            None if !self
                .plane
                .credentials(Some(&p.id))
                .map_err(store_err)?
                .is_empty() =>
            {
                UserState::Active
            }
            None => UserState::Pending,
        };
        Ok(User {
            groups: self.groups_of(&p.id)?,
            email: row.and_then(|r| r.email),
            display: p.name,
            state,
            created: p.created_at_unix_ms.div_euclid(1000),
            id: p.id,
        })
    }

    /// **The operator's first invite, as the core reads it**: a fed invite
    /// (`sha256:<hex>` of the installer's token) onto the operator.
    fn first_invite(&self) -> Result<Option<Invite>, StoreError> {
        Ok(self
            .plane
            .first_invite()
            .map_err(store_err)?
            .map(|f| Invite {
                id: nornir_identity::fed_invite_id(&f.secret_hash),
                display: String::new(),
                groups: Vec::new(),
                created: f.created_at_unix_ms.div_euclid(1000),
                expires: f.expires_at_unix_ms.div_euclid(1000),
                created_by: format!(
                    "{}{}:installer",
                    nornir_identity::PERSON_INVITE,
                    f.principal_id
                ),
                used_by: None,
                used_at: None,
                revoked: false,
            }))
    }

    fn credential_out(c: crate::seam::StoredCredential) -> Credential {
        Credential {
            id: c.credential.credential_id,
            user: c.credential.principal_id,
            public_key: c.public_key,
            sign_count: c.credential.sign_count,
            created: c.credential.created_at_unix_ms.div_euclid(1000),
        }
    }
}

fn is_person(p: &gunnar_proto::Principal) -> bool {
    p.kind == PrincipalKind::User as i32
}

/// A machine that holds a web credential (an ops kit, approved by a person)
/// logs in through the core like a person.
fn is_machine(p: &gunnar_proto::Principal) -> bool {
    p.kind == PrincipalKind::Machine as i32
}

impl UserStore for GunnarStore {
    fn user(&self, id: &str) -> Result<Option<User>, StoreError> {
        match self.plane.principal(id).map_err(store_err)? {
            Some(p) if is_person(&p) || is_machine(&p) => self.person(p).map(Some),
            _ => Ok(None),
        }
    }

    fn user_by_email(&self, email: &str) -> Result<Option<User>, StoreError> {
        let id = self
            .lock()
            .people
            .iter()
            .find(|(_, r)| r.email.as_deref() == Some(email))
            .map(|(id, _)| id.clone());
        match id {
            Some(id) => self.user(&id),
            None => Ok(None),
        }
    }

    fn users(&self) -> Result<Vec<User>, StoreError> {
        let all = self.plane.list_principals().map_err(store_err)?;
        all.into_iter()
            .filter(is_person)
            .map(|p| self.person(p))
            .collect()
    }

    fn put_user(&self, user: &User) -> Result<(), StoreError> {
        self.writable()?;
        match self.plane.principal(&user.id).map_err(store_err)? {
            None => {
                let made = self
                    .plane
                    .add_principal(&user.id, &user.display, PrincipalKind::User);
                if let Err(crate::seam::ControlError::AlreadyExists(_)) = made {
                    // The name is another principal's: keep the person, tell them apart.
                    let suffix: String = user
                        .id
                        .chars()
                        .rev()
                        .take(6)
                        .collect::<Vec<_>>()
                        .into_iter()
                        .rev()
                        .collect();
                    let name = format!("{}-{suffix}", user.display);
                    self.plane
                        .add_principal(&user.id, &name, PrincipalKind::User)
                        .map_err(store_err)?;
                } else {
                    made.map_err(store_err)?;
                }
            }
            Some(p) if !is_person(&p) => {
                return Err(StoreError(format!("principal {} is not a person", user.id)));
            }
            Some(p) if p.name != user.display => {
                self.plane
                    .rename_principal(&user.id, &user.display)
                    .map_err(store_err)?;
            }
            Some(_) => {}
        }
        let want = if user.groups.iter().any(|g| g == SERVER_ADMIN) {
            Some(GrantLevel::Admin)
        } else if user.groups.iter().any(|g| g == SERVER_WRITE) {
            Some(GrantLevel::Write)
        } else {
            None
        };
        if let Some(level) = want {
            if !self
                .plane
                .may(&user.id, &Access::Server, level)
                .map_err(store_err)?
            {
                self.plane
                    .set_server_level(&user.id, level)
                    .map_err(store_err)?;
            }
        }
        let row = PersonRow {
            email: user.email.clone(),
            state: state_name(user.state).to_owned(),
        };
        self.change(|t| {
            t.people.insert(user.id.clone(), row);
        })
    }

    fn credential(&self, id: &[u8]) -> Result<Option<Credential>, StoreError> {
        Ok(self
            .plane
            .credential(id)
            .map_err(store_err)?
            .map(Self::credential_out))
    }

    fn credentials_of(&self, user: &str) -> Result<Vec<Credential>, StoreError> {
        let rows = self.plane.credentials(Some(user)).map_err(store_err)?;
        let mut out = Vec::with_capacity(rows.len());
        for row in rows {
            if let Some(c) = self
                .plane
                .credential(&row.credential_id)
                .map_err(store_err)?
            {
                out.push(Self::credential_out(c));
            }
        }
        Ok(out)
    }

    fn put_credential(&self, c: &Credential) -> Result<(), StoreError> {
        self.writable()?;
        match self.plane.credential(&c.id).map_err(store_err)? {
            None => {
                self.plane
                    .add_credential(&c.user, &c.id, &c.public_key, PASSKEY_LABEL, c.sign_count)
                    .map_err(store_err)?;
            }
            Some(_) => {
                self.plane
                    .record_passkey_use(&c.id, c.sign_count, (self.clock)())
                    .map_err(store_err)?;
            }
        }
        Ok(())
    }

    fn session(&self, id: &str) -> Result<Option<Session>, StoreError> {
        Ok(self
            .lock()
            .sessions
            .iter()
            .find(|s| s.id == id)
            .map(Session::from))
    }

    fn put_session(&self, session: &Session) -> Result<(), StoreError> {
        let now = (self.clock)().div_euclid(1000);
        self.change(|t| {
            t.sessions.retain(|s| s.id != session.id && s.expires > now);
            t.sessions.push(SessionRow::from(session));
        })
    }

    fn delete_session(&self, id: &str) -> Result<(), StoreError> {
        self.change(|t| t.sessions.retain(|s| s.id != id))
    }

    fn invites(&self) -> Result<Vec<Invite>, StoreError> {
        let first = self.first_invite()?;
        let mut all = self.lock().invites.clone();
        all.extend(first);
        Ok(all)
    }

    /// The core's invites, and the operator's first invite beside them (the
    /// plane's record, read as a fed invite): kept at the plane while open,
    /// dropped there once the core spent or withdrew it.
    fn update_invites(&self, change: &mut dyn FnMut(&mut Vec<Invite>)) -> Result<(), StoreError> {
        let first = self.first_invite()?;
        let now = (self.clock)().div_euclid(1000);
        let mut dropped = false;
        self.change(|t| {
            let mut all = t.invites.clone();
            all.extend(first.clone());
            change(&mut all);
            if let Some(first) = &first {
                match all.iter().position(|i| i.id == first.id) {
                    Some(at) => {
                        let fed = all.remove(at);
                        dropped = !fed.open(now);
                    }
                    None => dropped = true,
                }
            }
            t.invites = all;
        })?;
        if dropped {
            self.plane.drop_first_invite().map_err(store_err)?;
        }
        Ok(())
    }

    fn revocations(&self) -> Result<Revocations, StoreError> {
        Ok(self.lock().revocations.clone())
    }

    fn update_revocations(
        &self,
        change: &mut dyn FnMut(&mut Revocations),
    ) -> Result<(), StoreError> {
        self.change(|t| change(&mut t.revocations))
    }
}

/// Where the core's audit events go beside the audit target (the server's ledger).
pub type AuditSink = Arc<dyn Fn(&Event) + Send + Sync>;

/// **`impl IdentityHost for GunnarHost`**: the whole of Gunnar's identity part.
pub struct GunnarHost {
    site: Site,
    store: GunnarStore,
    mailer: Arc<dyn Mailer>,
    sink: Option<AuditSink>,
    clock: Clock,
}

impl std::fmt::Debug for GunnarHost {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("GunnarHost")
            .field("site", &self.site)
            .finish_non_exhaustive()
    }
}

impl GunnarHost {
    /// A host of `site` over `store`, mailing through `mailer`.
    pub fn new(site: Site, store: GunnarStore, mailer: Arc<dyn Mailer>, clock: Clock) -> Self {
        GunnarHost {
            site,
            store,
            mailer,
            sink: None,
            clock,
        }
    }

    /// Also tell `sink` every event.
    pub fn with_audit_sink(mut self, sink: AuditSink) -> Self {
        self.sink = Some(sink);
        self
    }

    /// The store.
    pub fn gunnar_store(&self) -> &GunnarStore {
        &self.store
    }

    /// **The person's account sees its sign-ins and invites** (the owner's
    /// enterprise list, 2026-10-05): what `Access.WebLogin` and
    /// `Access.CreateInvite` wrote into the account's audit log before the
    /// core owned them. A login is the account of the person who logged in; an
    /// invite, the account of whoever minted it. A refused login is the account
    /// of the person the passkey named (nornir-identity 0.2.1's
    /// `LoginRefused.user`); a passkey nobody here holds names no account.
    fn account_audit(&self, event: &Event) {
        let refused = matches!(event, Event::LoginRefused { .. });
        let (actor, kind, target, detail) = match event {
            // A session company sign-on vouched for is the SSO service's to
            // log (`sso.login`, target `sso`); this one is a passkey's.
            Event::LoggedIn { .. } if VOUCHING.with(std::cell::Cell::get) => return,
            Event::LoggedIn {
                user,
                user_verified,
                ..
            } => (
                user,
                "login",
                "passkey",
                if *user_verified {
                    "passkey, user-verified"
                } else {
                    "passkey"
                }
                .to_owned(),
            ),
            Event::InviteMinted { invite, by, .. } => {
                (by, "invite.created", "invite", format!("invite {invite}"))
            }
            Event::InviteRevoked { invite, by } => {
                (by, "invite.revoked", "invite", format!("invite {invite}"))
            }
            Event::LoginRefused {
                code,
                user: Some(user),
            } => (
                user,
                "login",
                "passkey",
                format!("passkey: refused ({code:?})"),
            ),
            _ => return,
        };
        let Some(plane) = self.store.plane.enterprise() else {
            return;
        };
        let Ok(Some(account)) = plane.account_of_principal(actor) else {
            return;
        };
        let record = crate::enterprise::AccountAuditEvent {
            kind: kind.to_owned(),
            actor: actor.clone(),
            actor_machine: false,
            target: target.to_owned(),
            detail,
            refused,
        };
        if let Err(err) = plane.account_audit_record(&account, &record) {
            tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "account_audit.not_written", account = %account, kind, reason = %err, "an identity event was not written to its account's audit log");
        }
    }
}

impl IdentityHost for GunnarHost {
    fn site(&self) -> Site {
        self.site
    }

    fn store(&self) -> &dyn UserStore {
        &self.store
    }

    fn roles(&self, user: &User) -> Roles {
        let admin = user.groups.iter().any(|g| g == SERVER_ADMIN);
        Roles {
            admin,
            names: user.groups.clone(),
        }
    }

    fn audit(&self, event: Event) {
        tracing::info!(target: crate::auth::AUDIT_TARGET, event = "identity", detail = ?event, "identity");
        self.account_audit(&event);
        if let Some(sink) = &self.sink {
            sink(&event);
        }
    }

    fn mailer(&self) -> &dyn Mailer {
        &*self.mailer
    }

    /// **An account's admin enrols the account's own people** (the rule
    /// `Accounts.EnrollEmployee` keeps): `by` holds Admin over the account
    /// that enrolled `person` as an employee.
    fn may_enrol(&self, by: &User, person: &User) -> bool {
        let plane = &self.store.plane;
        match plane.employer_of(&person.id) {
            Ok(Some(account)) => plane
                .may(&by.id, &Access::Namespace(account), GrantLevel::Admin)
                .unwrap_or(false),
            _ => false,
        }
    }

    fn now_ms(&self) -> i64 {
        (self.clock)()
    }
}

/// The identity core Gunnar serves.
pub type GunnarIdentity = nornir_identity::Identity<GunnarHost>;

thread_local! {
    /// Set while [`vouched_session`] runs on this thread (the core's call is
    /// synchronous): its login event is company sign-on's, not a passkey's.
    static VOUCHING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}

/// **A session company sign-on vouched for** ([`nornir_identity::Identity::vouched_session`]):
/// the SSO service's sign-in, which logs itself to the account (`sso.login`), so
/// the core's login event is not logged a second time as a passkey's.
pub fn vouched_session(
    identity: &GunnarIdentity,
    user_id: &str,
    user_verified: bool,
) -> Result<nornir_identity::LoggedIn, nornir_identity::Refused> {
    VOUCHING.with(|v| v.set(true));
    let made = identity.vouched_session(user_id, user_verified);
    VOUCHING.with(|v| v.set(false));
    made
}

/// A core refusal as a Gunnar verb answers it: the IdentityService's own
/// status (its code in the message and in `nornir-identity-refusal`).
pub fn status_of(refused: nornir_identity::Refused) -> tonic::Status {
    nornir_identity::grpc::status(refused)
}