Skip to main content

gunnar_core/
auth.rs

1//! The identity table and the ACL (the ACL model).
2//!
3//! **gunnar owns its own table.** No OS users, no `~/.ssh/authorized_keys`, no
4//! `/etc/passwd`, no home directory. The server has to run as PID 1 in a
5//! distroless image where none of those exist (the no-OS server rule). Identity comes from
6//! the SSH key and nothing else; the SSH *username* is decorative, so
7//! `git@host:ns/repo.git` works and `git` means nothing.
8//!
9//! The auth flow the SSH listener runs is three calls:
10//!
11//! ```text
12//! russh auth_publickey  →  Fingerprint::of_key(offered)
13//!                       →  AuthTable::principal_by_fingerprint(&fp)   // who
14//!                       →  RepoCatalog::open(&sandboxed_path)         // what
15//!                       →  AuthTable::may(&principal.id, &repo, level) // may they
16//! ```
17//!
18//! `upload-pack` needs [`Level::Read`], `receive-pack` needs [`Level::Write`].
19//!
20//! # The one caller that never authenticates
21//!
22//! A **public** repository is readable with no key at all. Such a caller is
23//! served as [`Principal::anonymous`], which is not in this table, cannot be
24//! put in it, and holds neither key nor grant. The question *may it?* still
25//! goes to [`AuthTable::may`], exactly as it does for everybody else. Rule 0
26//! there is the whole of what anonymous can ever be allowed: `Read`, on a
27//! `Public` repository, and nothing anywhere else. See
28//! [`ANONYMOUS_PRINCIPAL_ID`](crate::model::ANONYMOUS_PRINCIPAL_ID).
29
30use std::sync::Arc;
31
32use crate::control::ControlStore;
33use crate::doc::{impl_doc, DocRef, ShardedMap};
34use crate::error::{Error, Result};
35use crate::key_attestation::AttestingKey;
36use crate::model::{
37    now_ms, AccessToken, Grant, Level, Principal, PrincipalKind, PublicKey, Repo, Scope, UnixMillis,
38};
39
40mod sso_table;
41pub use sso_table::{
42    SsoImportOp, SsoImportRow, SsoLogin, SsoLoginDone, SsoProvision, SsoUserPatch,
43};
44
45/// **The name the identity table is stored under** in the control store.
46///
47/// It still reads `refs/gunnar/auth`. Until 2026-08-09 that WAS a git reference
48/// (the certificate-identity plan); it is now a key in redb, and the string was kept because it is
49/// what every error message, log line and runbook calls this document.
50pub const AUTH_REF: &str = "refs/gunnar/auth";
51
52/// Id of the principal [`AuthTable::bootstrap_admin`] installs when no name is
53/// given (`--admin-key` on the command line).
54pub const BOOTSTRAP_ADMIN_ID: &str = "admin";
55
56/// **Clans** (owner, 2026-10-05): ordinary accounts grouped, each giving the
57/// clan part of its own room; the clan's admin administers what is made in it.
58mod clan_table;
59pub use clan_table::{
60    AccountRoom, Clan, ClanInvite, ClanMember, ClanRefusal, ClanRepoOffer, ClanRole,
61};
62
63/// Every fingerprint gunnar computes or accepts carries this prefix.
64pub const SHA256_PREFIX: &str = "SHA256:";
65
66/// The `algo` of a registered **quantum-safe encryption key** (crypt-pq): an
67/// `age1pq1…` recipient (age's hybrid ML-KEM-768 + X25519), registered beside
68/// a person's SSH keys so `Repos.RecipientKeys` can hand it to a member
69/// wrapping a quantum-safe repository's key ring. It is never a login: no SSH
70/// client can present a key with its fingerprint, and it cannot sign, so it is
71/// never an attesting key either.
72pub const PQ_RECIPIENT_ALGO: &str = "mlkem768x25519";
73
74/// The fingerprint of an `age1pq1…` line — SHA-256 over its 1216 key bytes,
75/// in the one fingerprint space — or `None` when the line is not one.
76pub fn pq_recipient_fingerprint(line: &str) -> Option<Fingerprint> {
77    gunnar_crypt::members::pq_public_key_bytes(line.trim()).map(|bytes| fingerprint_of_blob(&bytes))
78}
79
80/// **The one fingerprint: SHA-256 over the SSH wire-format public key
81/// blob, rendered `SHA256:<base64>` exactly as `ssh-keygen -lf` prints it.**
82///
83/// `ssh_wire_blob` is the `string` an SSH public key is encoded as on the wire:
84/// `ssh-ed25519` followed by its length-prefixed key material. Same bytes as
85/// the base64 payload in the middle of an `authorized_keys` line. Both
86/// `ssh-key` versions in gunnar's graph produce it with `PublicKey::to_bytes()`.
87///
88/// # Why bytes and not a type
89///
90/// `gunnar-ssh` links `ssh-key 0.7.0-rc.10`, because `russh 0.61.2` pins that
91/// version exactly and `Handler::auth_publickey` hands over *its*
92/// `PublicKey`. This crate links the workspace's `ssh-key 0.6.7`. They are
93/// semver-incompatible and both are in the graph, so a key that arrives through
94/// russh can never be passed to a 0.6 API. The workspace is deliberately
95/// **not** bumped to a release candidate to paper over that.
96///
97/// A blob crosses the line. Define the fingerprint over the blob and one
98/// fingerprint serves both crates: `gunnar-ssh` computes the same string the
99/// [`AuthTable`] stores, and `tests/fingerprint.rs` pins it against literal
100/// `ssh-keygen -lf` output rather than against gunnar's own arithmetic.
101///
102/// Any bytes at all are accepted: this is a hash, not a parser, and refusing to
103/// hash a malformed blob would only turn "this key is not registered" into an
104/// error path with a second answer in it.
105pub fn fingerprint_of_blob(ssh_wire_blob: &[u8]) -> Fingerprint {
106    use base64ct::Encoding as _;
107    use sha2::Digest as _;
108
109    let digest = sha2::Sha256::digest(ssh_wire_blob);
110    Fingerprint(format!(
111        "{SHA256_PREFIX}{}",
112        base64ct::Base64Unpadded::encode_string(&digest)
113    ))
114}
115
116/// The stored form of an access token secret: SHA-256, lowercase hex.
117///
118/// Hex rather than the `SHA256:<base64>` spelling [`Fingerprint`] uses, and the
119/// difference is on purpose: a fingerprint is meant to be compared by eye
120/// against what `ssh-keygen -lf` prints on somebody's laptop, and a token hash
121/// is never compared against anything outside gunnar. Sharing the spelling
122/// would invite the two to be confused in a log, where one identifies a key and
123/// the other is all that is left of a password.
124///
125/// **Length is not a secret and neither is this value.** It is what
126/// `token list` prints. What must never be printed is the input.
127///
128/// Public because the HTTP front end needs the same value to record a use and
129/// to name the token in a log line, and computing it there would be a second
130/// definition of the stored form of a credential (one implementation).
131pub fn token_hash(secret: &str) -> String {
132    use sha2::Digest as _;
133
134    hex::encode(sha2::Sha256::digest(secret.as_bytes()))
135}
136
137/// How fresh a token's recorded `last_used` may be before another
138/// authentication skips rewriting it — see [`AuthTable::touch_token`], where
139/// the measurement that sets this is written down. One minute: coarse enough
140/// that the shared-document CAS leaves the request path, fine enough that
141/// `token list` still answers "is this credential alive" honestly.
142pub const LAST_USED_GRANULARITY: UnixMillis = 60_000;
143
144/// One user of a bulk import — see [`AuthTable::bulk_add_users`].
145#[derive(Clone, Debug)]
146pub struct BulkUser {
147    /// The principal to create.
148    pub principal: Principal,
149    /// [`token_hash`] of an access-token secret, when the user gets one. The
150    /// secret itself stays with the caller, as it does for
151    /// [`AuthTable::register_token`].
152    pub token_hash: Option<String>,
153    /// The label stored beside the token; ignored without `token_hash`.
154    pub token_label: String,
155    /// An OpenSSH public key line to register for the user, when they get one.
156    /// Parsed and fingerprinted here exactly as [`AuthTable::register_key`]
157    /// does, so a bulk-imported key and a singly-registered one are the same
158    /// record.
159    pub openssh_public_key: Option<String>,
160    /// Grants to install, replacing nothing — the principal is new.
161    pub grants: Vec<Grant>,
162}
163
164/// Mint a token secret: 256 bits of OS entropy, `gunnar_pat_<base64url>`.
165///
166/// Separate from [`AuthTable::register_token`], which is handed a secret and
167/// stores only its hash — the same split as [`mint_ed25519`](crate::mint_ed25519)
168/// and [`AuthTable::register_key`]. Minting and storing are different acts, and
169/// keeping them apart is what makes it possible to state that the table never
170/// holds a usable credential.
171///
172/// **The prefix is not decoration.** It is what makes a leaked token findable:
173/// a bare base64 blob in a log or a commit is indistinguishable from any other
174/// blob, and every forge that has been through this ended up prefixing for the
175/// same reason. It costs eleven bytes.
176pub fn mint_token_secret() -> String {
177    use base64ct::Encoding as _;
178    use rand::Rng as _;
179
180    let mut bytes = [0u8; 32];
181    // The OS generator panics rather than returning short: a server that
182    // could not get entropy must not mint a guessable credential.
183    crate::os_rng().fill_bytes(&mut bytes);
184    format!(
185        "gunnar_pat_{}",
186        base64ct::Base64UrlUnpadded::encode_string(&bytes)
187    )
188}
189
190/// An OpenSSH `SHA256:<base64>` public key fingerprint.
191///
192/// This is exactly what `ssh-keygen -lf` prints and what OpenSSH shows on an
193/// unknown-host prompt: unpadded base64 of the sha256 of the wire encoding.
194/// gunnar never computes "something like" a fingerprint. An operator has to be
195/// able to compare what gunnar lists against what `ssh-keygen` says on their
196/// laptop, so `tests/fingerprint.rs` pins it to known answers taken from the
197/// real OpenSSH binary.
198///
199/// Every constructor routes through [`fingerprint_of_blob`], so there is one
200/// definition and it is a definition over bytes.
201#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
202pub struct Fingerprint(String);
203
204/// A certificate's DER, from bytes that may be PEM-armoured or already DER.
205///
206/// Lives beside [`fingerprint_of_certificate`] because the two have to agree
207/// about what "the certificate bytes" are. If the caller hashed the armoured
208/// form and this unwrapped it, the same certificate would register under two
209/// fingerprints depending only on which shape somebody happened to hand over.
210pub fn certificate_der(raw: &[u8]) -> Result<Vec<u8>> {
211    use base64ct::Encoding as _;
212
213    let text = String::from_utf8_lossy(raw);
214    const BEGIN: &str = "-----BEGIN CERTIFICATE-----";
215    const END: &str = "-----END CERTIFICATE-----";
216    let Some(start) = text.find(BEGIN) else {
217        // No armour: it is already DER.
218        return Ok(raw.to_vec());
219    };
220    let body = &text[start + BEGIN.len()..];
221    let end = body
222        .find(END)
223        .ok_or_else(|| Error::Key("the PEM has a BEGIN line and no END line".to_owned()))?;
224    let b64: String = body[..end].chars().filter(|c| !c.is_whitespace()).collect();
225    base64ct::Base64::decode_vec(&b64)
226        .map_err(|e| Error::Key(format!("the PEM body is not valid base64: {e}")))
227}
228
229/// The same SHA256 fingerprint, taken over an X.509 certificate's DER.
230///
231/// # Why mTLS reuses this type rather than growing its own
232///
233/// A `Fingerprint` is the whole of what gunnar knows about a credential: the
234/// `AuthTable` maps one to a principal, and every `Grant` check downstream works
235/// on the principal. So a second way to *arrive* at a fingerprint is the entire
236/// mTLS change — no new identity, no second grant path, nothing downstream aware
237/// there are now two kinds of credential (the mTLS plan).
238///
239/// # Over the DER, not the public key
240///
241/// The whole certificate, so that re-issuing the same subject with the same key
242/// under a new validity window produces a **different** fingerprint. That is the
243/// conservative direction: a re-issued certificate has to be registered again,
244/// rather than silently inheriting the grants of the one it replaced. A
245/// deployment that wants key continuity across renewal wants a different scheme
246/// than a fingerprint, and should not get it by accident here.
247///
248/// Any bytes are accepted, for the same reason [`fingerprint_of_blob`] accepts
249/// any: this is a hash, not a parser, and refusing to hash a malformed
250/// certificate would only turn "not registered" into a second answer.
251pub fn fingerprint_of_certificate(der: &[u8]) -> Fingerprint {
252    use base64ct::Encoding as _;
253    use sha2::Digest as _;
254
255    let digest = sha2::Sha256::digest(der);
256    Fingerprint(format!(
257        "{SHA256_PREFIX}{}",
258        base64ct::Base64Unpadded::encode_string(&digest)
259    ))
260}
261
262impl Fingerprint {
263    /// Fingerprint a parsed key.
264    ///
265    /// Convenience over [`fingerprint_of_blob`] for the crates that link *this*
266    /// `ssh-key`. `gunnar-ssh` cannot use it (see [`fingerprint_of_blob`]) and
267    /// calls that function with `russh`'s key blob instead.
268    pub fn of_key(key: &ssh_key::PublicKey) -> Self {
269        // Encoding a key that was already parsed cannot fail: `to_bytes` only
270        // writes the decoded `KeyData` into a fresh `Vec`.
271        let blob = key
272            .to_bytes()
273            .expect("a parsed public key always re-encodes to its own wire form");
274        fingerprint_of_blob(&blob)
275    }
276
277    /// Parse an `authorized_keys`-style line and fingerprint it.
278    ///
279    /// Accepts what OpenSSH writes: `<algo> <base64> [comment]`.
280    pub fn of_openssh(line: &str) -> Result<Self> {
281        Ok(Self::of_key(&parse_public_key(line)?))
282    }
283
284    /// Accept a fingerprint that arrived as text: config file, gRPC request,
285    /// an operator's terminal.
286    ///
287    /// Validated rather than trusted: `SHA256:` plus 43 base64 characters. A
288    /// malformed fingerprint that gets stored anyway is a key that can never be
289    /// matched and never be revoked.
290    pub fn parse(text: &str) -> Result<Self> {
291        let body = text.strip_prefix(SHA256_PREFIX).ok_or_else(|| {
292            Error::BadFingerprint(format!("{text:?}: missing the {SHA256_PREFIX} prefix"))
293        })?;
294        if body.len() != 43 {
295            return Err(Error::BadFingerprint(format!(
296                "{text:?}: expected 43 base64 characters after the prefix, got {}",
297                body.len()
298            )));
299        }
300        if !body
301            .bytes()
302            .all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
303        {
304            return Err(Error::BadFingerprint(format!(
305                "{text:?}: not unpadded base64"
306            )));
307        }
308        Ok(Fingerprint(text.to_owned()))
309    }
310
311    /// The `SHA256:...` text.
312    pub fn as_str(&self) -> &str {
313        &self.0
314    }
315}
316
317impl std::fmt::Display for Fingerprint {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.write_str(&self.0)
320    }
321}
322
323impl std::fmt::Debug for Fingerprint {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        write!(f, "Fingerprint({})", self.0)
326    }
327}
328
329impl std::str::FromStr for Fingerprint {
330    type Err = Error;
331    fn from_str(s: &str) -> Result<Self> {
332        Fingerprint::parse(s)
333    }
334}
335
336/// Parse an `authorized_keys` line into a key.
337///
338/// Pure Rust, through `ssh-key`. There is no `ssh-keygen` subprocess anywhere
339/// in gunnar (the no-OS server rule).
340pub fn parse_public_key(line: &str) -> Result<ssh_key::PublicKey> {
341    let line = line.trim();
342    if line.is_empty() {
343        return Err(Error::Key("empty".to_owned()));
344    }
345    ssh_key::PublicKey::from_openssh(line).map_err(Error::from)
346}
347
348/// The identity table as it is stored.
349///
350/// All three maps are sharded (`doc.rs`): at forge scale this document holds
351/// every principal, every public key and every grant on the server, and the
352/// pre-2026-08-03 layout put all of them inline in one blob that was parsed
353/// four times per SSH connection.
354///
355/// **`grants` is keyed by principal**, not a `Vec`. It was a flat list, so
356/// [`AuthTable::may`] — three calls deep on every connection, through
357/// `Control::access_for` — scanned every grant on the server to answer one
358/// question. Keyed, the answer is one shard probe and a walk of that
359/// principal's own handful of grants.
360#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
361#[serde(deny_unknown_fields)]
362struct AuthDoc {
363    #[serde(default)]
364    principals: ShardedMap<Principal>,
365    /// Keyed by fingerprint text, which is what an SSH handshake produces.
366    #[serde(default)]
367    keys: ShardedMap<PublicKey>,
368    /// Keyed by [`Principal::id`]: one principal's grants, sorted.
369    #[serde(default)]
370    grants: ShardedMap<Vec<Grant>>,
371    /// Keyed by [`AccessToken::hash`], so an HTTP request costs one probe and
372    /// never a scan.
373    #[serde(default)]
374    tokens: ShardedMap<AccessToken>,
375    /// **Group membership, keyed by member** (punkt 9): one principal's id maps
376    /// to the sorted ids of the groups it belongs to.
377    ///
378    /// Keyed by member and not by group **for the same reason `grants` is keyed
379    /// by principal**: [`AuthTable::may`] runs on every connection and asks one
380    /// question — *what may this caller reach* — so the walk it needs is "which
381    /// groups is this caller in", which is one shard probe here. The inverse
382    /// question, *who is in this group*, is a listing
383    /// ([`AuthTable::members_of`]) and walks, exactly as `grants(None)` walks:
384    /// a listing is allowed to be O(rows), a connection is not.
385    ///
386    /// A group is itself a principal, so a group id can appear as a *member*
387    /// here too — that is nesting, and [`AuthTable::grant_principals`] resolves
388    /// it transitively and cycle-safely.
389    #[serde(default)]
390    memberships: ShardedMap<Vec<String>>,
391    /// **Whether bootstrap has ever happened, as its own record.**
392    ///
393    /// Not a `ShardedMap`, not derived from any of the four above, and never
394    /// touched by [`AuthTable::remove_principal`] or
395    /// [`AuthTable::revoke_key`]: it is a field beside the identity table, not
396    /// a row in it. That separation is the whole security property. "Bootstrap
397    /// has happened" spelled as `!keys.is_empty()` would mean *deleting every
398    /// key reopens the unauthenticated enrolment window*, which is the first
399    /// thing an attacker who can reach `Access.RemovePrincipal` would try.
400    ///
401    /// It rides in the same document, and therefore in the same
402    /// compare-and-swap on `refs/gunnar/auth`, as the identity it installs.
403    /// That is deliberate and is what makes "exactly one of two concurrent
404    /// enrolments wins" true by construction rather than by a lock: the loser
405    /// re-reads inside [`DocRef::update`] and finds the seal already set. A
406    /// separate reference would need two writes and could be interrupted
407    /// between them.
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    bootstrap: Option<BootstrapRecord>,
410    /// **Accounts that are locked out**, keyed by principal id (see
411    /// [`crate::gate`]).
412    ///
413    /// In this document and not beside it, for the reason `bootstrap` is: the
414    /// gate is read on the same code path as the grants it overrides, and a
415    /// second document would mean a second read, a second cache and a second
416    /// version — three places for the lockout and the ACL to disagree, with the
417    /// permissive answer winning silently (the one-sandbox rule). Here they are one blob and
418    /// one compare-and-swap: a gate and the grants it overrides are never seen
419    /// from different versions of the table.
420    ///
421    /// Sharded like the other maps, because [`AuthTable::may`] probes it on
422    /// **every** connection and a whole-table parse per SSH handshake is the
423    /// cost the sharding removed.
424    #[serde(default)]
425    gates: ShardedMap<crate::gate::AccountGate>,
426    /// **Display name → principal id**, so a name is unique by construction
427    /// and a lookup by name is one probe rather than a walk.
428    ///
429    /// Written in the same compare-and-swap as the `principals` row it points
430    /// at — by [`AuthTable::add_principal`], [`AuthTable::rename_principal`]
431    /// and [`AuthTable::remove_principal`] — so the index and the row are never
432    /// seen from two versions of the table.
433    ///
434    /// **Rows from before this index existed have no entry**, and need none:
435    /// every one of them was created with `name == id`, so a probe of
436    /// `principals` by the name finds it. [`AuthTable::principal_by_name`] and
437    /// the uniqueness check both take that second step, which is what makes
438    /// the index additive — no migration, and a table written by an older
439    /// server reads exactly as it did.
440    #[serde(default)]
441    names: ShardedMap<String>,
442    /// **Employee → the account that enrolled it** (owner, 2026-10-03: "a user
443    /// starts an account. he can choose to enroll new users=employees then it
444    /// becomes an enterprise account").
445    ///
446    /// Written ONLY by [`AuthTable::enroll_employee`], in the same
447    /// compare-and-swap as the principal row it creates, so a principal is an
448    /// employee from the moment it exists or never: there is no writer that
449    /// binds an EXISTING principal to an account ("they might have repos
450    /// already, that would be a risk for the company"). Removed with the
451    /// principal ([`AuthTable::remove_principal`]). An account with at least
452    /// one row here IS an enterprise account; there is no flag to set.
453    #[serde(default)]
454    employers: ShardedMap<String>,
455    /// **Employee → the room it holds out of its account's room** (owner,
456    /// 2026-10-04, "Add users"). Written in the same compare-and-swap as the
457    /// employee it belongs to, and removed with it (the room goes back to the
458    /// account). An employee from before rooms existed has no row and holds
459    /// nothing.
460    #[serde(default)]
461    rooms: ShardedMap<EmployeeRoom>,
462    /// **Account → its SEATS**: places for employees not yet named, each
463    /// holding its room, filled one by one by a seat claim (an invite, later
464    /// an import). Lots, not a counter, because two orders may differ in the
465    /// room per seat.
466    #[serde(default)]
467    seats: ShardedMap<Vec<SeatLot>>,
468    /// **Order → what it granted**, so a seller that retries a grant after a
469    /// timeout is answered with the first one instead of granting twice.
470    #[serde(default)]
471    room_orders: ShardedMap<RoomGrant>,
472    /// **Clans** (owner, 2026-10-05), keyed by the clan's name — which is its
473    /// namespace. Here, beside the grants, for the reason `gates` is: rule 2½
474    /// of [`AuthTable::may`] reads it on the same path, from the same version.
475    /// Not written at all until the first clan exists, so a table with none
476    /// is byte-for-byte what it was before clans.
477    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
478    clans: ShardedMap<clan_table::Clan>,
479    /// **Account → the clans it is in or invited to**, by name: what it gives
480    /// its clans is one probe on the push path (its own room is its cap less
481    /// that), and "my clans" is a probe, not a walk.
482    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
483    clan_index: ShardedMap<Vec<String>>,
484    /// **Web credentials — the people's passkeys** (Stage L, 2026-10-04),
485    /// keyed by the credential id (base64url), which is what a login names.
486    /// In this document for the reason `keys` are: a passkey and the principal
487    /// it logs in as are one compare-and-swap, it goes with the principal
488    /// ([`AuthTable::remove_principal`]), and it reaches the twin with the
489    /// rest of the identity table — a reinstalled front keeps every login.
490    #[serde(default)]
491    credentials: ShardedMap<crate::model::WebCredential>,
492    /// **Invites — pending passkey enrolments** (Stage L), keyed by the
493    /// invite id. Here for the reason `credentials` are: redeeming one and
494    /// storing the passkey it enrols are one compare-and-swap, and an invite
495    /// goes with its principal.
496    #[serde(default)]
497    invites: ShardedMap<crate::model::FirstInviteRow>,
498    /// **Redeemed invites** (2026-10-19), keyed by the secret's hash: so
499    /// `Access.CheckInvite` can say USED rather than UNKNOWN for an invite a
500    /// passkey was registered with, and a console refuses it before a second
501    /// passkey replaces the first on the person's device. Written in the
502    /// redemption's own compare-and-swap, kept [`REDEEMED_INVITE_KEEP_MS`],
503    /// removed with the principal. Written only once it holds a row, as the
504    /// SSO maps are: an older gunnar still reads a table that never had one.
505    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
506    redeemed_invites: ShardedMap<RedeemedInvite>,
507    /// **The operator's looks into accounts** (2026-10-05), keyed by
508    /// `account␟operator`: the latest request per pair. Here, in the table
509    /// [`AuthTable::may`] reads, for the reason `gates` is: an approved look
510    /// widens grants and must be read from the same version as they are.
511    ///
512    /// Written only once it holds a row, as `clans` and SSO's maps are: a
513    /// table no operator ever asked into stays readable by an older gunnar.
514    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
515    operator_access: ShardedMap<operator_access::OperatorAccess>,
516    /// **Company SSO** (owner, 2026-10-05): each account's identity provider,
517    /// keyed by the account. No secret is in it (see [`crate::sso`]).
518    ///
519    /// This map and the five SSO maps below are written only once they hold
520    /// a row (as `clans` are), and dropped again with their last one: a table
521    /// that never used SSO is byte-for-byte what it was before SSO, so an
522    /// older gunnar — whose `AuthDoc` is `deny_unknown_fields` — still reads
523    /// it on a downgrade or a twin not yet upgraded.
524    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
525    sso_providers: ShardedMap<crate::sso::SsoProvider>,
526    /// Email domain → the account whose IdP it leads to. One domain, one
527    /// account; written with the provider row.
528    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
529    sso_domains: ShardedMap<String>,
530    /// **The companies' directories**: principal id → its directory row. An
531    /// employee from SSO, SCIM or a CSV import has one; written in the same
532    /// compare-and-swap as the employee, removed with it.
533    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
534    sso_users: ShardedMap<crate::sso::SsoUser>,
535    /// `s␟<account>␟<subject key>` and `u␟<account>␟<user name>` → principal
536    /// id: a login resolves its subject in one probe, never across accounts.
537    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
538    sso_index: ShardedMap<String>,
539    /// SCIM bearers by the SHA-256 of their secret.
540    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
541    scim_tokens: ShardedMap<crate::sso::ScimToken>,
542    /// SCIM groups by id.
543    #[serde(default, skip_serializing_if = "ShardedMap::is_empty")]
544    sso_groups: ShardedMap<crate::sso::SsoGroup>,
545}
546
547impl_doc!(AuthDoc {
548    principals,
549    keys,
550    grants,
551    tokens,
552    memberships,
553    gates,
554    names,
555    clans,
556    clan_index,
557    employers,
558    rooms,
559    seats,
560    room_orders,
561    credentials,
562    invites,
563    redeemed_invites,
564    operator_access,
565    sso_providers,
566    sso_domains,
567    sso_users,
568    sso_index,
569    scim_tokens,
570    sso_groups
571});
572
573/// How long a redeemed invite is remembered as USED: an invite lives at most
574/// thirty days, so a link older than that reads expired anyway.
575pub const REDEEMED_INVITE_KEEP_MS: UnixMillis = 30 * 24 * 60 * 60 * 1000;
576
577/// **An invite that was redeemed** (2026-10-19): whose passkey it enrolled, and when.
578#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
579pub struct RedeemedInvite {
580    /// The principal whose passkey it enrolled.
581    pub principal_id: String,
582    /// When it was redeemed.
583    pub redeemed: UnixMillis,
584}
585
586/// **The room one employee holds** out of its account's room.
587#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
588#[serde(deny_unknown_fields)]
589pub struct EmployeeRoom {
590    /// Bytes.
591    pub room_bytes: u64,
592    /// The order it came with (the seat's, for a filled seat); empty: none.
593    #[serde(default, skip_serializing_if = "String::is_empty")]
594    pub order: String,
595}
596
597/// **A block of SEATS** an account holds: `total` places for employees not
598/// yet named, each holding `room_bytes` of the account's room.
599#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
600#[serde(deny_unknown_fields)]
601pub struct SeatLot {
602    /// Bytes per seat.
603    pub room_bytes: u64,
604    /// How many seats the lot was granted with.
605    pub total: u32,
606    /// The employees that filled a seat, in the order they came. A removed
607    /// employee leaves this list, and its seat is open again.
608    #[serde(default, skip_serializing_if = "Vec::is_empty")]
609    pub filled: Vec<String>,
610    /// The order that granted it; empty: none.
611    #[serde(default, skip_serializing_if = "String::is_empty")]
612    pub order: String,
613    /// When.
614    pub at: UnixMillis,
615}
616
617impl SeatLot {
618    /// Seats not yet filled.
619    pub fn open(&self) -> u32 {
620        self.total
621            .saturating_sub(u32::try_from(self.filled.len()).unwrap_or(u32::MAX))
622    }
623}
624
625/// **What an order granted**, kept for its retry.
626#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
627#[serde(deny_unknown_fields)]
628pub struct RoomGrant {
629    /// The account.
630    pub account: String,
631    /// The named employee, or empty for seats.
632    #[serde(default, skip_serializing_if = "String::is_empty")]
633    pub principal: String,
634    /// Seats granted (0 for a named employee).
635    #[serde(default)]
636    pub seats: u32,
637    /// Bytes per employee or seat.
638    pub room_bytes: u64,
639    /// What it asked of the account, bytes.
640    #[serde(default)]
641    pub need_bytes: u64,
642}
643
644/// **What an enrolment asks of the account's room** ("Add users").
645#[derive(Clone, PartialEq, Eq, Debug, Default)]
646pub struct RoomAsk {
647    /// Bytes per employee or seat. Ignored for a seat claim: the seat's.
648    pub room_bytes: u64,
649    /// Seats to grant, unnamed. 0: one named employee.
650    pub seats: u32,
651    /// Fill an open seat with the named employee instead of asking for room.
652    pub from_seat: bool,
653    /// The account's room: its cap. `None`: nothing caps this account on
654    /// this server.
655    pub cap: Option<u64>,
656    /// What the account stores now, bytes (counted against `cap`).
657    pub stored: u64,
658    /// The order this belongs to (idempotency); empty: none.
659    pub order: String,
660    /// Ask, write nothing.
661    pub dry_run: bool,
662}
663
664/// **The answer** to a [`RoomAsk`].
665#[derive(Clone, PartialEq, Eq, Debug, Default)]
666pub struct RoomAnswer {
667    /// The named employee (unset for seats and for a dry run).
668    pub principal: Option<Principal>,
669    /// Bytes each new employee or seat holds.
670    pub room_bytes: u64,
671    /// The account's seats after this: every seat granted…
672    pub seats: u32,
673    /// …and the ones not yet filled.
674    pub seats_open: u32,
675    /// What was asked of the account's room, bytes.
676    pub need_bytes: u64,
677    /// The account's room neither stored nor promised, before this; `None`
678    /// when nothing caps it.
679    pub unused_bytes: Option<u64>,
680    /// How much is missing (a dry run's answer; the real call refuses).
681    pub shortfall_bytes: u64,
682    /// Nothing was written: a dry run, or the retry of a granted order.
683    pub unchanged: bool,
684}
685
686/// **The record that the unauthenticated enrolment window has been used.**
687///
688/// Written once, never rewritten, never removed. Every field is here so an
689/// operator can answer *who opened this server, with which key, and when* from
690/// the server itself rather than from a log that may have rotated.
691#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
692#[serde(deny_unknown_fields)]
693pub struct BootstrapRecord {
694    /// When the window closed.
695    pub at: UnixMillis,
696    /// The principal that was created. It may since have been removed; the
697    /// record still stands, which is the point.
698    pub principal_id: String,
699    /// The `SHA256:…` fingerprint of the key it was created with.
700    pub fingerprint: String,
701    /// The account (namespace) that principal is intrinsically admin of. Equal
702    /// to `principal_id`; carried explicitly so a later change to that rule is
703    /// a visible schema change and not a silent re-derivation.
704    pub account: String,
705}
706
707/// What [`AuthTable::bootstrap_admin`] did.
708#[derive(Clone, PartialEq, Eq, Debug)]
709pub enum Bootstrap {
710    /// The table was empty and this admin was installed.
711    Installed(Principal),
712    /// The table already held keys; nothing was changed.
713    AlreadyProvisioned,
714}
715
716/// **What state the identity table is in at start-up, and therefore whether the
717/// unauthenticated enrolment door is open.**
718///
719/// Until 2026-08-05 there were two states and one of them was *refuse to
720/// start*: a server whose identity table was empty and which had been given no
721/// `--admin-key` bailed out of `Server::start`. That is why there was no first
722/// boot at all — the box could not be reached to be given its first key, and
723/// the only way in was `gunnar key add` **with the server down**, which needs a
724/// shell on the machine and there is no shell on the appliance.
725///
726/// The refusal is replaced by [`Readiness::AwaitingBootstrap`]: the server
727/// starts, and `Bootstrap.Enroll` is servable **without a credential** for
728/// exactly as long as [`AuthTable::bootstrap_record`] is `None`.
729///
730/// Three states, and each predicate below answers `false` for at least one of
731/// them: a `may_serve()` that had become true for every state would be an
732/// identity value, and a test sitting on it could not tell a working gate from
733/// a dead one (measured, not asserted).
734#[derive(Clone, PartialEq, Eq, Debug)]
735pub enum Readiness {
736    /// There is at least one registered key, and bootstrap has happened.
737    /// Ordinary operation.
738    Ready,
739    /// **Nothing has ever been enrolled.** The identity table is empty *and*
740    /// there is no [`BootstrapRecord`]. `Bootstrap.Enroll` is open, to anyone
741    /// who can reach the control port. This must be said out loud at every
742    /// start and reported in status; see `gunnar_grpc::service::bootstrap`.
743    AwaitingBootstrap,
744    /// **The enrolment window is shut and there is no key to authenticate
745    /// with.** Nobody can reach this server over either listener until a key is
746    /// put back with `gunnar key add`, offline — and the window stays shut
747    /// regardless, which is the attack this design exists to refuse.
748    SealedAndEmpty {
749        /// Who bootstrapped it, and when, so the operator can tell an attack
750        /// from an accident.
751        ///
752        /// `None` for a table that holds principals but no keys and carries no
753        /// bootstrap record: a half-provisioned data path written before the
754        /// record existed. That is a real state and must be described as
755        /// itself rather than with an invented record.
756        record: Option<Box<BootstrapRecord>>,
757    },
758}
759
760impl Readiness {
761    /// Is the unauthenticated `Bootstrap.Enroll` window open?
762    ///
763    /// True for exactly one of the three states.
764    pub fn bootstrap_window_open(&self) -> bool {
765        matches!(self, Readiness::AwaitingBootstrap)
766    }
767
768    /// Can anybody actually authenticate against this server right now?
769    ///
770    /// False for both non-`Ready` states, for two different reasons, which is
771    /// why this is not the negation of [`Readiness::bootstrap_window_open`].
772    pub fn has_an_identity(&self) -> bool {
773        matches!(self, Readiness::Ready)
774    }
775}
776
777impl std::fmt::Display for Readiness {
778    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
779        match self {
780            Readiness::Ready => f.write_str("ready"),
781            Readiness::AwaitingBootstrap => f.write_str(
782                "AWAITING BOOTSTRAP: no identity has ever been enrolled, so Bootstrap.Enroll is \
783                 open to any caller that can reach the control port",
784            ),
785            Readiness::SealedAndEmpty {
786                record: Some(record),
787            } => write!(
788                f,
789                "bootstrapped at {} by {} but every identity has since been removed; the \
790                 enrolment window stays SHUT and nobody can reach this server",
791                record.at, record.principal_id
792            ),
793            Readiness::SealedAndEmpty { record: None } => f.write_str(
794                "this identity table holds principals but no keys and carries no bootstrap \
795                 record; the enrolment window stays SHUT and nobody can reach this server",
796            ),
797        }
798    }
799}
800
801/// Principals, their keys, and the grants between them and repositories.
802///
803/// Cheap to clone; every clone reads and writes the same document.
804#[derive(Clone, Debug)]
805pub struct AuthTable {
806    doc: DocRef<AuthDoc>,
807}
808
809impl AuthTable {
810    /// Open the identity table held in `store` (the certificate-identity plan).
811    pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
812        Ok(AuthTable {
813            doc: DocRef::new(store, AUTH_REF)?,
814        })
815    }
816
817    /// The name the table is stored under.
818    pub fn ref_name(&self) -> &str {
819        self.doc.ref_name()
820    }
821
822    /// **Every version this table has ever had**, oldest first (the certificate-identity plan).
823    ///
824    /// A ref update per change used to be a free audit trail; on a key/value
825    /// store it is an explicit append-only table, and this is the door to it.
826    /// Each entry's `blob` is still readable through the control store, so
827    /// "what did the ACL look like before Tuesday" is answerable from the
828    /// server rather than from a log that may have rotated.
829    pub fn history(&self) -> Result<Vec<crate::control::DocVersion>> {
830        self.doc.history()
831    }
832
833    // ── principals ─────────────────────────────────────────────────────────
834
835    /// Add a principal.
836    ///
837    /// Refuses [`ANONYMOUS_PRINCIPAL_ID`](crate::model::ANONYMOUS_PRINCIPAL_ID)
838    /// [`Principal::new`] refuses it too, but this is the door a
839    /// [`Principal::anonymous`] *value* would arrive at. A principal sitting in
840    /// the table under that name would be handed the public-read rule of
841    /// [`AuthTable::may`] and could then be given keys and grants on top of it.
842    ///
843    /// **The table stamps `created`** with its own clock, whatever the caller
844    /// put there: one writer for the field, so a row's creation time is the
845    /// server's word and not the client's.
846    ///
847    /// The name is refused if another principal carries it — as its name or as
848    /// its id ([`Error::NameExists`]). The id is what everything keys on; the
849    /// name is the label; the label is still unique, in the same
850    /// compare-and-swap as the row, so a listing can never show two rows a
851    /// human reads as one.
852    pub fn add_principal(&self, principal: Principal) -> Result<Principal> {
853        crate::sandbox::check_component(&principal.id, &principal.id)?;
854        if principal.id == crate::model::ANONYMOUS_PRINCIPAL_ID {
855            return Err(Error::ReservedPrincipalId(principal.id));
856        }
857        let principal = Principal {
858            created: now_ms(),
859            ..principal
860        };
861        self.doc.update(|doc| {
862            if doc.principals.contains_key(&principal.id)? {
863                return Err(Error::PrincipalExists(principal.id.clone()));
864            }
865            Self::refuse_taken_name(doc, &principal.name, &principal.id)?;
866            doc.principals
867                .insert(principal.id.clone(), principal.clone())?;
868            Self::index_name(doc, &principal)?;
869            Ok(principal.clone())
870        })
871    }
872
873    /// **Change a principal's display name and nothing else.**
874    ///
875    /// The id — and with it the account namespace, every key, grant,
876    /// membership and gate — stays exactly where it is; only the label moves.
877    /// This is what makes a rename an edit rather than a re-identification: a
878    /// console that pinned the id sees the same principal afterwards, and a
879    /// URL, a policy file or a grant that names the id is untouched.
880    ///
881    /// Refuses a name another principal already carries, as its name or as its
882    /// id ([`Error::NameExists`]). Renaming to the name it already has is a
883    /// no-op that succeeds.
884    pub fn rename_principal(&self, id: &str, name: &str) -> Result<Principal> {
885        self.doc.update(|doc| {
886            let before = doc
887                .principals
888                .get(id)?
889                .ok_or_else(|| Error::NoSuchPrincipal(id.to_owned()))?;
890            if before.name == name {
891                return Ok(before);
892            }
893            Self::refuse_taken_name(doc, name, id)?;
894            let after = Principal {
895                name: name.to_owned(),
896                ..before.clone()
897            };
898            // The old index entry goes only if it was this principal's. A row
899            // from before the index has no entry, and `remove` of an absent key
900            // is the same no-op.
901            if doc.names.get(&before.name)?.as_deref() == Some(id) {
902                doc.names.remove(&before.name)?;
903            }
904            doc.principals.insert(id.to_owned(), after.clone())?;
905            Self::index_name(doc, &after)?;
906            Ok(after)
907        })
908    }
909
910    /// **One principal, by its display name.** One index probe, then one row
911    /// probe for a principal from before the index (whose name is its id).
912    ///
913    /// This is the lookup a console makes at the one moment a name is anybody's
914    /// to give — registration — before it pins the id and stops asking by name.
915    pub fn principal_by_name(&self, name: &str) -> Result<Option<Principal>> {
916        let doc = self.doc.load()?;
917        Self::resolve_name(&doc, name)
918    }
919
920    /// The principal `name` resolves to, through the index first and the
921    /// pre-index fallback second. One reader for both
922    /// [`AuthTable::principal_by_name`] and [`AuthTable::refuse_taken_name`], so
923    /// "what does this name mean" and "is this name free" cannot disagree.
924    fn resolve_name(doc: &AuthDoc, name: &str) -> Result<Option<Principal>> {
925        if let Some(id) = doc.names.get(name)? {
926            return doc.principals.get(&id);
927        }
928        // Before the index every row was `name == id`, so the name is the key.
929        // The name check matters: a row whose name has since been changed is
930        // still at this key, and it is not the principal called `name` now.
931        Ok(doc
932            .principals
933            .get(name)?
934            .filter(|principal| principal.name == name))
935    }
936
937    /// Refuse `name` unless it is free, or is `own_id`'s own.
938    ///
939    /// Taken means: another principal's name (through [`AuthTable::resolve_name`])
940    /// or another principal's **id** — the second rule is what stops a display
941    /// name from ever spelling somebody else's identity on the wire.
942    fn refuse_taken_name(doc: &AuthDoc, name: &str, own_id: &str) -> Result<()> {
943        let holder = match Self::resolve_name(doc, name)? {
944            Some(principal) => Some(principal.id),
945            None if name != own_id && doc.principals.contains_key(name)? => Some(name.to_owned()),
946            None => None,
947        };
948        match holder {
949            Some(id) if id != own_id => Err(Error::NameExists {
950                name: name.to_owned(),
951                principal_id: id,
952            }),
953            _ => Ok(()),
954        }
955    }
956
957    /// Write the index entry for `principal`'s name. The one writer, used by
958    /// every path that puts a row in `principals`.
959    fn index_name(doc: &mut AuthDoc, principal: &Principal) -> Result<()> {
960        // A clan's name is its namespace, and a principal is admin of the
961        // namespace named after its id (`owns_account`): no principal is ever
962        // made under a clan's name, on any path that makes one.
963        if !doc.clans.is_empty() && doc.clans.contains_key(&principal.id)? {
964            return Err(Error::PrincipalExists(principal.id.clone()));
965        }
966        doc.names
967            .insert(principal.name.clone(), principal.id.clone())?;
968        Ok(())
969    }
970
971    /// Remove a principal, its keys and its grants.
972    ///
973    /// All three together, by construction: a principal whose keys outlived it
974    /// is an account that still authenticates.
975    ///
976    /// **O(keys on the server)**. Keys are stored by fingerprint, because that
977    /// is what the connection path looks them up by, so finding one principal's
978    /// keys is a walk. Deleting an account happens once a day, not once a
979    /// connection; making it cheap would mean a second index to keep in step
980    /// with the first, which is the twin that one implementation refuses.
981    pub fn remove_principal(&self, id: &str) -> Result<Principal> {
982        self.doc.update(|doc| {
983            let principal = doc
984                .principals
985                .remove(id)?
986                .ok_or_else(|| Error::NoSuchPrincipal(id.to_owned()))?;
987            // The name goes with the row, in the same update, so the name is
988            // claimable again exactly when the id is. Only if the entry is this
989            // principal's: a pre-index row has none, and `remove` of an absent
990            // key is the same no-op.
991            if doc.names.get(&principal.name)?.as_deref() == Some(id) {
992                doc.names.remove(&principal.name)?;
993            }
994            doc.keys.retain(|_, key| key.principal_id != id)?;
995            // **Tokens go with the principal, for the reason the gate does**
996            // (GUN-026).
997            //
998            // Leaving them was nearly harmless and not quite:
999            // `principal_by_token_at` ends in `doc.principals.get(&token.\
1000            // principal_id)`, so an orphaned token authenticates as nobody and
1001            // the credential is dead the moment its holder is. But a principal
1002            // id is CLAIMABLE AGAIN the moment the principal is gone —
1003            // `add_principal` refuses only a name that is currently taken — and
1004            // a token row is keyed by its hash and names its principal by that
1005            // id. So recreating `alice` handed the new alice every access token
1006            // the old one had, and neither the operator who removed the
1007            // principal nor the one who created it saw a line about it. That is
1008            // the gate bug below in its other direction: there, a leftover row
1009            // locked the next principal out; here, it lets somebody else in.
1010            doc.tokens.retain(|_, token| token.principal_id != id)?;
1011            doc.grants.remove(id)?;
1012            // Membership, both directions. A principal whose membership edges
1013            // outlived it would keep inheriting grants (its own list) or keep
1014            // conferring them (if it was a group other principals were in), so
1015            // both are cut in the same update, by construction — the same rule
1016            // as keys and grants above.
1017            doc.memberships.remove(id)?;
1018            let strip: Vec<(String, Vec<String>)> = doc
1019                .memberships
1020                .entries()?
1021                .into_iter()
1022                .filter(|(_, groups)| groups.iter().any(|g| g == id))
1023                .map(|(member, mut groups)| {
1024                    groups.retain(|g| g != id);
1025                    (member, groups)
1026                })
1027                .collect();
1028            for (member, groups) in strip {
1029                if groups.is_empty() {
1030                    doc.memberships.remove(&member)?;
1031                } else {
1032                    doc.memberships.insert(member, groups)?;
1033                }
1034            }
1035            // **The gate goes with the principal, in the same update.**
1036            //
1037            // A gate is keyed by principal id, and a principal id is claimable
1038            // again the moment the principal is gone (`add_principal` only
1039            // refuses a name that is currently taken). A row left behind here
1040            // would therefore lock out *the next* principal created under this
1041            // name — an account bricked before it existed, by a decision taken
1042            // about somebody else. `tests/gate.rs` drives exactly that
1043            // sequence.
1044            doc.gates.remove(id)?;
1045            // An employee's binding to its account goes with it, so the id —
1046            // claimable again from here — never comes back as somebody's
1047            // employee.
1048            doc.employers.remove(id)?;
1049            // Its room goes back to the account, and the seat it filled (if
1050            // any) is open again; an account that goes takes its seats and its
1051            // orders with it.
1052            doc.rooms.remove(id)?;
1053            let reopen: Vec<(String, Vec<SeatLot>)> = doc
1054                .seats
1055                .entries()?
1056                .into_iter()
1057                .filter(|(_, lots)| lots.iter().any(|lot| lot.filled.iter().any(|f| f == id)))
1058                .map(|(account, mut lots)| {
1059                    for lot in &mut lots {
1060                        lot.filled.retain(|f| f != id);
1061                    }
1062                    (account, lots)
1063                })
1064                .collect();
1065            for (account, lots) in reopen {
1066                doc.seats.insert(account, lots)?;
1067            }
1068            doc.seats.remove(id)?;
1069            doc.room_orders
1070                .retain(|_, grant| grant.account != id && grant.principal != id)?;
1071            // Its clans (2026-10-05): out of every one, its contribution with
1072            // it; a clan it was the last admin of goes to its longest member.
1073            Self::forget_clan_member(doc, id)?;
1074            // Its passkeys go too (Stage L): a credential that outlived its
1075            // principal would log the NEXT principal of that id in.
1076            doc.credentials
1077                .retain(|_, credential| credential.principal_id != id)?;
1078            // …and its invites: a pending enrolment on a principal that is
1079            // gone would enrol the next principal of that id.
1080            doc.invites.retain(|_, invite| invite.principal_id != id)?;
1081            doc.redeemed_invites
1082                .retain(|_, row| row.principal_id != id)?;
1083            // …and the operator's looks into its account, or its own looks
1084            // into others': a grant keyed by a name that is claimable again.
1085            doc.operator_access
1086                .retain(|_, look| look.account != id && look.operator != id)?;
1087            // …and its directory row and the index entries that lead to it
1088            // (SSO): a subject bound to a principal that is gone would log
1089            // the NEXT principal of that id in. An account's own SSO rows go
1090            // with the account.
1091            doc.sso_users.remove(id)?;
1092            doc.sso_index.retain(|_, held| held != id)?;
1093            doc.sso_users.retain(|_, user| user.account != id)?;
1094            doc.sso_index
1095                .retain(|key, _| !Self::sso_index_of_account(key, id))?;
1096            if let Some(provider) = doc.sso_providers.remove(id)? {
1097                for domain in provider.domains {
1098                    doc.sso_domains.remove(&domain)?;
1099                }
1100            }
1101            doc.scim_tokens.retain(|_, token| token.account != id)?;
1102            doc.sso_groups.retain(|_, group| group.account != id)?;
1103            for (gid, mut group) in doc.sso_groups.entries()? {
1104                if group.members.iter().any(|m| m == id) {
1105                    group.members.retain(|m| m != id);
1106                    doc.sso_groups.insert(gid, group)?;
1107                }
1108            }
1109            Ok(principal)
1110        })
1111    }
1112
1113    /// **Enroll an employee: a NEW user, created by and bound to `account`.**
1114    ///
1115    /// The only way a principal becomes an employee, and it can only make a
1116    /// new one: the id must not exist (an existing principal "might have repos
1117    /// already, that would be a risk for the company") and the name is checked
1118    /// as for every principal. In ONE compare-and-swap it writes the principal
1119    /// row, its name, the employee binding and a `Read` grant on the account's
1120    /// namespace — a member sees the account's repositories and events; what
1121    /// it may write is granted per repository by the account admin.
1122    ///
1123    /// An employee gets NO account of its own (no namespace named after it;
1124    /// `CoreControlPlane::enroll_employee` refuses an id that names one), so
1125    /// the intrinsic "your own account" rule never makes it an admin of
1126    /// anything, and it may not make a repository public anywhere: that is the
1127    /// account admin's ([`AuthTable::may_namespace`] at `Admin`).
1128    ///
1129    /// Who may enroll (the account's admin) is the caller's question; the
1130    /// account must exist as a namespace, which the control plane checks. Only
1131    /// a [`PrincipalKind::User`](crate::PrincipalKind::User) can be enrolled.
1132    pub fn enroll_employee(&self, account: &str, principal: Principal) -> Result<Principal> {
1133        let answer = self.enroll_with_room(account, Some(principal), &RoomAsk::default())?;
1134        answer.principal.ok_or_else(|| Error::InvalidInput {
1135            what: "employee",
1136            reason: "the enrolment named nobody".to_owned(),
1137        })
1138    }
1139
1140    /// **Enroll with room** (owner, 2026-10-04, "Add users"): ONE named
1141    /// employee holding `room_bytes`, or `seats` unnamed seats each holding
1142    /// it, or a seat claim (`from_seat`: the named employee takes an open
1143    /// seat and its room). The room comes out of what the account holds and
1144    /// has neither stored nor promised — `budget` minus the room its
1145    /// employees and open seats hold — and in the SAME compare-and-swap as
1146    /// the rows it writes, so two enrolments can never both spend the last of
1147    /// it. Short: [`Error::RoomShort`] naming what is missing, nothing
1148    /// written. A dry run answers with the shortfall instead and writes
1149    /// nothing; a retried `order` is answered with its first grant.
1150    ///
1151    /// The rules [`AuthTable::enroll_employee`] keeps for a named employee
1152    /// hold here unchanged: a NEW principal only, a person, not the account.
1153    pub fn enroll_with_room(
1154        &self,
1155        account: &str,
1156        principal: Option<Principal>,
1157        ask: &RoomAsk,
1158    ) -> Result<RoomAnswer> {
1159        let principal = match principal {
1160            Some(principal) => {
1161                crate::sandbox::check_component(&principal.id, &principal.id)?;
1162                if principal.id == crate::model::ANONYMOUS_PRINCIPAL_ID {
1163                    return Err(Error::ReservedPrincipalId(principal.id));
1164                }
1165                if principal.kind != crate::model::PrincipalKind::User {
1166                    return Err(Error::InvalidInput {
1167                        what: "employee",
1168                        reason: format!(
1169                            "an employee is a person; {} is a {:?}, enroll a user",
1170                            principal.id, principal.kind
1171                        ),
1172                    });
1173                }
1174                if principal.id == account {
1175                    return Err(Error::InvalidInput {
1176                        what: "employee",
1177                        reason: format!("{account} is the account itself, not an employee of it"),
1178                    });
1179                }
1180                Some(Principal {
1181                    created: now_ms(),
1182                    ..principal
1183                })
1184            }
1185            None => None,
1186        };
1187        if principal.is_none() && (ask.seats == 0 || ask.from_seat) {
1188            return Err(Error::InvalidInput {
1189                what: "employee",
1190                reason: "one employee, or a seat filled, needs a name".to_owned(),
1191            });
1192        }
1193        if principal.is_some() && ask.seats > 0 {
1194            return Err(Error::InvalidInput {
1195                what: "employee",
1196                reason: "seats name nobody: a seat is named when it is filled".to_owned(),
1197            });
1198        }
1199        if ask.dry_run {
1200            let doc = self.doc.load()?;
1201            return Self::plan_room(&doc, account, principal.as_ref(), ask).map(|plan| plan.answer);
1202        }
1203        self.doc.update(|doc| {
1204            let plan = Self::plan_room(doc, account, principal.as_ref(), ask)?;
1205            if plan.answer.unchanged {
1206                return Ok(plan.answer);
1207            }
1208            if plan.answer.shortfall_bytes > 0 {
1209                return Err(Error::RoomShort {
1210                    account: account.to_owned(),
1211                    need: plan.answer.need_bytes,
1212                    unused: plan.answer.unused_bytes.unwrap_or(0),
1213                    short: plan.answer.shortfall_bytes,
1214                });
1215            }
1216            let mut answer = plan.answer.clone();
1217            let mut seats = doc.seats.get(account)?.unwrap_or_default();
1218            match &principal {
1219                Some(principal) => {
1220                    Self::write_employee(
1221                        doc,
1222                        account,
1223                        principal,
1224                        plan.lot,
1225                        &ask.order,
1226                        answer.room_bytes,
1227                        &mut seats,
1228                    )?;
1229                    answer.principal = Some(principal.clone());
1230                }
1231                None => seats.push(SeatLot {
1232                    room_bytes: answer.room_bytes,
1233                    total: ask.seats,
1234                    filled: Vec::new(),
1235                    order: ask.order.clone(),
1236                    at: now_ms(),
1237                }),
1238            }
1239            answer.seats = seats.iter().map(|lot| lot.total).sum();
1240            answer.seats_open = seats.iter().map(SeatLot::open).sum();
1241            if !seats.is_empty() {
1242                doc.seats.insert(account.to_owned(), seats)?;
1243            }
1244            if !ask.order.is_empty() {
1245                doc.room_orders.insert(
1246                    ask.order.clone(),
1247                    RoomGrant {
1248                        account: account.to_owned(),
1249                        principal: principal.as_ref().map(|p| p.id.clone()).unwrap_or_default(),
1250                        seats: ask.seats,
1251                        room_bytes: answer.room_bytes,
1252                        need_bytes: answer.need_bytes,
1253                    },
1254                )?;
1255            }
1256            Ok(answer)
1257        })
1258    }
1259
1260    /// **The rows one NEW named employee is**, inside the caller's
1261    /// compare-and-swap: the principal and its name, the employee binding, a
1262    /// `Read` grant on the account's namespace, the seat it fills (when
1263    /// `lot`), and the room it holds. The one writer of an employee —
1264    /// [`AuthTable::enroll_with_room`] and an SSO first login both call it.
1265    fn write_employee(
1266        doc: &mut AuthDoc,
1267        account: &str,
1268        principal: &Principal,
1269        lot: Option<usize>,
1270        order: &str,
1271        room_bytes: u64,
1272        seats: &mut [SeatLot],
1273    ) -> Result<()> {
1274        doc.principals
1275            .insert(principal.id.clone(), principal.clone())?;
1276        Self::index_name(doc, principal)?;
1277        doc.employers
1278            .insert(principal.id.clone(), account.to_owned())?;
1279        doc.grants.insert(
1280            principal.id.clone(),
1281            vec![Grant {
1282                principal: principal.id.clone(),
1283                scope: Scope::Namespace(account.to_owned()),
1284                level: Level::Read,
1285            }],
1286        )?;
1287        let order = match lot {
1288            Some(lot) => {
1289                seats[lot].filled.push(principal.id.clone());
1290                seats[lot].order.clone()
1291            }
1292            None => order.to_owned(),
1293        };
1294        if room_bytes > 0 {
1295            doc.rooms
1296                .insert(principal.id.clone(), EmployeeRoom { room_bytes, order })?;
1297        }
1298        Ok(())
1299    }
1300
1301    /// The decision, read-only: what the ask needs, what is unused, which seat
1302    /// a claim takes, and whether `order` was already granted.
1303    fn plan_room(
1304        doc: &AuthDoc,
1305        account: &str,
1306        principal: Option<&Principal>,
1307        ask: &RoomAsk,
1308    ) -> Result<RoomPlan> {
1309        let lots = doc.seats.get(account)?.unwrap_or_default();
1310        let seats = lots.iter().map(|lot| lot.total).sum();
1311        let seats_open = lots.iter().map(SeatLot::open).sum();
1312        if !ask.order.is_empty() {
1313            if let Some(grant) = doc.room_orders.get(&ask.order)? {
1314                if grant.account != account {
1315                    return Err(Error::InvalidInput {
1316                        what: "order",
1317                        reason: format!(
1318                            "order {} was granted to {}, not to {account}",
1319                            ask.order, grant.account
1320                        ),
1321                    });
1322                }
1323                let principal = match grant.principal.as_str() {
1324                    "" => None,
1325                    id => doc.principals.get(id)?,
1326                };
1327                return Ok(RoomPlan {
1328                    answer: RoomAnswer {
1329                        principal,
1330                        room_bytes: grant.room_bytes,
1331                        seats,
1332                        seats_open,
1333                        need_bytes: grant.need_bytes,
1334                        unused_bytes: None,
1335                        shortfall_bytes: 0,
1336                        unchanged: true,
1337                    },
1338                    lot: None,
1339                });
1340            }
1341        }
1342        if let Some(principal) = principal {
1343            if doc.principals.contains_key(&principal.id)? {
1344                return Err(Error::PrincipalExists(principal.id.clone()));
1345            }
1346            Self::refuse_taken_name(doc, &principal.name, &principal.id)?;
1347        }
1348        let lot = if ask.from_seat {
1349            Some(
1350                lots.iter()
1351                    .position(|lot| lot.open() > 0)
1352                    .ok_or_else(|| Error::NoOpenSeat(account.to_owned()))?,
1353            )
1354        } else {
1355            None
1356        };
1357        let room_bytes = match lot {
1358            Some(lot) => lots[lot].room_bytes,
1359            None => ask.room_bytes,
1360        };
1361        let need_bytes = match (lot, ask.seats) {
1362            (Some(_), _) => 0,
1363            (None, 0) => room_bytes,
1364            (None, seats) => room_bytes.saturating_mul(u64::from(seats)),
1365        };
1366        // What the account already holds that is NOT free: what it stores and
1367        // what its employees and open seats hold. Counted in full — an
1368        // account storing past its cap is short by that much too, so the
1369        // shortfall is everything a sale must add for the ask to fit.
1370        // What it gives its clans is promised too (2026-10-05): a byte given
1371        // to a clan is not a byte an employee may hold.
1372        let committed = match ask.cap {
1373            None => None,
1374            Some(_) => Some(
1375                Self::promised_by(doc, account)?
1376                    .saturating_add(Self::contributed_in(doc, account, None)?)
1377                    .saturating_add(ask.stored),
1378            ),
1379        };
1380        let unused_bytes = ask
1381            .cap
1382            .zip(committed)
1383            .map(|(cap, committed)| cap.saturating_sub(committed));
1384        let shortfall_bytes = ask.cap.zip(committed).map_or(0, |(cap, committed)| {
1385            need_bytes.saturating_add(committed).saturating_sub(cap)
1386        });
1387        Ok(RoomPlan {
1388            answer: RoomAnswer {
1389                principal: None,
1390                room_bytes,
1391                seats,
1392                seats_open,
1393                need_bytes,
1394                unused_bytes,
1395                shortfall_bytes,
1396                unchanged: ask.dry_run,
1397            },
1398            lot,
1399        })
1400    }
1401
1402    /// **What an account has promised its people**: the room its employees
1403    /// hold and its open seats hold, bytes. The one count `plan_room` and a
1404    /// clan contribution both take.
1405    fn promised_by(doc: &AuthDoc, account: &str) -> Result<u64> {
1406        let lots = doc.seats.get(account)?.unwrap_or_default();
1407        let mut promised: u64 = lots
1408            .iter()
1409            .map(|lot| lot.room_bytes.saturating_mul(u64::from(lot.open())))
1410            .fold(0, u64::saturating_add);
1411        for (id, employer) in doc.employers.entries()? {
1412            if employer == account {
1413                if let Some(room) = doc.rooms.get(&id)? {
1414                    promised = promised.saturating_add(room.room_bytes);
1415                }
1416            }
1417        }
1418        Ok(promised)
1419    }
1420
1421    /// **The account's seats**: every seat granted, and the open ones.
1422    pub fn seats_of(&self, account: &str) -> Result<(u32, u32)> {
1423        let lots = self.doc.load()?.seats.get(account)?.unwrap_or_default();
1424        Ok((
1425            lots.iter().map(|lot| lot.total).sum(),
1426            lots.iter().map(SeatLot::open).sum(),
1427        ))
1428    }
1429
1430    /// The room an employee holds, if any.
1431    pub fn room_of(&self, principal_id: &str) -> Result<Option<EmployeeRoom>> {
1432        self.doc.load()?.rooms.get(principal_id)
1433    }
1434
1435    /// The account that enrolled this principal, if it is an employee.
1436    pub fn employer_of(&self, principal_id: &str) -> Result<Option<String>> {
1437        self.doc.load()?.employers.get(principal_id)
1438    }
1439
1440    /// Every employee an account has enrolled, by id. A listing: it walks the
1441    /// bindings, as `members_of` walks memberships.
1442    pub fn employees_of(&self, account: &str) -> Result<Vec<Principal>> {
1443        let doc = self.doc.load()?;
1444        let mut out = Vec::new();
1445        for (id, employer) in doc.employers.entries()? {
1446            if employer == account {
1447                if let Some(p) = doc.principals.get(&id)? {
1448                    out.push(p);
1449                }
1450            }
1451        }
1452        out.sort_by(|a, b| a.id.cmp(&b.id));
1453        Ok(out)
1454    }
1455
1456    /// **Is this an enterprise account?** — derived, never set: an account
1457    /// whose admin has enrolled at least one employee.
1458    pub fn is_enterprise(&self, account: &str) -> Result<bool> {
1459        // Stops at the first binding (asked on every `Access.WhoAmI`), cloning nothing.
1460        self.doc
1461            .load()?
1462            .employers
1463            .any(|_, employer| employer == account)
1464    }
1465
1466    /// One principal, if it exists.
1467    pub fn principal(&self, id: &str) -> Result<Option<Principal>> {
1468        self.doc.load()?.principals.get(id)
1469    }
1470
1471    /// Every principal. The listing path, **O(principals)**.
1472    pub fn principals(&self) -> Result<Vec<Principal>> {
1473        self.doc.load()?.principals.values()
1474    }
1475
1476    // ── keys ───────────────────────────────────────────────────────────────
1477
1478    /// Register an `authorized_keys`-style public key line to a principal.
1479    ///
1480    /// Refuses an unknown principal, and refuses a key that is already
1481    /// registered. [`Error::KeyExists`] says why re-pointing it would be worse
1482    /// than failing.
1483    /// Register an **X.509 certificate** to a principal.
1484    ///
1485    /// The mTLS twin of [`register_key`](Self::register_key), and deliberately
1486    /// the same record: a `Fingerprint` is all gunnar knows about a credential,
1487    /// so a certificate and an SSH key differ in how they are *presented*, not
1488    /// in what is stored or in what any grant check afterwards does (the mTLS plan).
1489    ///
1490    /// `der` is the certificate itself. `algo` is recorded as `x509` so an
1491    /// operator listing keys can tell the two apart — which matters precisely
1492    /// because [`fingerprint_of_certificate`] and [`fingerprint_of_blob`] share
1493    /// one hash space, so the fingerprint alone cannot say which kind it is.
1494    ///
1495    /// **Not parsed.** gunnar does not validate the certificate here and must
1496    /// not: validity, chain and revocation are the TLS handshake's job, at
1497    /// connection time, against the configured CA. Checking a subset of that
1498    /// here would produce a second, weaker opinion about whether a credential is
1499    /// good — and the weaker opinion is the one an attacker picks.
1500    pub fn register_certificate(
1501        &self,
1502        principal_id: &str,
1503        der: &[u8],
1504        expires: Option<UnixMillis>,
1505    ) -> Result<PublicKey> {
1506        let fingerprint = fingerprint_of_certificate(der);
1507        let record = PublicKey {
1508            fingerprint_sha256: fingerprint.as_str().to_owned(),
1509            algo: "x509".to_owned(),
1510            principal_id: principal_id.to_owned(),
1511            added: now_ms(),
1512            last_used: None,
1513            expires,
1514            // The credential's own bytes, base64 as a PEM body carries them, so
1515            // an operator can see WHICH certificate was registered rather than
1516            // only its digest.
1517            openssh: {
1518                use base64ct::Encoding as _;
1519                base64ct::Base64::encode_string(der)
1520            },
1521        };
1522        self.doc.update(|doc| {
1523            if !doc.principals.contains_key(principal_id)? {
1524                return Err(Error::NoSuchPrincipal(principal_id.to_owned()));
1525            }
1526            // A fingerprint already registered is REFUSED rather than
1527            // reassigned, exactly as for an SSH key. Two principals sharing one
1528            // credential is not a convenience, it is an audit trail that cannot
1529            // say who acted.
1530            if let Some(existing) = doc.keys.get(fingerprint.as_str())? {
1531                return Err(Error::KeyExists {
1532                    fingerprint: fingerprint.as_str().to_owned(),
1533                    principal_id: existing.principal_id.clone(),
1534                });
1535            }
1536            doc.keys
1537                .insert(fingerprint.as_str().to_owned(), record.clone())?;
1538            Ok(record.clone())
1539        })
1540    }
1541
1542    /// Register an OpenSSH public key line for `principal_id`, deriving the
1543    /// fingerprint here so no caller can store one that disagrees with the key.
1544    pub fn register_key(
1545        &self,
1546        principal_id: &str,
1547        openssh: &str,
1548        expires: Option<UnixMillis>,
1549    ) -> Result<PublicKey> {
1550        // A quantum-safe encryption key (crypt-pq) is kept beside the SSH
1551        // keys, in the same table under its own `algo`; see
1552        // [`PQ_RECIPIENT_ALGO`].
1553        let (fingerprint, algo, line) = match pq_recipient_fingerprint(openssh) {
1554            Some(fingerprint) => (
1555                fingerprint,
1556                PQ_RECIPIENT_ALGO.to_owned(),
1557                openssh.trim().to_owned(),
1558            ),
1559            None => {
1560                let parsed = parse_public_key(openssh)?;
1561                (
1562                    Fingerprint::of_key(&parsed),
1563                    parsed.algorithm().as_str().to_owned(),
1564                    parsed.to_openssh().map_err(Error::from)?.trim().to_owned(),
1565                )
1566            }
1567        };
1568        let record = PublicKey {
1569            fingerprint_sha256: fingerprint.as_str().to_owned(),
1570            algo,
1571            principal_id: principal_id.to_owned(),
1572            added: now_ms(),
1573            last_used: None,
1574            expires,
1575            openssh: line,
1576        };
1577        self.doc.update(|doc| {
1578            if !doc.principals.contains_key(principal_id)? {
1579                return Err(Error::NoSuchPrincipal(principal_id.to_owned()));
1580            }
1581            if let Some(existing) = doc.keys.get(fingerprint.as_str())? {
1582                return Err(Error::KeyExists {
1583                    fingerprint: fingerprint.as_str().to_owned(),
1584                    principal_id: existing.principal_id.clone(),
1585                });
1586            }
1587            doc.keys
1588                .insert(fingerprint.as_str().to_owned(), record.clone())?;
1589            Ok(record.clone())
1590        })
1591    }
1592
1593    /// **Register a web credential (a passkey) on a person** (Stage L).
1594    ///
1595    /// Refuses an unknown principal, a principal that is not a USER (no human
1596    /// credential on a machine principal) and a credential id already
1597    /// registered — to anybody. The key's shape is the caller's to have
1598    /// checked; this stores what it is given.
1599    pub fn add_web_credential(
1600        &self,
1601        credential: crate::model::WebCredential,
1602    ) -> Result<crate::model::WebCredential> {
1603        self.doc.update(|doc| {
1604            let principal = doc
1605                .principals
1606                .get(&credential.principal_id)?
1607                .ok_or_else(|| Error::NoSuchPrincipal(credential.principal_id.clone()))?;
1608            if principal.kind != crate::model::PrincipalKind::User {
1609                return Err(Error::NotAPerson {
1610                    principal_id: principal.id.clone(),
1611                    kind: format!("{:?}", principal.kind).to_lowercase(),
1612                });
1613            }
1614            if let Some(existing) = doc.credentials.get(&credential.id)? {
1615                return Err(Error::CredentialExists {
1616                    id: credential.id.clone(),
1617                    principal_id: existing.principal_id,
1618                });
1619            }
1620            doc.credentials
1621                .insert(credential.id.clone(), credential.clone())?;
1622            Ok(credential.clone())
1623        })
1624    }
1625
1626    /// **A passkey was used** (the identity core's login and step-up,
1627    /// IDENTITY-HOST 2026-10-27): its counter (when it counts) and last use, in
1628    /// one update; a counter that went back is refused, as a web login's is.
1629    pub fn touch_web_credential(
1630        &self,
1631        id: &str,
1632        presented: u32,
1633        now: UnixMillis,
1634    ) -> Result<crate::model::WebCredential> {
1635        self.doc.update(|doc| {
1636            let mut credential = doc
1637                .credentials
1638                .get(id)?
1639                .ok_or_else(|| Error::NoSuchCredential(id.to_owned()))?;
1640            if credential.sign_count != 0 && presented != 0 && presented <= credential.sign_count {
1641                return Err(Error::CounterRollback {
1642                    id: id.to_owned(),
1643                    stored: credential.sign_count,
1644                    presented,
1645                });
1646            }
1647            if presented != 0 {
1648                credential.sign_count = presented;
1649            }
1650            credential.last_used = Some(now);
1651            doc.credentials.insert(id.to_owned(), credential.clone())?;
1652            Ok(credential)
1653        })
1654    }
1655
1656    /// One web credential by its id (base64url), or `None`.
1657    pub fn web_credential(&self, id: &str) -> Result<Option<crate::model::WebCredential>> {
1658        self.doc.load()?.credentials.get(id)
1659    }
1660
1661    /// One principal's web credentials, or every one. A listing path.
1662    pub fn web_credentials(
1663        &self,
1664        principal_id: Option<&str>,
1665    ) -> Result<Vec<crate::model::WebCredential>> {
1666        let mut rows: Vec<crate::model::WebCredential> = self
1667            .doc
1668            .load()?
1669            .credentials
1670            .values()?
1671            .into_iter()
1672            .filter(|c| principal_id.is_none_or(|want| c.principal_id == want))
1673            .collect();
1674        rows.sort_by(|a, b| {
1675            (a.principal_id.as_str(), a.created, a.id.as_str()).cmp(&(
1676                b.principal_id.as_str(),
1677                b.created,
1678                b.id.as_str(),
1679            ))
1680        });
1681        Ok(rows)
1682    }
1683
1684    /// Revoke a web credential **and the session keys its logins enrolled**,
1685    /// in one update — the person's console sessions on that passkey end with
1686    /// it. Returns the row removed, if there was one.
1687    pub fn revoke_web_credential(&self, id: &str) -> Result<Option<crate::model::WebCredential>> {
1688        let removed = self.doc.update(|doc| {
1689            let removed = doc.credentials.remove(id)?;
1690            if let Some(credential) = &removed {
1691                for fingerprint in &credential.sessions {
1692                    if doc
1693                        .keys
1694                        .get(fingerprint)?
1695                        .is_some_and(|key| key.principal_id == credential.principal_id)
1696                    {
1697                        doc.keys.remove(fingerprint)?;
1698                    }
1699                }
1700            }
1701            Ok(removed)
1702        })?;
1703        if let Some(removed) = &removed {
1704            tracing::info!(credential = %id, principal = %removed.principal_id, "web credential revoked");
1705        }
1706        Ok(removed)
1707    }
1708
1709    /// **Commit an approved machine enrolment** (2026-10-05), in ONE update:
1710    /// the approver's step-up passkey's counter (re-checked: it
1711    /// must increase when both it and the stored one are non-zero) and last
1712    /// use; the MACHINE principal `machine` made when absent (a principal of
1713    /// that id that is not a machine is [`Error::NotAMachine`]); its
1714    /// server-wide grant set to `level`; and `credential` stored on it (an id
1715    /// already registered is [`Error::CredentialExists`]). The ONE way a web
1716    /// credential lands on a machine — [`AuthTable::add_web_credential`] and
1717    /// [`AuthTable::add_web_credential`] still refuse one. Returns the
1718    /// machine, the stored credential, and whether this update made the
1719    /// machine.
1720    pub fn approve_machine_credential(
1721        &self,
1722        machine: &str,
1723        level: Level,
1724        mut credential: crate::model::WebCredential,
1725        step_up: Option<(&str, u32)>,
1726        now: UnixMillis,
1727    ) -> Result<(Principal, crate::model::WebCredential, bool)> {
1728        crate::sandbox::check_component(machine, machine)?;
1729        if machine == crate::model::ANONYMOUS_PRINCIPAL_ID {
1730            return Err(Error::ReservedPrincipalId(machine.to_owned()));
1731        }
1732        self.doc.update(|doc| {
1733            if let Some(existing) = doc.credentials.get(&credential.id)? {
1734                return Err(Error::CredentialExists {
1735                    id: credential.id.clone(),
1736                    principal_id: existing.principal_id,
1737                });
1738            }
1739            let approver = match step_up {
1740                Some((id, presented)) => {
1741                    let mut held = doc
1742                        .credentials
1743                        .get(id)?
1744                        .ok_or_else(|| Error::NoSuchCredential(id.to_owned()))?;
1745                    if held.sign_count != 0 && presented != 0 && presented <= held.sign_count {
1746                        return Err(Error::CounterRollback {
1747                            id: id.to_owned(),
1748                            stored: held.sign_count,
1749                            presented,
1750                        });
1751                    }
1752                    if presented != 0 {
1753                        held.sign_count = presented;
1754                    }
1755                    held.last_used = Some(now);
1756                    Some((id.to_owned(), held))
1757                }
1758                None => None,
1759            };
1760            let (principal, created) = match doc.principals.get(machine)? {
1761                Some(held) if held.kind != crate::model::PrincipalKind::Machine => {
1762                    return Err(Error::NotAMachine {
1763                        principal_id: held.id.clone(),
1764                        kind: format!("{:?}", held.kind).to_lowercase(),
1765                    })
1766                }
1767                Some(held) => (held, false),
1768                None => {
1769                    Self::refuse_taken_name(doc, machine, machine)?;
1770                    let made = Principal {
1771                        id: machine.to_owned(),
1772                        name: machine.to_owned(),
1773                        kind: crate::model::PrincipalKind::Machine,
1774                        created: now,
1775                    };
1776                    doc.principals.insert(made.id.clone(), made.clone())?;
1777                    Self::index_name(doc, &made)?;
1778                    (made, true)
1779                }
1780            };
1781            if let Some((id, held)) = approver {
1782                doc.credentials.insert(id, held)?;
1783            }
1784            let grant = Grant::server(machine.to_owned(), level);
1785            let mut grants = doc.grants.get(machine)?.unwrap_or_default();
1786            grants.retain(|old| old.scope != grant.scope);
1787            grants.push(grant);
1788            grants.sort();
1789            doc.grants.insert(machine.to_owned(), grants)?;
1790            credential.principal_id = machine.to_owned();
1791            doc.credentials
1792                .insert(credential.id.clone(), credential.clone())?;
1793            Ok((principal, credential.clone(), created))
1794        })
1795    }
1796
1797    /// Change a web credential's label. Returns the updated row, if there was
1798    /// one.
1799    pub fn rename_web_credential(
1800        &self,
1801        id: &str,
1802        label: &str,
1803    ) -> Result<Option<crate::model::WebCredential>> {
1804        self.doc.update(|doc| {
1805            let Some(mut credential) = doc.credentials.get(id)? else {
1806                return Ok(None);
1807            };
1808            credential.label = label.to_owned();
1809            doc.credentials.insert(id.to_owned(), credential.clone())?;
1810            Ok(Some(credential))
1811        })
1812    }
1813
1814    /// The person an invite is for, checked: exists, and is a USER.
1815    fn invitable(doc: &AuthDoc, principal_id: &str) -> Result<Principal> {
1816        let principal = doc
1817            .principals
1818            .get(principal_id)?
1819            .ok_or_else(|| Error::NoSuchPrincipal(principal_id.to_owned()))?;
1820        if principal.kind != crate::model::PrincipalKind::User {
1821            return Err(Error::NotAPerson {
1822                principal_id: principal.id.clone(),
1823                kind: format!("{:?}", principal.kind).to_lowercase(),
1824            });
1825        }
1826        Ok(principal)
1827    }
1828
1829    /// **Record the operator's first invite** (the installer's token by its
1830    /// hash; IDENTITY-HOST: the identity core reads it as a fed invite onto
1831    /// the operator). The person must be a USER; one per box, recorded again
1832    /// it is replaced.
1833    pub fn put_first_invite(&self, invite: crate::model::FirstInviteRow) -> Result<()> {
1834        self.doc.update(|doc| {
1835            Self::invitable(doc, &invite.principal_id)?;
1836            doc.invites.insert(invite.id.clone(), invite.clone())?;
1837            Ok(())
1838        })
1839    }
1840
1841    /// The operator's first invite, if one is recorded (`id`), expired or not.
1842    pub fn first_invite(&self, id: &str) -> Result<Option<crate::model::FirstInviteRow>> {
1843        self.doc.load()?.invites.get(id)
1844    }
1845
1846    /// Drop the operator's first invite (spent, withdrawn, or its person
1847    /// holds a passkey). Returns the row removed, if there was one.
1848    pub fn drop_first_invite(&self, id: &str) -> Result<Option<crate::model::FirstInviteRow>> {
1849        self.doc.update(|doc| doc.invites.remove(id))
1850    }
1851
1852    /// Revoke a key. Returns the record that was removed, if there was one.
1853    pub fn revoke_key(&self, fingerprint: &Fingerprint) -> Result<Option<PublicKey>> {
1854        let removed = self
1855            .doc
1856            .update(|doc| doc.keys.remove(fingerprint.as_str()))?;
1857        if removed.is_some() {
1858            tracing::info!(%fingerprint, "public key revoked");
1859        }
1860        Ok(removed)
1861    }
1862
1863    /// **Revoke every certificate registered to `principal_id` that certifies a
1864    /// key OTHER than `spki`** (a DER SubjectPublicKeyInfo), in one update.
1865    /// Returns the rows removed. SSH keys and certificates for `spki` itself (a
1866    /// renewal: the same key, a new validity window) are kept; a certificate row
1867    /// whose bytes do not parse certifies no key anyone can prove, and goes.
1868    ///
1869    /// What re-certifying a SINGLE-HOLDER principal means (the twin's
1870    /// `gunnar-twin`, twin reborn 2026-09-30): a new key is a new box, and the
1871    /// box it replaces — nuked, its disk possibly copied — must not keep a
1872    /// registered credential beside it.
1873    pub fn supersede_certificates(
1874        &self,
1875        principal_id: &str,
1876        spki: &[u8],
1877    ) -> Result<Vec<PublicKey>> {
1878        use base64ct::Encoding as _;
1879        self.doc.update(|doc| {
1880            let mut gone = Vec::new();
1881            for row in doc.keys.values()? {
1882                if row.principal_id != principal_id || row.algo != "x509" {
1883                    continue;
1884                }
1885                let same_key = base64ct::Base64::decode_vec(&row.openssh)
1886                    .ok()
1887                    .and_then(|der| crate::issuer::public_key_info_of_der(&der).ok())
1888                    .is_some_and(|key| key == spki);
1889                if !same_key {
1890                    if let Some(removed) = doc.keys.remove(&row.fingerprint_sha256)? {
1891                        gone.push(removed);
1892                    }
1893                }
1894            }
1895            Ok(gone)
1896        })
1897    }
1898
1899    /// Every registered key, **O(keys)**. Also a listing path.
1900    ///
1901    /// **Includes expired keys**, deliberately: `gunnar key list` must show a key
1902    /// that has run out, or nobody can see why authentication stopped working.
1903    /// Anything making a *decision* wants [`AuthTable::signing_keys`] instead.
1904    pub fn keys(&self) -> Result<Vec<PublicKey>> {
1905        self.doc.load()?.keys.values()
1906    }
1907
1908    /// Every key that may still **satisfy a signature requirement**, as of now.
1909    ///
1910    /// # Why this exists as its own method
1911    ///
1912    /// Found 2026-08-05: `gunnar-cli`'s `key_directory` — the whole of the
1913    /// ssh-sig gate's key material (the pre-receive gate) — built itself from
1914    /// [`AuthTable::keys`] with **no expiry check at all**. So a key past its
1915    /// `expires` could no longer open an SSH session, and could still sign a
1916    /// commit that `require_signature` accepted. That is one credential with two
1917    /// lifetimes, and the longer one was the one nobody had written down.
1918    ///
1919    /// A *revoked* key is a removed row and so cannot reach either path, which is
1920    /// exactly why the bug hid: the revocation case, which is the one everybody
1921    /// tests, was already correct.
1922    ///
1923    /// The fix is a named method rather than a `filter` at the call site because
1924    /// the call site is what got it wrong. "Every key" and "every key that may
1925    /// decide something" are different questions, and until now the second one
1926    /// had no way to be asked. [`AuthTable::principal_by_fingerprint_at`] answers
1927    /// the same question for authentication, through the same
1928    /// [`PublicKey::is_valid_at`] — one predicate, two consumers, so the two
1929    /// lifetimes cannot drift apart again (one implementation).
1930    pub fn signing_keys(&self) -> Result<Vec<PublicKey>> {
1931        self.signing_keys_at(now_ms())
1932    }
1933
1934    /// [`AuthTable::signing_keys`] at an explicit time, so expiry can be tested
1935    /// without waiting for it.
1936    pub fn signing_keys_at(&self, now: UnixMillis) -> Result<Vec<PublicKey>> {
1937        Ok(self
1938            .doc
1939            .load()?
1940            .keys
1941            .values()?
1942            .into_iter()
1943            .filter(|key| key.is_valid_at(now))
1944            .collect())
1945    }
1946
1947    /// **The keys one principal could attest an addition with**: its keys that
1948    /// are valid now and are OpenSSH key material rather than a certificate,
1949    /// since only those can make an `sshsig`. What
1950    /// [`crate::key_attestation::Requirement::of`] is handed.
1951    ///
1952    /// **O(keys on the server)**, for the reason [`AuthTable::keys_of`] is: it
1953    /// sits on `Access.AddKey`, which happens when a person gets a machine, not
1954    /// on a connection.
1955    pub fn attesting_keys_of(&self, principal_id: &str) -> Result<Vec<AttestingKey>> {
1956        let now = now_ms();
1957        Ok(self
1958            .keys_of(principal_id)?
1959            .into_iter()
1960            .filter(|key| {
1961                key.is_valid_at(now) && key.algo != "x509" && key.algo != PQ_RECIPIENT_ALGO
1962            })
1963            .map(|key| AttestingKey {
1964                fingerprint: key.fingerprint_sha256,
1965                openssh: key.openssh,
1966            })
1967            .collect())
1968    }
1969
1970    /// Every key registered to one principal. `gunnar-policy` needs this to
1971    /// verify an ssh-sig signature (the pre-receive gate).
1972    ///
1973    /// **O(keys on the server)**, for the reason
1974    /// [`AuthTable::remove_principal`] gives. It sits on the
1975    /// signature-verification path of a push, not on a connection.
1976    pub fn keys_of(&self, principal_id: &str) -> Result<Vec<PublicKey>> {
1977        Ok(self
1978            .doc
1979            .load()?
1980            .keys
1981            .values()?
1982            .into_iter()
1983            .filter(|key| key.principal_id == principal_id)
1984            .collect())
1985    }
1986
1987    /// **The keys a key ring may be wrapped to** (`Repos.RecipientKeys`, the
1988    /// forge's `gunnar-recipient-keys` door): the principal's keys valid NOW —
1989    /// an expired key is one its holder retired, and a ring wrapped to it
1990    /// opens for whoever holds that private half (wiring tracer B 2026-10-08).
1991    /// A certificate is never a recipient.
1992    pub fn recipient_keys_of(&self, principal_id: &str) -> Result<Vec<PublicKey>> {
1993        let now = now_ms();
1994        Ok(self
1995            .keys_of(principal_id)?
1996            .into_iter()
1997            .filter(|key| key.is_valid_at(now) && key.algo != "x509")
1998            .collect())
1999    }
2000
2001    /// **The SSH auth answer**: who owns this key, as of now?
2002    ///
2003    /// `None` for an unregistered key, for a key whose principal has been
2004    /// removed, and for an expired key.
2005    pub fn principal_by_fingerprint(&self, fingerprint: &Fingerprint) -> Result<Option<Principal>> {
2006        self.principal_by_fingerprint_at(fingerprint, now_ms())
2007    }
2008
2009    /// [`AuthTable::principal_by_fingerprint`] at an explicit time, so expiry
2010    /// can be tested without waiting for it.
2011    pub fn principal_by_fingerprint_at(
2012        &self,
2013        fingerprint: &Fingerprint,
2014        now: UnixMillis,
2015    ) -> Result<Option<Principal>> {
2016        let doc = self.doc.load()?;
2017        let Some(key) = doc.keys.get(fingerprint.as_str())? else {
2018            return Ok(None);
2019        };
2020        if !key.is_valid_at(now) {
2021            tracing::warn!(%fingerprint, "expired key offered");
2022            return Ok(None);
2023        }
2024        doc.principals.get(&key.principal_id)
2025    }
2026
2027    /// Record that a key just authenticated.
2028    ///
2029    /// Separate from the lookup because it writes, and the lookup is on the
2030    /// connection path: a listener that cannot write must still be able to
2031    /// authenticate.
2032    ///
2033    /// # DELIBERATELY UNCALLED. Do not "fix" this by adding the call
2034    ///
2035    /// Nothing in `crates/*/src` calls this, so [`PublicKey::last_used`] is
2036    /// `None` for every key on the server and `Refs`/`Identity` hand a consumer
2037    /// a field that is never written. That is a real gap and it is filed. It is
2038    /// **not** closed by wiring this into `auth_publickey`.
2039    ///
2040    /// The cost is no longer the objection it was: sharding split the identity
2041    /// table, so this rewrites one leaf plus two trie nodes plus the document —
2042    /// single-digit KB below ~30 000 keys — and not the several hundred
2043    /// megabytes the finding was originally written against.
2044    ///
2045    /// What forbids it is the **shape**, not the size. [`DocRef::update`] is a
2046    /// compare-and-swap on **one** reference, `refs/gunnar/auth`, with
2047    /// `MAX_ATTEMPTS = 8`. Calling this per successful authentication puts every
2048    /// login on the server in contention for that single ref, and the failure
2049    /// mode is not slowness: it is an `Err` after eight lost races, **on the
2050    /// authentication path**. A key that authenticated correctly would be
2051    /// refused because a bookkeeping write lost. `last_used` reading `None` is
2052    /// strictly better than that.
2053    ///
2054    /// Either of two things closes it, and both are more than a call site: an
2055    /// append-only identity log, where "key K authenticated at T" is one
2056    /// appended row and needs no CAS; or a low-durability sidecar written
2057    /// best-effort off the connection path and allowed to lose updates. **No
2058    /// sidecar exists in the tree today**, which is why this is still here.
2059    pub fn touch_key(&self, fingerprint: &Fingerprint, now: UnixMillis) -> Result<()> {
2060        self.doc.update(|doc| {
2061            doc.keys
2062                .modify(fingerprint.as_str(), |key| key.last_used = Some(now))?;
2063            Ok(())
2064        })
2065    }
2066
2067    // ── access tokens ──────────────────────────────────────────────────────
2068
2069    /// Register an access token for `principal_id`, storing only its hash.
2070    ///
2071    /// **The secret is minted by the caller, not here**, exactly as
2072    /// [`AuthTable::register_key`] is handed an `authorized_keys` line rather
2073    /// than generating a key pair. That keeps a random number generator out of
2074    /// this crate, and it keeps the one moment the secret exists in a place the
2075    /// operator can see: whoever calls this is the only code that ever holds
2076    /// it, and after this returns nothing in gunnar can produce it again.
2077    ///
2078    /// Refuses an unknown principal, refuses
2079    /// [`ANONYMOUS_PRINCIPAL_ID`](crate::model::ANONYMOUS_PRINCIPAL_ID) (which
2080    /// cannot be in the table anyway, so this is the belt to
2081    /// [`AuthTable::add_principal`]'s braces), and refuses a secret that is
2082    /// already registered.
2083    pub fn register_token(
2084        &self,
2085        principal_id: &str,
2086        secret: &str,
2087        label: &str,
2088        expires: Option<UnixMillis>,
2089    ) -> Result<AccessToken> {
2090        if principal_id == crate::model::ANONYMOUS_PRINCIPAL_ID {
2091            return Err(Error::ReservedPrincipalId(principal_id.to_owned()));
2092        }
2093        let hash = token_hash(secret);
2094        let record = AccessToken {
2095            hash: hash.clone(),
2096            principal_id: principal_id.to_owned(),
2097            label: label.to_owned(),
2098            created: now_ms(),
2099            last_used: None,
2100            expires,
2101        };
2102        self.doc.update(|doc| {
2103            if !doc.principals.contains_key(principal_id)? {
2104                return Err(Error::NoSuchPrincipal(principal_id.to_owned()));
2105            }
2106            if let Some(existing) = doc.tokens.get(&hash)? {
2107                return Err(Error::TokenExists {
2108                    hash: hash.clone(),
2109                    principal_id: existing.principal_id.clone(),
2110                });
2111            }
2112            doc.tokens.insert(hash.clone(), record.clone())?;
2113            Ok(record.clone())
2114        })
2115    }
2116
2117    /// Revoke a token by its **hash**, which is what listing prints.
2118    pub fn revoke_token(&self, hash: &str) -> Result<Option<AccessToken>> {
2119        let removed = self.doc.update(|doc| doc.tokens.remove(hash))?;
2120        if removed.is_some() {
2121            tracing::info!(token = %hash, "access token revoked");
2122        }
2123        Ok(removed)
2124    }
2125
2126    /// Every registered token, **O(tokens)**. A listing path, never an auth
2127    /// path.
2128    pub fn tokens(&self) -> Result<Vec<AccessToken>> {
2129        self.doc.load()?.tokens.values()
2130    }
2131
2132    /// **The HTTP auth answer**: who holds this secret, as of now?
2133    ///
2134    /// `None` for an unregistered secret, for a token whose principal has been
2135    /// removed, and for an expired token — the same three cases, in the same
2136    /// order, as [`AuthTable::principal_by_fingerprint`], because they are the
2137    /// same question asked with a different credential.
2138    pub fn principal_by_token(&self, secret: &str) -> Result<Option<Principal>> {
2139        self.principal_by_token_at(secret, now_ms())
2140    }
2141
2142    /// [`AuthTable::principal_by_token`] at an explicit time, so expiry can be
2143    /// tested without waiting for it.
2144    pub fn principal_by_token_at(
2145        &self,
2146        secret: &str,
2147        now: UnixMillis,
2148    ) -> Result<Option<Principal>> {
2149        let doc = self.doc.load()?;
2150        let Some(token) = doc.tokens.get(&token_hash(secret))? else {
2151            return Ok(None);
2152        };
2153        if !token.is_valid_at(now) {
2154            // The hash, never the secret: this line goes to a log.
2155            tracing::warn!(token = %token.hash, "expired access token presented");
2156            return Ok(None);
2157        }
2158        doc.principals.get(&token.principal_id)
2159    }
2160
2161    /// Record that a token just authenticated — **at most once per
2162    /// [`LAST_USED_GRANULARITY`] per token**.
2163    ///
2164    /// Separate from the lookup for the same reason [`AuthTable::touch_key`]
2165    /// is: the lookup is on the request path and must work on a table that
2166    /// cannot be written.
2167    ///
2168    /// The granularity is the fix for the serving tier's measured ceiling.
2169    /// `last_used` lives in ONE control document shared by every principal and
2170    /// every token, so writing it per request made every authenticated HTTP
2171    /// request in the process contend on one compare-and-swap — MEASURED on
2172    /// 2026-08-11 at 573 → 1416 req/s (16-way ls-refs, 4-CPU cpuset) with
2173    /// this call the only difference, and again on 2026-08-12 by
2174    /// `gunnar.multiuser_scaling`: 102 users plateaued at 82 ops/s with the
2175    /// server stuck below 2 of 32 cores, 8 630 lost CAS attempts and 862
2176    /// silently dropped updates in one 12-second window. A credential's
2177    /// "when was this last seen" is a minute-grained fact; a fresh
2178    /// `last_used` answers the question and the skipped write is the
2179    /// throughput. The first touch of a quiet token still writes immediately.
2180    pub fn touch_token(&self, hash: &str, now: UnixMillis) -> Result<()> {
2181        // The read is the same cached document load every request's auth
2182        // lookup already does; only a token whose record is stale (or never
2183        // written) proceeds to the shared-document CAS.
2184        if let Some(token) = self.doc.load()?.tokens.get(hash)? {
2185            if let Some(last) = token.last_used {
2186                if now.saturating_sub(last) < LAST_USED_GRANULARITY {
2187                    return Ok(());
2188                }
2189            }
2190        }
2191        self.doc.update(|doc| {
2192            doc.tokens.modify(hash, |token| {
2193                // Re-checked under the CAS: of the requests that race into
2194                // the same window boundary, one write wins and the rest
2195                // rewrite to the same bytes, which `DocRef::update` commits
2196                // as "unchanged" without moving the pointer.
2197                if token
2198                    .last_used
2199                    .is_none_or(|last| now.saturating_sub(last) >= LAST_USED_GRANULARITY)
2200                {
2201                    token.last_used = Some(now);
2202                }
2203            })?;
2204            Ok(())
2205        })
2206    }
2207
2208    // ── bulk import ────────────────────────────────────────────────────────
2209
2210    /// Import many users — principal, optional access token, grants — in **one
2211    /// document transaction**.
2212    ///
2213    /// The per-entity verbs commit one CAS each, and each CAS clones the
2214    /// document and rewrites its skeleton: at an appliance-scale import that is
2215    /// O(users²) work aimed at one shared pointer. This is the batch door the
2216    /// 1→100 000-user story needs: every insert lands in the in-memory
2217    /// document once, dirty shards are flushed once, and the whole population
2218    /// becomes durable in a single compare-and-swap — so the import is also
2219    /// **atomic**: a refused user refuses the batch, and a batch that lost the
2220    /// CAS race retries as one unit.
2221    ///
2222    /// Token secrets never enter this crate: callers hand over
2223    /// [`token_hash`]es, exactly as [`AuthTable::register_token`] stores and
2224    /// [`AuthTable::principal_by_token`] looks up.
2225    pub fn bulk_add_users(&self, users: &[BulkUser], now: UnixMillis) -> Result<usize> {
2226        for user in users {
2227            crate::sandbox::check_component(&user.principal.id, &user.principal.id)?;
2228            if user.principal.id == crate::model::ANONYMOUS_PRINCIPAL_ID {
2229                return Err(Error::ReservedPrincipalId(user.principal.id.clone()));
2230            }
2231        }
2232        // Keys parsed OUTSIDE the transaction: parsing 100k OpenSSH lines is
2233        // real work, and a CAS retry must not repeat it.
2234        let mut keys: Vec<Option<PublicKey>> = Vec::with_capacity(users.len());
2235        for user in users {
2236            keys.push(match &user.openssh_public_key {
2237                None => None,
2238                Some(line) => {
2239                    let parsed = parse_public_key(line)?;
2240                    let fingerprint = Fingerprint::of_key(&parsed);
2241                    Some(PublicKey {
2242                        fingerprint_sha256: fingerprint.as_str().to_owned(),
2243                        algo: parsed.algorithm().as_str().to_owned(),
2244                        principal_id: user.principal.id.clone(),
2245                        added: now,
2246                        last_used: None,
2247                        expires: None,
2248                        openssh: parsed.to_openssh().map_err(Error::from)?.trim().to_owned(),
2249                    })
2250                }
2251            });
2252        }
2253        self.doc.update(|doc| {
2254            for user in users {
2255                let id = &user.principal.id;
2256                if doc.principals.contains_key(id)? {
2257                    return Err(Error::PrincipalExists(id.clone()));
2258                }
2259                Self::refuse_taken_name(doc, &user.principal.name, id)?;
2260                let principal = Principal {
2261                    created: now,
2262                    ..user.principal.clone()
2263                };
2264                doc.principals.insert(id.clone(), principal.clone())?;
2265                Self::index_name(doc, &principal)?;
2266                if let Some(hash) = &user.token_hash {
2267                    if let Some(existing) = doc.tokens.get(hash)? {
2268                        return Err(Error::TokenExists {
2269                            hash: hash.clone(),
2270                            principal_id: existing.principal_id.clone(),
2271                        });
2272                    }
2273                    doc.tokens.insert(
2274                        hash.clone(),
2275                        AccessToken {
2276                            hash: hash.clone(),
2277                            principal_id: id.clone(),
2278                            label: user.token_label.clone(),
2279                            created: now,
2280                            last_used: None,
2281                            expires: None,
2282                        },
2283                    )?;
2284                }
2285                if !user.grants.is_empty() {
2286                    // The principal is new in this same transaction, so there
2287                    // is nothing to merge with; sorted for the same reason
2288                    // `grant` sorts.
2289                    let mut held = user.grants.clone();
2290                    held.sort();
2291                    doc.grants.insert(id.clone(), held)?;
2292                }
2293            }
2294            for record in keys.iter().flatten() {
2295                if let Some(existing) = doc.keys.get(&record.fingerprint_sha256)? {
2296                    return Err(Error::KeyExists {
2297                        fingerprint: record.fingerprint_sha256.clone(),
2298                        principal_id: existing.principal_id.clone(),
2299                    });
2300                }
2301                doc.keys
2302                    .insert(record.fingerprint_sha256.clone(), record.clone())?;
2303            }
2304            Ok(users.len())
2305        })
2306    }
2307
2308    // ── grants ─────────────────────────────────────────────────────────────
2309
2310    /// Add a grant, replacing any existing grant this principal has on the same
2311    /// scope.
2312    ///
2313    /// Replacing rather than accumulating means a downgrade is a downgrade: if
2314    /// grants piled up, "set them to Read" would leave the old Write in place
2315    /// and the ACL would take the maximum.
2316    pub fn grant(&self, grant: Grant) -> Result<Grant> {
2317        self.doc.update(|doc| {
2318            if !doc.principals.contains_key(&grant.principal)? {
2319                return Err(Error::NoSuchPrincipal(grant.principal.clone()));
2320            }
2321            let mut held = doc.grants.get(&grant.principal)?.unwrap_or_default();
2322            held.retain(|old| old.scope != grant.scope);
2323            held.push(grant.clone());
2324            held.sort();
2325            doc.grants.insert(grant.principal.clone(), held)?;
2326            Ok(grant.clone())
2327        })
2328    }
2329
2330    /// Remove a principal's grant on one scope. `true` if there was one.
2331    pub fn revoke_grant(&self, principal_id: &str, scope: &Scope) -> Result<bool> {
2332        self.doc.update(|doc| {
2333            let Some(mut held) = doc.grants.get(principal_id)? else {
2334                return Ok(false);
2335            };
2336            let before = held.len();
2337            held.retain(|old| &old.scope != scope);
2338            if held.len() == before {
2339                return Ok(false);
2340            }
2341            if held.is_empty() {
2342                doc.grants.remove(principal_id)?;
2343            } else {
2344                doc.grants.insert(principal_id.to_owned(), held)?;
2345            }
2346            Ok(true)
2347        })
2348    }
2349
2350    /// Every grant, or every grant held by one principal.
2351    ///
2352    /// Naming a principal is a shard probe; asking for all of them walks the
2353    /// whole ACL, which is what a listing is.
2354    pub fn grants(&self, principal_id: Option<&str>) -> Result<Vec<Grant>> {
2355        let doc = self.doc.load()?;
2356        match principal_id {
2357            Some(id) => Ok(doc.grants.get(id)?.unwrap_or_default()),
2358            None => Ok(doc.grants.values()?.into_iter().flatten().collect()),
2359        }
2360    }
2361
2362    // ── groups (punkt 9) ───────────────────────────────────────────────────
2363
2364    /// **Make `member_id` a member of the group `group_id`.**
2365    ///
2366    /// The group must exist and be a
2367    /// [`PrincipalKind::Group`](crate::PrincipalKind::Group); the member must
2368    /// exist, and may be a group itself (nesting). Idempotent: adding a member
2369    /// already in the group is a no-op, so a config that lists a membership
2370    /// twice does not create a duplicate edge.
2371    ///
2372    /// This is admin-gated at the RPC exactly like [`AuthTable::grant`], and for
2373    /// the same reason: adding a member to a group that holds a grant *is*
2374    /// giving that member the grant. The gate is the caller's; here we only
2375    /// refuse memberships that do not typecheck.
2376    pub fn add_to_group(&self, group_id: &str, member_id: &str) -> Result<()> {
2377        self.doc.update(|doc| {
2378            let group = doc
2379                .principals
2380                .get(group_id)?
2381                .ok_or_else(|| Error::NoSuchPrincipal(group_id.to_owned()))?;
2382            if group.kind != PrincipalKind::Group {
2383                return Err(Error::NotAGroup(group_id.to_owned()));
2384            }
2385            if !doc.principals.contains_key(member_id)? {
2386                return Err(Error::NoSuchPrincipal(member_id.to_owned()));
2387            }
2388            let mut held = doc.memberships.get(member_id)?.unwrap_or_default();
2389            if !held.iter().any(|g| g == group_id) {
2390                held.push(group_id.to_owned());
2391                held.sort();
2392                doc.memberships.insert(member_id.to_owned(), held)?;
2393            }
2394            Ok(())
2395        })
2396    }
2397
2398    /// **Remove `member_id` from the group `group_id`.** `true` if it was a
2399    /// member.
2400    ///
2401    /// The access the group conferred is gone the moment this returns: the next
2402    /// [`AuthTable::may`] resolves the member's groups afresh from the document
2403    /// this write updated, so there is no cached membership and no window. The
2404    /// tests clone through SSH before and after to prove the applied effect, not
2405    /// the row.
2406    pub fn remove_from_group(&self, group_id: &str, member_id: &str) -> Result<bool> {
2407        self.doc.update(|doc| {
2408            let Some(mut held) = doc.memberships.get(member_id)? else {
2409                return Ok(false);
2410            };
2411            let before = held.len();
2412            held.retain(|g| g != group_id);
2413            if held.len() == before {
2414                return Ok(false);
2415            }
2416            if held.is_empty() {
2417                doc.memberships.remove(member_id)?;
2418            } else {
2419                doc.memberships.insert(member_id.to_owned(), held)?;
2420            }
2421            Ok(true)
2422        })
2423    }
2424
2425    /// The groups `member_id` belongs to **directly** (one shard probe).
2426    pub fn groups_of(&self, member_id: &str) -> Result<Vec<String>> {
2427        Ok(self
2428            .doc
2429            .load()?
2430            .memberships
2431            .get(member_id)?
2432            .unwrap_or_default())
2433    }
2434
2435    /// **Who is directly in this group.** A listing, **O(members on the
2436    /// server)**: membership is keyed by member for the connection path, so the
2437    /// inverse question is a walk. Sorted, for a stable listing.
2438    pub fn members_of(&self, group_id: &str) -> Result<Vec<String>> {
2439        let doc = self.doc.load()?;
2440        let mut members: Vec<String> = doc
2441            .memberships
2442            .entries()?
2443            .into_iter()
2444            .filter(|(_, groups)| groups.iter().any(|g| g == group_id))
2445            .map(|(member, _)| member)
2446            .collect();
2447        members.sort();
2448        Ok(members)
2449    }
2450
2451    /// **Every group on the server** — the principals whose kind is
2452    /// [`Group`](crate::PrincipalKind::Group). Walks the principals, like the
2453    /// principal listing does.
2454    pub fn groups(&self) -> Result<Vec<Principal>> {
2455        Ok(self
2456            .doc
2457            .load()?
2458            .principals
2459            .values()?
2460            .into_iter()
2461            .filter(|p| p.kind == PrincipalKind::Group)
2462            .collect())
2463    }
2464
2465    /// **Every principal whose grants `principal_id` may exercise**: itself,
2466    /// then the transitive closure of the groups it belongs to.
2467    ///
2468    /// This is the whole of group inheritance. A member is authorized by any
2469    /// grant its group holds; because a group is itself a principal that can be
2470    /// a member of another group, it is authorized by any grant a group's group
2471    /// holds, and so on — a breadth-first walk of the membership edges.
2472    ///
2473    /// # Bounded against a cycle, by construction
2474    ///
2475    /// Nothing at write time forbids a membership cycle (A ∈ B, B ∈ A), because
2476    /// forbidding it would need this very closure. `seen` is what makes the walk
2477    /// terminate anyway: a principal already expanded is never expanded again,
2478    /// so a cycle is visited once and stops. `group_cycle_terminates` in the
2479    /// tests drives A ∈ B ∈ A through here and asserts it returns rather than
2480    /// hangs — an unbounded resolver hangs that test red.
2481    ///
2482    /// The querying principal is always first and always present, so a
2483    /// principal in no group resolves to exactly itself and every `may*` below
2484    /// behaves precisely as it did before groups existed (one implementation).
2485    fn grant_principals(doc: &AuthDoc, principal_id: &str) -> Result<Vec<String>> {
2486        let mut seen = std::collections::BTreeSet::new();
2487        let mut queue = std::collections::VecDeque::new();
2488        let mut holders = Vec::new();
2489        queue.push_back(principal_id.to_owned());
2490        while let Some(id) = queue.pop_front() {
2491            if !seen.insert(id.clone()) {
2492                continue;
2493            }
2494            if let Some(groups) = doc.memberships.get(&id)? {
2495                for group in groups {
2496                    if !seen.contains(&group) {
2497                        queue.push_back(group);
2498                    }
2499                }
2500            }
2501            holders.push(id);
2502        }
2503        Ok(holders)
2504    }
2505
2506    /// Every grant held by `principal_id` **or by any group it inherits from**.
2507    ///
2508    /// The one place group inheritance widens the grant set, so `may`,
2509    /// `may_namespace` and `may_server` share it and cannot drift: each applies
2510    /// its own scope filter to the same widened set (one implementation).
2511    fn holder_grants(doc: &AuthDoc, principal_id: &str) -> Result<Vec<Grant>> {
2512        let mut all = Vec::new();
2513        for holder in Self::grant_principals(doc, principal_id)? {
2514            if let Some(grants) = doc.grants.get(&holder)? {
2515                all.extend(grants);
2516            }
2517        }
2518        Ok(all)
2519    }
2520
2521    // ── the ACL ────────────────────────────────────────────────────────────
2522
2523    /// **May this principal do this to this repository?**
2524    ///
2525    /// The rules, in full:
2526    ///
2527    /// 0. The **anonymous** principal — the one an unauthenticated caller
2528    ///    is served as — may `Read` a
2529    ///    [`Visibility::Public`](crate::model::Visibility::Public) repository
2530    ///    and may do nothing else, anywhere. It is not in the table, cannot be
2531    ///    put in the table ([`Error::ReservedPrincipalId`]) and therefore can
2532    ///    never hold a grant; this line is the *whole* of what "anonymous" can
2533    ///    ever be allowed, and it is reached before the table is even read.
2534    /// 1. An unknown principal may do nothing. Not "read-only" — nothing.
2535    /// 2. **A principal is unconditionally admin of its own account**: the
2536    ///    namespace whose name is its own id, which is the `<account>` in
2537    ///    `<host>/<account>/<repo>`. See [`AuthTable::owns_account`].
2538    /// 3. A [`Visibility::Public`](crate::model::Visibility::Public)
2539    ///    repository is readable by any registered principal too, with no
2540    ///    grant.
2541    /// 4. Otherwise the highest grant covering the repository — server,
2542    ///    namespace or repository scope — held by this principal **or by any
2543    ///    group it is (transitively) a member of** must be at least `level`.
2544    ///    [`Level`] is ordered, so `Admin` implies `Write` implies `Read`.
2545    ///
2546    /// **Groups (punkt 9).** A group is a principal
2547    /// ([`PrincipalKind::Group`](crate::PrincipalKind::Group)) that holds no
2548    /// keys; a member inherits the group's grants and nothing else. Inheritance
2549    /// is of grants only — never of a group's intrinsic account ownership (rule
2550    /// 2 reads the *querying* principal's own id, so being in a group named
2551    /// `team` does not make you admin of the `team` namespace). Nesting is
2552    /// resolved transitively and cycle-safely by
2553    /// [`AuthTable::grant_principals`]. The maximum over the inherited grants is
2554    /// taken, so a group granted `Read` can never let a member `Write`: a
2555    /// read-only group is read-only for everyone in it, by the same ordering
2556    /// that stops a read grant from becoming a write one for a lone user.
2557    ///
2558    /// A namespace grant matches its namespace **exactly**: `team` does not
2559    /// reach `team/sub`. See [`Scope::Namespace`].
2560    ///
2561    /// # Why anonymous is a rule here and not a branch at the listener
2562    ///
2563    /// One implementation. The SSH dispatch, the gRPC gate and anything added later ask this
2564    /// one function; a listener that decided "public, so serve it" for itself
2565    /// would be a second ACL, and the failure mode of two ACLs is that the
2566    /// permissive one wins silently. `may` is the only place a `Visibility` is
2567    /// ever read for an access decision.
2568    ///
2569    /// **A server-scope grant stops at a customer's account** (2026-10-05,
2570    /// the operator sees aggregates only): inside a namespace a PERSON owns,
2571    /// a PERSON's server grant counts for nothing unless the account's admin
2572    /// approved a look, and then up to `Read` ([`operator_access`]). A
2573    /// machine's keeps its reach: it is the estate's plumbing.
2574    pub fn may(&self, principal_id: &str, repo: &Repo, level: Level) -> Result<bool> {
2575        self.may_at(principal_id, repo, level, now_ms())
2576    }
2577
2578    /// [`AuthTable::may`] at a given instant (an operator's look ends on time).
2579    pub fn may_at(&self, principal_id: &str, repo: &Repo, level: Level, now: i64) -> Result<bool> {
2580        let doc = self.doc.load()?;
2581        Self::may_in(&doc, principal_id, repo, level, now)
2582    }
2583
2584    /// **[`AuthTable::may`] for every repository of a listing, under ONE read
2585    /// of the identity table** (round 5.3.1, test5 2026-10-08: a listing asked
2586    /// `may` once per row, and each ask read the table's pointer from the
2587    /// control store again — two store reads and a handful of shared-cache
2588    /// locks per row, so concurrent listings queued on each other). The same
2589    /// rule, row by row: [`AuthTable::may_at`] is this with one row.
2590    pub fn may_each(&self, principal_id: &str, repos: &[Repo], level: Level) -> Result<Vec<bool>> {
2591        let doc = self.doc.load()?;
2592        let now = now_ms();
2593        repos
2594            .iter()
2595            .map(|repo| Self::may_in(&doc, principal_id, repo, level, now))
2596            .collect()
2597    }
2598
2599    fn may_in(
2600        doc: &AuthDoc,
2601        principal_id: &str,
2602        repo: &Repo,
2603        level: Level,
2604        now: i64,
2605    ) -> Result<bool> {
2606        // **Rule −1: the gate, and it is read before anything else** (see
2607        // [`crate::gate`]). Both directions: a gated principal acts nowhere,
2608        // and nobody acts inside a gated account — including the anonymous
2609        // principal, which is why this sits ABOVE the anonymous rule rather
2610        // than after it. A stranger cloning a public repository out of an
2611        // account that is being emptied is exactly the reader that puts bytes
2612        // back behind a purge.
2613        if Self::gated(doc, principal_id)? || Self::gated(doc, &repo.namespace)? {
2614            return Ok(false);
2615        }
2616        if principal_id == crate::model::ANONYMOUS_PRINCIPAL_ID {
2617            return Ok(level == Level::Read && repo.visibility == crate::model::Visibility::Public);
2618        }
2619        if !doc.principals.contains_key(principal_id)? {
2620            return Ok(false);
2621        }
2622        // Rule 2½: a CLAN's own (2026-10-05) — its admin administers every
2623        // repository in the clan's namespace, its members read and write
2624        // them. Intrinsic like rule 2: no grant row a `Revoke` could take.
2625        if Self::clan_level(doc, principal_id, &repo.namespace)?.is_some_and(|have| have >= level) {
2626            return Ok(true);
2627        }
2628        // Rule 2: your own account. Intrinsic, so `level` is not consulted —
2629        // every level is allowed, including `Admin`. Never a MEMBER's: an
2630        // employee has no account of its own (2026-10-07).
2631        if Self::owns_account(principal_id, &repo.namespace) && !Self::is_member(doc, principal_id)?
2632        {
2633            return Ok(true);
2634        }
2635        if level == Level::Read && repo.visibility == crate::model::Visibility::Public {
2636            return Ok(true);
2637        }
2638        // Rule 4, now including inheritance: the highest grant covering the
2639        // repository held by this principal OR by any group it is (transitively)
2640        // a member of. `holder_grants` is `[principal] ∪ its group closure`, so
2641        // a principal in no group searches exactly its own grants as before.
2642        let covering = Self::holder_grants(doc, principal_id)?
2643            .into_iter()
2644            .filter(|grant| grant.scope.covers(repo));
2645        let best = Self::best_under(covering, || {
2646            Self::shield_in(doc, principal_id, &repo.namespace, now)
2647        })?;
2648        Ok(best.is_some_and(|have| have >= level))
2649    }
2650
2651    /// [`AuthTable::may`], as the error the SSH and gRPC layers want to
2652    /// return.
2653    pub fn require(&self, principal_id: &str, repo: &Repo, level: Level) -> Result<()> {
2654        if self.may(principal_id, repo, level)? {
2655            return Ok(());
2656        }
2657        // **A gate is reported AS a gate**, and never as a missing grant. The
2658        // two mean different things to whoever is on the other end of the SSH
2659        // session: "ask for a grant" and "your account is locked and a grant
2660        // will not help". Reported for the principal's own gate first and for
2661        // the account's second, so a locked-out user is told about *their*
2662        // lockout rather than about the namespace they were reaching into.
2663        for account in [principal_id, repo.namespace.as_str()] {
2664            if let Some(gate) = self.gate_covering(account)? {
2665                return Err(Error::Gated {
2666                    account: gate.account,
2667                    reason: gate.reason.as_str().to_owned(),
2668                    note: gate.note,
2669                });
2670            }
2671        }
2672        Err(Error::Denied {
2673            principal_id: principal_id.to_owned(),
2674            level,
2675            repo: repo.path(),
2676        })
2677    }
2678
2679    /// May this principal create and delete repositories in this namespace?
2680    ///
2681    /// Never, for the anonymous principal. Refused **by construction**
2682    /// rather than by the absence of a grant: absence is already an answer
2683    /// here, but an answer that depends on a table staying empty is a weaker
2684    /// promise than one that does not look at the table at all.
2685    pub fn may_namespace(&self, principal_id: &str, namespace: &str, level: Level) -> Result<bool> {
2686        if principal_id == crate::model::ANONYMOUS_PRINCIPAL_ID {
2687            return Ok(false);
2688        }
2689        let doc = self.doc.load()?;
2690        // The gate, both directions, exactly as in [`AuthTable::may`]. This is
2691        // the question `repo create` and `repo delete` ask, so a gate that
2692        // covered only `may` would let a locked-out user go on making
2693        // repositories in the account a purge is walking.
2694        if Self::gated(&doc, principal_id)? || Self::gated(&doc, namespace)? {
2695            return Ok(false);
2696        }
2697        if !doc.principals.contains_key(principal_id)? {
2698            return Ok(false);
2699        }
2700        // A clan's own, as in [`AuthTable::may`]'s rule 2½: its admin at
2701        // every level, its members up to `Write` (they create repositories
2702        // there; administering them is the admin's).
2703        if Self::clan_level(&doc, principal_id, namespace)?.is_some_and(|have| have >= level) {
2704            return Ok(true);
2705        }
2706        // Your own account, at every level. Same rule as [`AuthTable::may`]'s
2707        // rule 2 and the same one writer, so a repository question and a
2708        // namespace question can never disagree about who owns an account.
2709        if Self::owns_account(principal_id, namespace) && !Self::is_member(&doc, principal_id)? {
2710            return Ok(true);
2711        }
2712        let covering = Self::holder_grants(&doc, principal_id)?
2713            .into_iter()
2714            .filter(|grant| grant.scope.covers_namespace(namespace));
2715        let best = Self::best_under(covering, || {
2716            Self::shield_in(&doc, principal_id, namespace, now_ms())
2717        })?;
2718        Ok(best.is_some_and(|have| have >= level))
2719    }
2720
2721    /// May this principal administer the server (create namespaces, register
2722    /// principals)?
2723    ///
2724    /// Never, for the anonymous principal, for the reason
2725    /// [`AuthTable::may_namespace`] gives.
2726    pub fn may_server(&self, principal_id: &str, level: Level) -> Result<bool> {
2727        if principal_id == crate::model::ANONYMOUS_PRINCIPAL_ID {
2728            return Ok(false);
2729        }
2730        let doc = self.doc.load()?;
2731        // Only the actor's own gate here: `Scope::Server` names no account, so
2732        // there is no second name to check. Without this line a gated principal
2733        // holding `Server`/`Admin` could lift their own gate through the front
2734        // door, which would make the whole table advisory.
2735        if Self::gated(&doc, principal_id)? {
2736            return Ok(false);
2737        }
2738        if !doc.principals.contains_key(principal_id)? {
2739            return Ok(false);
2740        }
2741        let best = Self::holder_grants(&doc, principal_id)?
2742            .into_iter()
2743            .filter(|grant| grant.scope == Scope::Server)
2744            .map(|grant| grant.level)
2745            .max();
2746        Ok(best.is_some_and(|have| have >= level))
2747    }
2748
2749    /// **Does this principal hold a SERVER-scope grant** — any level, its own or through a
2750    /// group (2026-10-08)? The operator's mark: a namespace whose owner holds one is the
2751    /// SELLER's own (`Namespace.owner_server_grant`), never a customer a selling shop must give
2752    /// a fact. Unlike [`AuthTable::may_server`] the gate is not asked: a gated operator is still
2753    /// the seller (a fact given in the meantime would stay after the gate lifts). Only an
2754    /// operator grants `Scope::Server`; nothing a customer does — signing up, an invite, its
2755    /// own account, an employee's enrolment, a namespace or repository grant — makes one.
2756    pub fn holds_server_grant(&self, principal_id: &str) -> Result<bool> {
2757        if principal_id == crate::model::ANONYMOUS_PRINCIPAL_ID {
2758            return Ok(false);
2759        }
2760        let doc = self.doc.load()?;
2761        if !doc.principals.contains_key(principal_id)? {
2762            return Ok(false);
2763        }
2764        Ok(Self::holder_grants(&doc, principal_id)?
2765            .iter()
2766            .any(|grant| grant.scope == Scope::Server))
2767    }
2768
2769    // ── accounts (`<host>/<account>/<repo>`) ───────────────────────────────
2770
2771    /// **Does this principal own this account?**
2772    ///
2773    /// An account is a namespace, and a principal owns the namespace whose name
2774    /// is its own id — `github.com/<konto>/<repo>`, with `<konto>` the user.
2775    ///
2776    /// # Why this is a rule and not a grant
2777    ///
2778    /// Account admin is **intrinsic**, not granted. A `Grant { scope:
2779    /// Namespace(id), level: Admin }` row would look identical from the outside
2780    /// and would be *revocable*: `Access.Revoke` would take a user's own
2781    /// account away from them, and nothing would put it back, because nothing
2782    /// would remember that it was supposed to be there. This function reads no
2783    /// table, so there is no row to remove.
2784    ///
2785    /// The collision it depends on is closed at the other end: a principal's
2786    /// account namespace is created **with** the principal
2787    /// (`CoreControlPlane::add_principal`), so a namespace that already exists
2788    /// makes the principal fail to be created and a principal that already
2789    /// exists makes the namespace fail to be created. Neither name can be
2790    /// claimed twice, so this rule can never hand one person's namespace to
2791    /// somebody else.
2792    ///
2793    /// Associated rather than free so there is one spelling of the rule and
2794    /// both [`AuthTable::may`] and [`AuthTable::may_namespace`] use it (one implementation).
2795    pub fn owns_account(principal_id: &str, namespace: &str) -> bool {
2796        // Never for the anonymous principal, which holds no account. Both
2797        // callers already refuse it above; this is the belt, because the whole
2798        // rule is "a name equals a name" and `""` equalling `""` would be a
2799        // catastrophic reading of it.
2800        !principal_id.is_empty()
2801            && principal_id != crate::model::ANONYMOUS_PRINCIPAL_ID
2802            && principal_id == namespace
2803    }
2804
2805    /// **Is `principal_id` an account's MEMBER** (an employee its admin
2806    /// enrolled)? A member has no account of its own (2026-10-07, the members
2807    /// model): rule 2 does not make it the admin of a namespace that happens
2808    /// to carry its id — the walkthrough's `ben` "owned" a namespace `ben`
2809    /// nobody had made, was offered it for New repository and Import, and
2810    /// bought room for it. Asked only when the names are equal, so the hot
2811    /// path pays one probe for the rare case.
2812    fn is_member(doc: &AuthDoc, principal_id: &str) -> Result<bool> {
2813        Ok(doc.employers.get(principal_id)?.is_some())
2814    }
2815
2816    // ── the account gate (see `crate::gate`) ───────────────────────────────
2817
2818    /// **Is `account` locked out right now?** The predicate every `may*` reads.
2819    ///
2820    /// Takes the already-loaded document rather than loading its own, because
2821    /// [`AuthTable::may`] asks it twice per call and a second `load()` would
2822    /// double the parse on the path the sharding exists to keep cheap — and, worse,
2823    /// could answer the two halves of one decision from two versions of the
2824    /// table.
2825    ///
2826    /// An **expired** lease is not a gate. That is the whole crash-safety
2827    /// story: see [`crate::gate`].
2828    /// **Namespaces nest, so this walks the ancestors.** `alice/team` is
2829    /// a different namespace from `alice` — [`Scope::Namespace`] matches
2830    /// exactly and [`AuthTable::owns_account`] is a string equality — but on
2831    /// the disk it is `repos/alice/team/…`, inside the account's own directory
2832    /// subtree, and `Accounts.Purge` reclaims it along with the rest. A gate
2833    /// keyed by exact name would leave open the one part of an account a purge
2834    /// is walking.
2835    ///
2836    /// On PATH COMPONENTS and never on characters: `alicehacks` is a different
2837    /// account and a gate on `alice` must not close it.
2838    ///
2839    /// The cost is one shard probe per component, on a path
2840    /// [`AuthTable::may`] runs twice per connection. Namespaces are one
2841    /// component deep in every deployment there has been, so this is one probe;
2842    /// the walk exists so that the rare nested case is correct rather than
2843    /// silently exempt.
2844    fn gated(doc: &AuthDoc, account: &str) -> Result<bool> {
2845        let now = crate::model::now_ms();
2846        let mut prefix = String::new();
2847        for component in account.split('/') {
2848            if !prefix.is_empty() {
2849                prefix.push('/');
2850            }
2851            prefix.push_str(component);
2852            if doc
2853                .gates
2854                .get(&prefix)?
2855                .is_some_and(|gate| gate.is_live_at(now))
2856            {
2857                return Ok(true);
2858            }
2859        }
2860        Ok(false)
2861    }
2862
2863    /// **Lock an account out.** Upserts: the newest writer owns the row.
2864    ///
2865    /// Upsert rather than refuse-if-present on purpose. The two writers are a
2866    /// purge taking a lease and a person suspending an account, and in every
2867    /// ordering the *later* decision is the one that should stand — a refusal
2868    /// would mean an operator could not suspend an account a purge is holding,
2869    /// which is precisely when they most want to. What is protected instead is
2870    /// the *lift*: see [`AuthTable::lift_gate_held_by`].
2871    ///
2872    /// Returns the row as stored. Idempotent in the sense that matters: gating
2873    /// an already-gated account leaves it gated.
2874    pub fn gate(&self, gate: crate::gate::AccountGate) -> Result<crate::gate::AccountGate> {
2875        // A gate is a statement about a name, and the name has to be one this
2876        // server could ever hold — the same check `add_principal` makes, so a
2877        // path-traversal string cannot be parked in the table as a gate.
2878        crate::sandbox::check_component(&gate.account, &gate.account)?;
2879        if gate.account == crate::model::ANONYMOUS_PRINCIPAL_ID {
2880            // Gating "anonymous" would look like it closed public reads across
2881            // the whole server and would in fact do nothing of the sort, since
2882            // `may` reads the *repository's* namespace for the second check.
2883            // A refusal by name beats a row that lies.
2884            return Err(Error::ReservedPrincipalId(gate.account));
2885        }
2886        self.doc.update(|doc| {
2887            doc.gates.insert(gate.account.clone(), gate.clone())?;
2888            Ok(gate.clone())
2889        })
2890    }
2891
2892    /// **The gate on `account`, if one is in force.**
2893    ///
2894    /// `None` covers both "never gated" and "the lease expired": to everything
2895    /// downstream those are the same fact, and reporting an expired lease as a
2896    /// lockout would tell an operator an account is shut when it is open.
2897    pub fn gate_of(&self, account: &str) -> Result<Option<crate::gate::AccountGate>> {
2898        Ok(self
2899            .doc
2900            .load()?
2901            .gates
2902            .get(account)?
2903            .filter(|gate| gate.is_live_at(crate::model::now_ms())))
2904    }
2905
2906    /// **The gate that closes `account`**, which may be on an ancestor
2907    /// namespace rather than on `account` itself — see [`AuthTable::gated`],
2908    /// whose walk this is the reporting half of.
2909    ///
2910    /// [`AuthTable::gate_of`] stays an exact lookup because `Accounts.Ungate`
2911    /// and the purge's own release name a row, and lifting "whatever covers
2912    /// this" would let an ungate of `alice/team` silently lift the gate on
2913    /// `alice`. This one answers the different question a refusal has to
2914    /// answer: *why can I not do this*.
2915    pub fn gate_covering(&self, account: &str) -> Result<Option<crate::gate::AccountGate>> {
2916        let doc = self.doc.load()?;
2917        let now = crate::model::now_ms();
2918        let mut prefix = String::new();
2919        for component in account.split('/') {
2920            if !prefix.is_empty() {
2921                prefix.push('/');
2922            }
2923            prefix.push_str(component);
2924            if let Some(gate) = doc.gates.get(&prefix)?.filter(|g| g.is_live_at(now)) {
2925                return Ok(Some(gate));
2926            }
2927        }
2928        Ok(None)
2929    }
2930
2931    /// Every gate in force, by account. The listing path, **O(gates)**.
2932    pub fn gates(&self) -> Result<Vec<crate::gate::AccountGate>> {
2933        let now = crate::model::now_ms();
2934        let mut live: Vec<crate::gate::AccountGate> = self
2935            .doc
2936            .load()?
2937            .gates
2938            .values()?
2939            .into_iter()
2940            .filter(|gate| gate.is_live_at(now))
2941            .collect();
2942        live.sort_by(|a, b| a.account.cmp(&b.account));
2943        Ok(live)
2944    }
2945
2946    /// **Lift the gate on `account`, whoever holds it.** Returns what was
2947    /// there, or `None` if nothing was.
2948    ///
2949    /// The operator's override, and the reason a bricked account is always
2950    /// recoverable: it does not consult the holder, so a lease left behind by
2951    /// a process that no longer exists can still be cleared by hand.
2952    pub fn lift_gate(&self, account: &str) -> Result<Option<crate::gate::AccountGate>> {
2953        self.doc.update(|doc| doc.gates.remove(account))
2954    }
2955
2956    /// **Lift the gate on `account` only if `holder` still holds it.**
2957    ///
2958    /// What a finishing purge calls. If an operator suspended the account
2959    /// while the purge was running, the row now names them, this returns
2960    /// `None`, and the suspension survives — a purge must not be able to
2961    /// un-suspend an account as a side effect of finishing.
2962    ///
2963    /// The read and the remove are inside one [`DocRef::update`], so there is
2964    /// no window between checking the holder and clearing the row.
2965    pub fn lift_gate_held_by(
2966        &self,
2967        account: &str,
2968        holder: &str,
2969    ) -> Result<Option<crate::gate::AccountGate>> {
2970        self.doc.update(|doc| {
2971            match doc.gates.get(account)? {
2972                Some(gate) if gate.holder == holder => doc.gates.remove(account),
2973                // Somebody else's row, or none at all. Both are "nothing of
2974                // mine to lift", and neither is an error: a purge that failed
2975                // its own lease renewal and had it expire finds no row here and
2976                // must still be able to finish reporting.
2977                _ => Ok(None),
2978            }
2979        })
2980    }
2981
2982    /// **Push a lease's deadline out**, if `holder` still holds it.
2983    ///
2984    /// The heartbeat a running purge makes after each repository it reclaims.
2985    /// Returns whether the row was renewed — `false` means the purge no longer
2986    /// holds the gate (an operator replaced it, or its lease expired and
2987    /// something else took the account), which the worker logs rather than
2988    /// treats as a failure: it is still emptying an account that is still shut.
2989    pub fn renew_gate(&self, account: &str, holder: &str, expires: UnixMillis) -> Result<bool> {
2990        self.doc.update(|doc| {
2991            let renew = doc
2992                .gates
2993                .get(account)?
2994                .is_some_and(|gate| gate.holder == holder);
2995            if renew {
2996                doc.gates
2997                    .modify(account, |gate| gate.expires = Some(expires))?;
2998            }
2999            Ok(renew)
3000        })
3001    }
3002
3003    // ── bootstrap (the secret-store seam) ──────────────────────────────────
3004
3005    /// Are there no keys at all? A fresh server, in other words.
3006    ///
3007    /// **This is not "has bootstrap happened".** See [`AuthTable::bootstrap_record`].
3008    pub fn is_empty(&self) -> Result<bool> {
3009        Ok(self.doc.load()?.keys.is_empty())
3010    }
3011
3012    /// **Has the unauthenticated enrolment window ever been used?**
3013    ///
3014    /// `Some` means it is shut, permanently, whatever the identity table now
3015    /// holds. `Err` means the table could not be read, and a caller must treat
3016    /// that as *unknown*, never as *no*: reading "cannot read" as "not
3017    /// bootstrapped" is precisely how the door reopens for real. Every caller
3018    /// in the tree propagates it; `bootstrap_persistence.rs` corrupts the
3019    /// document and asserts the enrolment RPC answers `UNAVAILABLE` rather than
3020    /// enrolling.
3021    pub fn bootstrap_record(&self) -> Result<Option<BootstrapRecord>> {
3022        Ok(self.doc.load()?.bootstrap.clone())
3023    }
3024
3025    /// How many principals, keys and grants there are, for `Admin.Stats`.
3026    ///
3027    /// Principals and keys are counted, not walked. Grant *rows* are not grant
3028    /// *entries*; one entry holds a principal's whole list, so the third number
3029    /// costs a walk of the ACL. `Admin.Stats` is an operator asking a question,
3030    /// not a connection being served.
3031    pub fn counts(&self) -> Result<(usize, usize, usize)> {
3032        let doc = self.doc.load()?;
3033        let grants = doc.grants.values()?.iter().map(Vec::len).sum();
3034        Ok((doc.principals.len(), doc.keys.len(), grants))
3035    }
3036
3037    /// **THE ONE WRITER of the first identity, and the one place the
3038    /// unauthenticated enrolment window is closed.**
3039    ///
3040    /// `--admin-key` at start-up and the `Bootstrap.Enroll` RPC both land here
3041    /// (one implementation). There is exactly one code path that can produce a
3042    /// [`BootstrapRecord`], and it does five things in **one** compare-and-swap
3043    /// on `refs/gunnar/auth`:
3044    ///
3045    /// 1. refuse if a [`BootstrapRecord`] already exists;
3046    /// 2. refuse if the identity table already holds a principal or a key —
3047    ///    belt to (1)'s braces, for a table written before this field existed;
3048    /// 3. create the principal, whose id **is** its account name;
3049    /// 4. register the key, and grant it `Server`/`Admin`, which is what
3050    ///    "gunnar-admin" is;
3051    /// 5. write the [`BootstrapRecord`].
3052    ///
3053    /// # Why one CAS is the whole concurrency argument
3054    ///
3055    /// [`DocRef::update`] re-reads the document on every attempt and commits
3056    /// with a compare-and-swap on the reference. Two enrolments racing both run
3057    /// step 1 against a table with no record; one of them commits; the other
3058    /// loses the CAS, is retried from a **fresh read**, sees the winner's
3059    /// record, and returns [`Error::AlreadyBootstrapped`]. There is no window
3060    /// between "check" and "write" because they are the same write. A lock, a
3061    /// flag file or a second reference would each have one.
3062    ///
3063    /// Partial failure cannot leave the door half-shut for the same reason: the
3064    /// principal, the key, the grant and the record are one blob and one ref
3065    /// move. Either all five happened or none did.
3066    fn install_first_identity(&self, id: &str, name: &str, openssh: &str) -> Result<Enrolled> {
3067        crate::sandbox::check_component(id, id)?;
3068        if id == crate::model::ANONYMOUS_PRINCIPAL_ID {
3069            return Err(Error::ReservedPrincipalId(id.to_owned()));
3070        }
3071        let parsed = parse_public_key(openssh)?;
3072        let fingerprint = Fingerprint::of_key(&parsed);
3073        let algo = parsed.algorithm().as_str().to_owned();
3074        let normalised = parsed.to_openssh().map_err(Error::from)?.trim().to_owned();
3075
3076        let enrolled = self.doc.update(|doc| {
3077            if let Some(record) = &doc.bootstrap {
3078                return Err(Error::AlreadyBootstrapped {
3079                    at: record.at,
3080                    principal_id: record.principal_id.clone(),
3081                });
3082            }
3083            // A table with identities in it but no record predates this field.
3084            // Refuse anyway and say which of the two refusals fired, or the
3085            // upgrade path becomes a way to enrol a second admin on a live
3086            // server.
3087            if !doc.principals.is_empty() || !doc.keys.is_empty() {
3088                return Err(Error::AlreadyProvisioned {
3089                    principals: doc.principals.len(),
3090                    keys: doc.keys.len(),
3091                });
3092            }
3093            let principal = Principal {
3094                id: id.to_owned(),
3095                name: name.to_owned(),
3096                kind: PrincipalKind::User,
3097                created: now_ms(),
3098            };
3099            doc.principals.insert(id.to_owned(), principal.clone())?;
3100            Self::index_name(doc, &principal)?;
3101            let key = PublicKey {
3102                fingerprint_sha256: fingerprint.as_str().to_owned(),
3103                algo: algo.clone(),
3104                principal_id: id.to_owned(),
3105                added: now_ms(),
3106                last_used: None,
3107                expires: None,
3108                openssh: normalised.clone(),
3109            };
3110            doc.keys
3111                .insert(fingerprint.as_str().to_owned(), key.clone())?;
3112            // gunnar-admin. A `Scope::Server` Admin grant and nothing new:
3113            // it is the same row `Access.Grant --server … admin` writes, so
3114            // any gunnar-admin can hand it to anybody else through the ordinary
3115            // authenticated verb and there is no second path (one implementation).
3116            doc.grants.insert(
3117                id.to_owned(),
3118                vec![Grant::server(id.to_owned(), Level::Admin)],
3119            )?;
3120            let record = BootstrapRecord {
3121                at: now_ms(),
3122                principal_id: id.to_owned(),
3123                fingerprint: fingerprint.as_str().to_owned(),
3124                account: id.to_owned(),
3125            };
3126            doc.bootstrap = Some(record.clone());
3127            Ok(Enrolled {
3128                principal,
3129                key,
3130                record,
3131            })
3132        })?;
3133
3134        tracing::warn!(
3135            target: "gunnar::audit",
3136            event = "bootstrap.enrolled",
3137            principal = %enrolled.principal.id,
3138            account = %enrolled.record.account,
3139            %fingerprint,
3140            "THE UNAUTHENTICATED ENROLMENT WINDOW IS NOW CLOSED. It cannot be reopened by \
3141             removing this principal or this key: the record lives beside the identity table, \
3142             not in it."
3143        );
3144        Ok(enrolled)
3145    }
3146
3147    /// **Enrol the first identity over the wire.** The `Bootstrap.Enroll` RPC.
3148    ///
3149    /// One name, one public key. The principal that comes out is
3150    /// gunnar-admin (a `Server`/`Admin` grant) *and* intrinsically admin of the
3151    /// account `<name>`; everything after this is ordinary authenticated
3152    /// administration through `Access.*`.
3153    ///
3154    /// Refuses with [`Error::AlreadyBootstrapped`] once it has succeeded once,
3155    /// **including after every identity it created has been deleted**.
3156    pub fn enroll_first_user(&self, name: &str, openssh: &str) -> Result<Enrolled> {
3157        self.install_first_identity(name, name, openssh)
3158    }
3159
3160    /// Install the first admin from a public key, on an empty table.
3161    ///
3162    /// This is the answer to "how does the first key get in on a box with no
3163    /// shell" **when the operator already has the key at boot time**: it comes
3164    /// from `--admin-key` and is installed here. It closes the enrolment window
3165    /// exactly as [`AuthTable::enroll_first_user`] does — otherwise a server
3166    /// started with `--admin-key` would still be standing there with an open
3167    /// unauthenticated door.
3168    ///
3169    /// Returns [`Bootstrap::AlreadyProvisioned`] and changes nothing if
3170    /// bootstrap has already happened, so it is safe to call on every boot.
3171    pub fn bootstrap_admin(&self, openssh: &str) -> Result<Bootstrap> {
3172        match self.install_first_identity(BOOTSTRAP_ADMIN_ID, "bootstrap admin", openssh) {
3173            Ok(enrolled) => Ok(Bootstrap::Installed(enrolled.principal)),
3174            Err(Error::AlreadyBootstrapped { .. }) | Err(Error::AlreadyProvisioned { .. }) => {
3175                tracing::debug!("identity table already provisioned; bootstrap key ignored");
3176                Ok(Bootstrap::AlreadyProvisioned)
3177            }
3178            Err(err) => Err(err),
3179        }
3180    }
3181
3182    /// **The start-up gate.** Install the admin key if one was supplied and
3183    /// nothing has been enrolled, then say what state this server is in.
3184    ///
3185    /// | bootstrap record | keys | `admin_key` | result |
3186    /// |---|---|---|---|
3187    /// | present | any | any | [`Readiness::Ready`], key ignored |
3188    /// | absent | any | `Some` | admin installed, [`Readiness::Ready`] |
3189    /// | absent | none | `None` | [`Readiness::AwaitingBootstrap`] — **door open** |
3190    /// | present | none | any | [`Readiness::SealedAndEmpty`] — door stays shut |
3191    ///
3192    /// The third row used to be a refusal to start, which is why gunnar had no
3193    /// first boot; see [`Readiness`].
3194    ///
3195    /// A read fault propagates as `Err`. It is never reported as "nothing is
3196    /// enrolled": that would open the door on a corrupt or unreadable document,
3197    /// which is the single worst failure this design can have.
3198    pub fn prepare_to_serve(&self, admin_key: Option<&str>) -> Result<Readiness> {
3199        if let Some(key) = admin_key {
3200            // Idempotent, and it seals. A no-op when bootstrap has happened.
3201            self.bootstrap_admin(key)?;
3202        }
3203        let doc = self.doc.load()?;
3204        if !doc.keys.is_empty() {
3205            return Ok(Readiness::Ready);
3206        }
3207        match &doc.bootstrap {
3208            Some(record) => Ok(Readiness::SealedAndEmpty {
3209                record: Some(Box::new(record.clone())),
3210            }),
3211            // A table with principals but no keys and no record is not a fresh
3212            // server either: `install_first_identity` refuses it, so reporting
3213            // the window as open here would be a status that disagrees with the
3214            // gate. `bootstrap_window_open` is the one predicate both use.
3215            None if !doc.principals.is_empty() => Ok(Readiness::SealedAndEmpty { record: None }),
3216            None => Ok(Readiness::AwaitingBootstrap),
3217        }
3218    }
3219
3220    /// **Is `Bootstrap.Enroll` servable without a credential right now?**
3221    ///
3222    /// The single predicate the gate and the status report both read, so a
3223    /// server can never say "closed" and then enrol, or say "open" and then
3224    /// refuse. Its three clauses are the three refusals
3225    /// [`AuthTable::install_first_identity`] performs, in the same order.
3226    pub fn bootstrap_window_open(&self) -> Result<bool> {
3227        let doc = self.doc.load()?;
3228        Ok(doc.bootstrap.is_none() && doc.principals.is_empty() && doc.keys.is_empty())
3229    }
3230}
3231
3232/// What [`AuthTable::enroll_first_user`] created: the principal, its key, and
3233/// the record that shut the door behind it.
3234#[derive(Clone, PartialEq, Eq, Debug)]
3235pub struct Enrolled {
3236    /// The principal that was created. Its id is also its account name.
3237    pub principal: Principal,
3238    /// The key it was created with.
3239    pub key: PublicKey,
3240    /// The record that shut the enrolment window.
3241    pub record: BootstrapRecord,
3242}
3243
3244#[cfg(test)]
3245mod name_index_tests {
3246    use super::*;
3247
3248    /// A row written by a server from before the `names` index existed has no
3249    /// index entry — and it must still be found by name, and its name must
3250    /// still be refused to a newcomer. Driven by writing the row the old way,
3251    /// straight into `principals` with no index, which nothing public can do
3252    /// any more; that is why this test lives here.
3253    #[test]
3254    fn a_row_from_before_the_index_resolves_by_name_and_keeps_it() {
3255        let store = crate::memory_control_store().expect("store");
3256        let auth = AuthTable::new(store).expect("table");
3257        let legacy = Principal::new("alice", "alice", PrincipalKind::User).unwrap();
3258        auth.doc
3259            .update(|doc| {
3260                doc.principals.insert("alice".into(), legacy.clone())?;
3261                Ok(())
3262            })
3263            .expect("write the old way");
3264        assert!(
3265            auth.doc.load().unwrap().names.is_empty(),
3266            "the fixture must be a row with NO index entry, or the test proves nothing"
3267        );
3268
3269        assert_eq!(
3270            auth.principal_by_name("alice").expect("read").map(|p| p.id),
3271            Some("alice".to_owned()),
3272            "a pre-index row must be found by its name"
3273        );
3274        let err = auth
3275            .add_principal(Principal::new("alice2", "alice", PrincipalKind::User).unwrap())
3276            .expect_err("a pre-index row's name is still taken");
3277        assert!(matches!(err, Error::NameExists { .. }), "{err}");
3278
3279        // Renaming it brings it into the index, and frees nothing that is an id.
3280        auth.rename_principal("alice", "Alice").expect("rename");
3281        assert_eq!(
3282            auth.principal_by_name("Alice").expect("read").map(|p| p.id),
3283            Some("alice".to_owned())
3284        );
3285        assert!(auth.principal_by_name("alice").expect("read").is_none());
3286    }
3287}
3288
3289#[cfg(test)]
3290mod certificate_fingerprint_tests {
3291    use super::{fingerprint_of_blob, fingerprint_of_certificate};
3292
3293    /// **Different certificates must fingerprint differently, and the same one
3294    /// must fingerprint the same.**
3295    ///
3296    /// The first half is what makes the value an identity at all. The second is
3297    /// what makes it storable: a fingerprint that varied per call would put a
3298    /// principal in the `AuthTable` that nothing could ever match.
3299    #[test]
3300    fn a_certificate_fingerprint_is_stable_and_distinguishing() {
3301        let a = b"-----BEGIN CERTIFICATE-----AAAA-----END CERTIFICATE-----";
3302        let b = b"-----BEGIN CERTIFICATE-----AAAB-----END CERTIFICATE-----";
3303
3304        assert_eq!(
3305            fingerprint_of_certificate(a),
3306            fingerprint_of_certificate(a),
3307            "the same certificate must fingerprint the same, or nothing could store one"
3308        );
3309        assert_ne!(
3310            fingerprint_of_certificate(a),
3311            fingerprint_of_certificate(b),
3312            "two certificates differing by one byte must not share a fingerprint"
3313        );
3314    }
3315
3316    /// **A certificate and an SSH key must never collide**, even on identical
3317    /// bytes.
3318    ///
3319    /// They cannot today — both are SHA256 over the input, so identical input
3320    /// gives identical output — and that is worth *asserting* rather than
3321    /// assuming, because the consequence is not cosmetic: a collision would let
3322    /// a credential of one kind be presented as the other and match a principal
3323    /// it was never issued to.
3324    ///
3325    /// This test therefore documents a REAL property of the current scheme: the
3326    /// two spaces are not separated. Registering a fingerprint registers the
3327    /// bytes, not the credential type. If that is ever not wanted, the fix is a
3328    /// domain separator in the hash input, and this test is where it will be
3329    /// noticed.
3330    #[test]
3331    fn the_two_credential_kinds_share_one_hash_space_and_that_is_recorded() {
3332        let bytes = b"identical bytes, two meanings";
3333        assert_eq!(
3334            fingerprint_of_certificate(bytes),
3335            fingerprint_of_blob(bytes),
3336            "if these ever differ, the hash grew a domain separator and the AuthTable's \
3337             existing entries are all invalidated — which is a migration, not a tidy-up"
3338        );
3339    }
3340}
3341
3342/// [`AuthTable::plan_room`]'s answer, and which seat lot a claim takes.
3343struct RoomPlan {
3344    answer: RoomAnswer,
3345    lot: Option<usize>,
3346}
3347
3348/// **The operator's server grant stops at an account's door; the account
3349/// admin approves a look** (2026-10-05). A child module so the identity table
3350/// keeps one file per concern.
3351pub mod operator_access;
3352
3353/// **The seller's mark** (2026-10-08): only a SERVER-scope grant makes a principal's namespace
3354/// the seller's own (`Namespace.owner_server_grant`) — what a selling shop leaves out when it
3355/// gives every person's account a fact.
3356#[cfg(test)]
3357mod seller_mark_tests {
3358    use super::*;
3359    use crate::model::{Grant, Level};
3360
3361    fn table() -> AuthTable {
3362        AuthTable::new(crate::memory_control_store().expect("store")).expect("table")
3363    }
3364
3365    fn person(auth: &AuthTable, id: &str) {
3366        auth.add_principal(Principal::new(id, id, PrincipalKind::User).unwrap())
3367            .expect("a person");
3368    }
3369
3370    /// ★ **A normal person never holds the seller's mark**: their own account, a namespace
3371    /// grant (even Admin, even on another account), a repository grant, being an employee —
3372    /// none of it is a server grant. A server grant, direct or through a group, is.
3373    #[test]
3374    fn only_a_server_grant_marks_the_seller_and_a_normal_person_never_holds_it() {
3375        let auth = table();
3376        for id in ["cust", "boss", "emp", "op", "staff", "ops-group"] {
3377            if id == "ops-group" {
3378                auth.add_principal(Principal::new(id, id, PrincipalKind::Group).unwrap())
3379                    .unwrap();
3380            } else {
3381                person(&auth, id);
3382            }
3383        }
3384        auth.grant(Grant::namespace("cust", "cust", Level::Admin))
3385            .unwrap();
3386        auth.grant(Grant::namespace("cust", "boss", Level::Admin))
3387            .unwrap();
3388        let _ = auth.enroll_employee(
3389            "boss",
3390            Principal::new("emp2", "emp2", PrincipalKind::User).unwrap(),
3391        );
3392        for normal in [
3393            "cust",
3394            "boss",
3395            "emp",
3396            "emp2",
3397            "nobody",
3398            crate::model::ANONYMOUS_PRINCIPAL_ID,
3399        ] {
3400            assert!(
3401                !auth.holds_server_grant(normal).unwrap(),
3402                "{normal} is a normal person (or nobody): never the seller"
3403            );
3404        }
3405        auth.grant(Grant::server("op", Level::Read)).unwrap();
3406        assert!(
3407            auth.holds_server_grant("op").unwrap(),
3408            "a server grant at any level is the seller's mark"
3409        );
3410        auth.grant(Grant::server("ops-group", Level::Write))
3411            .unwrap();
3412        auth.add_to_group("ops-group", "staff").unwrap();
3413        assert!(
3414            auth.holds_server_grant("staff").unwrap(),
3415            "through a group too"
3416        );
3417        assert!(
3418            !auth.holds_server_grant("cust").unwrap(),
3419            "and still never the customer"
3420        );
3421    }
3422}