Skip to main content

AuthTable

Struct AuthTable 

Source
pub struct AuthTable { /* private fields */ }
Expand description

Principals, their keys, and the grants between them and repositories.

Cheap to clone; every clone reads and writes the same document.

Implementations§

Source§

impl AuthTable

Source

pub fn sso_provider(&self, account: &str) -> Result<Option<SsoProvider>>

An account’s identity provider, if it has one.

Source

pub fn sso_providers(&self) -> Result<Vec<SsoProvider>>

Every account’s identity provider. A listing.

Source

pub fn sso_account_for_domain(&self, domain: &str) -> Result<Option<String>>

The account an email domain leads to.

Source

pub fn set_sso_provider(&self, provider: SsoProvider) -> Result<SsoProvider>

Configure an account’s identity provider (replacing the one it had). The account must be a principal; its domains must be free or already its own — one domain, one company.

Source

pub fn remove_sso_provider(&self, account: &str) -> Result<Option<SsoProvider>>

Remove an account’s identity provider. Its directory stays: its employees are still employees (and a re-configured IdP finds them).

Source

pub fn sso_user(&self, principal_id: &str) -> Result<Option<SsoUser>>

One directory user, by principal id.

Source

pub fn sso_users_of(&self, account: &str) -> Result<Vec<SsoUser>>

An account’s directory (deleted users included; the caller filters).

Source

pub fn sso_principal_for_subject( &self, account: &str, subject: &str, ) -> Result<Option<String>>

The principal a subject is bound to in account, if any.

Source

pub fn sso_login(&self, login: &SsoLogin) -> Result<SsoLoginDone>

Commit one verified SSO login. In one update:

  1. the subject is looked up in THIS account’s index; a bound user must be an employee of the account and active;
  2. else a provisioned user (SCIM/CSV) of the IdP’s user name that no subject has claimed yet is bound to this subject — but ONLY on the admin’s binding (its externalId is one of the IdP’s stable ids for the person) or on an e-mail the IdP asserts is verified, that is the row’s address, in one of the account’s e-mail domains (A34-2). A user name (preferred_username) alone never claims; a user name another subject already holds is refused — never re-pointed;
  3. else a NEW employee is created (never an existing principal attached), bound, with room;

and the session key, when asked, is enrolled on the employee.

Source

pub fn sso_provision(&self, provision: &SsoProvision) -> Result<SsoUser>

Provision a NEW employee from the company’s directory (SCIM create, a CSV row): unbound until its first login claims it. A user name the account’s directory already holds is refused (Error::NameExists): SCIM’s uniqueness.

Source

pub fn sso_user_update( &self, account: &str, principal_id: &str, patch: &SsoUserPatch, actor: &str, now: UnixMillis, ) -> Result<(SsoUser, bool)>

Change a directory user: its names and, with active, whether it may log in — a deactivation gates the employee (holder crate::sso::gate_holder) and revokes its web sessions in the same update; a reactivation lifts that gate only (an operator’s suspension stays). Returns the row as it stands and whether active changed.

Source

pub fn sso_user_delete( &self, account: &str, principal_id: &str, actor: &str, now: UnixMillis, ) -> Result<SsoUser>

Remove a user from the directory (SCIM DELETE): deactivated — gated, sessions revoked — and no longer listed or claimable. The employee and everything it pushed stay; its user name is free for a NEW person, never for this one again.

Source

pub fn create_scim_token(&self, token: ScimToken) -> Result<ScimToken>

Store a new SCIM bearer (its hash).

Source

pub fn scim_token_by_hash(&self, hash: &str) -> Result<Option<ScimToken>>

The bearer a secret’s hash names, if any. One probe.

Source

pub fn scim_tokens_of(&self, account: &str) -> Result<Vec<ScimToken>>

An account’s SCIM bearers.

Source

pub fn revoke_scim_token( &self, account: &str, id: &str, ) -> Result<Option<ScimToken>>

Revoke one of an account’s bearers by its public id.

Source

pub fn touch_scim_token(&self, hash: &str, now: UnixMillis) -> Result<()>

Note a bearer’s use, at most once a minute.

Source

pub fn sso_groups_of(&self, account: &str) -> Result<Vec<SsoGroup>>

An account’s groups.

Source

pub fn put_sso_group(&self, group: SsoGroup) -> Result<SsoGroup>

Write a group (create or replace). Members must be the account’s own directory users; a display name is unique within the account.

Source

pub fn remove_sso_group( &self, account: &str, id: &str, ) -> Result<Option<SsoGroup>>

Remove one of an account’s groups.

Source

pub fn sso_live_sessions( &self, principal_id: &str, now: UnixMillis, ) -> Result<usize>

A pruned copy of the live web sessions of a directory user (for a listing): the ones still held and unexpired.

Source§

impl AuthTable

Source

pub fn clan(&self, name: &str) -> Result<Option<Clan>>

One clan, if there is one by that name.

Source

pub fn is_clan(&self, name: &str) -> Result<bool>

Is name a clan’s namespace?

Source

pub fn clans_of(&self, principal: &str) -> Result<Vec<Clan>>

The clans principal is in or invited to, by name.

Source

pub fn clan_role( &self, principal: &str, namespace: &str, ) -> Result<Option<ClanRole>>

Where principal stands in the clan whose namespace is namespace, or None when it is not a clan or principal is not in it.

Source

pub fn contributed_by(&self, account: &str) -> Result<u64>

What account gives all its clans, bytes — what its own room is short of by being in them. One probe and one per clan.

Source

pub fn room_left(&self, account: &str, room: AccountRoom) -> Result<Option<u64>>

What account could still give a clan: its room, less what it stores, what its employees and open seats hold, and what it gives its clans. None when nothing caps the account here.

Source

pub fn clan_for(&self, clan: &str, by: &str) -> Result<Clan>

The clan, if by is its admin — the refusal its admin verbs give otherwise, asked without writing (the plane checks this before it touches the catalog for a dissolve).

Source

pub fn create_clan( &self, name: &str, creator: &str, contribution: u64, room: AccountRoom, ) -> Result<Clan>

Make a clan: name (its namespace — the caller creates that in the catalog), creator its first admin, giving contribution bytes of its own room. The name must be no principal’s id and no clan’s; the creator must be an ordinary account (AuthTable::ordinary_account).

Source

pub fn invite_to_clan( &self, clan: &str, by: &str, principal: &str, ) -> Result<Clan>

Invite principal into clan — an admin’s (by). The account must be an ordinary one, not in the clan and not invited already.

Source

pub fn answer_clan_invite( &self, clan: &str, principal: &str, accept: bool, contribution: u64, room: AccountRoom, ) -> Result<Clan>

The invited account answers. accept: it joins, giving contribution bytes of its own room (checked against room, as any raise is); otherwise the invitation is declined. Only the invited account answers its own invitation.

Source

pub fn withdraw_clan_invite( &self, clan: &str, by: &str, principal: &str, ) -> Result<Clan>

An admin withdraws an invitation it (or another admin) made.

Source

pub fn set_clan_contribution( &self, clan: &str, member: &str, bytes: u64, room: AccountRoom, used: Option<u64>, lapsed: &[String], ) -> Result<Clan>

A member sets its own contribution to bytes. Up: out of its own room (room). Down: no lower than what the clan stores (used) leaves room for — None (not measured) refuses a lowering rather than guessing it is free.

Source

pub fn leave_clan( &self, clan: &str, member: &str, used: Option<u64>, lapsed: &[String], ) -> Result<Clan>

A member leaves: its contribution goes back to its own account. Refused while the clan uses it (used past what the others give), and for the clan’s last admin.

Source

pub fn remove_from_clan( &self, clan: &str, by: &str, member: &str, used: Option<u64>, lapsed: &[String], ) -> Result<Clan>

An admin removes a member — not itself (that is leaving), and not while the clan uses the member’s contribution; its contribution goes back to its own account.

Source

pub fn set_clan_admin( &self, clan: &str, by: &str, member: &str, admin: bool, ) -> Result<Clan>

An admin makes a member admin, or not. The last admin is never made a plain member.

Source

pub fn dissolve_clan(&self, clan: &str, by: &str) -> Result<Clan>

An admin dissolves the clan: every member’s contribution goes back, every invitation goes. The caller checks first that the clan’s namespace holds nothing, and removes the namespace after.

Source

pub fn offer_clan_repo( &self, clan: &str, by: &str, repo: &str, to: &str, ) -> Result<Clan>

The clan’s admin offers repo to account to. The plane checks first that the repository is the clan’s; this checks who may offer and to whom: an account (a person’s, a principal of its own — never a machine, a group, an employee or another clan), one open offer per repository.

Source

pub fn clan_repo_offer( &self, clan: &str, repo: &str, ) -> Result<Option<ClanRepoOffer>>

The open offer of clan/repo, if any.

Source

pub fn clan_offers_to(&self, to: &str) -> Result<Vec<(String, ClanRepoOffer)>>

Every open offer to account to, as (clan, offer), by clan. A listing: it walks the clans.

Source

pub fn withdraw_clan_repo_offer( &self, clan: &str, by: &str, repo: &str, ) -> Result<Clan>

The clan’s admin withdraws an offer.

Source

pub fn close_clan_repo_offer( &self, clan: &str, repo: &str, to: &str, ) -> Result<ClanRepoOffer>

The offer is answered (declined, or taken once the move is done): it goes, and the offer as it was comes back. Who may answer — the offered account’s admin — is the caller’s to have asked; this only checks the offer is the one named.

Source§

impl AuthTable

Source

pub fn shielded(&self, principal_id: &str, namespace: &str) -> Result<bool>

Is namespace closed to principal_id’s server grants right now? What a listing asks per row: a closed row is not shown.

Source

pub fn shielded_at( &self, principal_id: &str, namespace: &str, now: i64, ) -> Result<bool>

AuthTable::shielded at a given instant.

Source

pub fn account_of_namespace(&self, namespace: &str) -> Result<Option<String>>

The account namespace belongs to, if a person owns it.

Source

pub fn account_of_principal(&self, principal_id: &str) -> Result<Option<String>>

The account principal_id belongs to: its employer, or its own when it is a person. None for machines, groups and nobody.

Source

pub fn role_in_account( &self, actor: &str, account: &str, ) -> Result<&'static str>

What actor is to account, as its audit log says it: admin (its person, or one it made admin), member (its employee), machine, operator (a person with a server grant), outsider — or system for nobody.

Source

pub fn is_account_admin( &self, principal_id: &str, account: &str, ) -> Result<bool>

Is principal_id an admin of account? The account’s own person, or one its admin made admin over the namespace. A SERVER grant is not: the operator is no account’s admin.

Source

pub fn request_operator_access( &self, account: &str, operator: &str, reason: &str, minutes: u32, now: i64, ) -> Result<OperatorAccess>

The operator asks to look inside account. Pending until the account admin decides; a request already waiting is answered as is.

Source

pub fn decide_operator_access( &self, id: &str, approver: &str, approve: bool, minutes: Option<u32>, now: i64, ) -> Result<OperatorAccess>

The account admin decides a pending request. Approved, the look lasts the minutes asked for (or minutes, if the admin set fewer or more, up to a week).

Source

pub fn decide_operator_access_stepped( &self, id: &str, approver: &str, approve: bool, minutes: Option<u32>, step_up: Option<(&str, u32)>, now: i64, ) -> Result<OperatorAccess>

AuthTable::decide_operator_access with the approver’s STEP-UP stored in the same update: step_up = the passkey (its id as the table keys it, base64url) and the counter it presented — the passkey must be the approver’s, and its counter must move as a login’s does.

Source

pub fn revoke_operator_access( &self, id: &str, by: &str, now: i64, ) -> Result<OperatorAccess>

End a look (or withdraw a request) early: the account’s admin, or the operator that asked.

Source

pub fn operator_access( &self, account: Option<&str>, ) -> Result<Vec<OperatorAccess>>

The looks asked into account (or into every account, None), newest first, each as it stands now.

Source

pub fn operator_access_at( &self, account: Option<&str>, now: i64, ) -> Result<Vec<OperatorAccess>>

AuthTable::operator_access at a given instant.

Source

pub fn operator_access_by_id(&self, id: &str) -> Result<Option<OperatorAccess>>

One request by its id, as it stands now.

Source§

impl AuthTable

Source

pub fn new(store: Arc<dyn ControlStore>) -> Result<Self>

Open the identity table held in store (the certificate-identity plan).

Source

pub fn ref_name(&self) -> &str

The name the table is stored under.

Source

pub fn history(&self) -> Result<Vec<DocVersion>>

Every version this table has ever had, oldest first (the certificate-identity plan).

A ref update per change used to be a free audit trail; on a key/value store it is an explicit append-only table, and this is the door to it. Each entry’s blob is still readable through the control store, so “what did the ACL look like before Tuesday” is answerable from the server rather than from a log that may have rotated.

Source

pub fn add_principal(&self, principal: Principal) -> Result<Principal>

Add a principal.

Refuses ANONYMOUS_PRINCIPAL_ID Principal::new refuses it too, but this is the door a Principal::anonymous value would arrive at. A principal sitting in the table under that name would be handed the public-read rule of AuthTable::may and could then be given keys and grants on top of it.

The table stamps created with its own clock, whatever the caller put there: one writer for the field, so a row’s creation time is the server’s word and not the client’s.

The name is refused if another principal carries it — as its name or as its id (Error::NameExists). The id is what everything keys on; the name is the label; the label is still unique, in the same compare-and-swap as the row, so a listing can never show two rows a human reads as one.

Source

pub fn rename_principal(&self, id: &str, name: &str) -> Result<Principal>

Change a principal’s display name and nothing else.

The id — and with it the account namespace, every key, grant, membership and gate — stays exactly where it is; only the label moves. This is what makes a rename an edit rather than a re-identification: a console that pinned the id sees the same principal afterwards, and a URL, a policy file or a grant that names the id is untouched.

Refuses a name another principal already carries, as its name or as its id (Error::NameExists). Renaming to the name it already has is a no-op that succeeds.

Source

pub fn principal_by_name(&self, name: &str) -> Result<Option<Principal>>

One principal, by its display name. One index probe, then one row probe for a principal from before the index (whose name is its id).

This is the lookup a console makes at the one moment a name is anybody’s to give — registration — before it pins the id and stops asking by name.

Source

pub fn remove_principal(&self, id: &str) -> Result<Principal>

Remove a principal, its keys and its grants.

All three together, by construction: a principal whose keys outlived it is an account that still authenticates.

O(keys on the server). Keys are stored by fingerprint, because that is what the connection path looks them up by, so finding one principal’s keys is a walk. Deleting an account happens once a day, not once a connection; making it cheap would mean a second index to keep in step with the first, which is the twin that one implementation refuses.

Source

pub fn enroll_employee( &self, account: &str, principal: Principal, ) -> Result<Principal>

Enroll an employee: a NEW user, created by and bound to account.

The only way a principal becomes an employee, and it can only make a new one: the id must not exist (an existing principal “might have repos already, that would be a risk for the company”) and the name is checked as for every principal. In ONE compare-and-swap it writes the principal row, its name, the employee binding and a Read grant on the account’s namespace — a member sees the account’s repositories and events; what it may write is granted per repository by the account admin.

An employee gets NO account of its own (no namespace named after it; CoreControlPlane::enroll_employee refuses an id that names one), so the intrinsic “your own account” rule never makes it an admin of anything, and it may not make a repository public anywhere: that is the account admin’s (AuthTable::may_namespace at Admin).

Who may enroll (the account’s admin) is the caller’s question; the account must exist as a namespace, which the control plane checks. Only a PrincipalKind::User can be enrolled.

Source

pub fn enroll_with_room( &self, account: &str, principal: Option<Principal>, ask: &RoomAsk, ) -> Result<RoomAnswer>

Enroll with room (owner, 2026-10-04, “Add users”): ONE named employee holding room_bytes, or seats unnamed seats each holding it, or a seat claim (from_seat: the named employee takes an open seat and its room). The room comes out of what the account holds and has neither stored nor promised — budget minus the room its employees and open seats hold — and in the SAME compare-and-swap as the rows it writes, so two enrolments can never both spend the last of it. Short: Error::RoomShort naming what is missing, nothing written. A dry run answers with the shortfall instead and writes nothing; a retried order is answered with its first grant.

The rules AuthTable::enroll_employee keeps for a named employee hold here unchanged: a NEW principal only, a person, not the account.

Source

pub fn seats_of(&self, account: &str) -> Result<(u32, u32)>

The account’s seats: every seat granted, and the open ones.

Source

pub fn room_of(&self, principal_id: &str) -> Result<Option<EmployeeRoom>>

The room an employee holds, if any.

Source

pub fn employer_of(&self, principal_id: &str) -> Result<Option<String>>

The account that enrolled this principal, if it is an employee.

Source

pub fn employees_of(&self, account: &str) -> Result<Vec<Principal>>

Every employee an account has enrolled, by id. A listing: it walks the bindings, as members_of walks memberships.

Source

pub fn is_enterprise(&self, account: &str) -> Result<bool>

Is this an enterprise account? — derived, never set: an account whose admin has enrolled at least one employee.

Source

pub fn principal(&self, id: &str) -> Result<Option<Principal>>

One principal, if it exists.

Source

pub fn principals(&self) -> Result<Vec<Principal>>

Every principal. The listing path, O(principals).

Source

pub fn register_certificate( &self, principal_id: &str, der: &[u8], expires: Option<UnixMillis>, ) -> Result<PublicKey>

Register an authorized_keys-style public key line to a principal.

Refuses an unknown principal, and refuses a key that is already registered. Error::KeyExists says why re-pointing it would be worse than failing. Register an X.509 certificate to a principal.

The mTLS twin of register_key, and deliberately the same record: a Fingerprint is all gunnar knows about a credential, so a certificate and an SSH key differ in how they are presented, not in what is stored or in what any grant check afterwards does (the mTLS plan).

der is the certificate itself. algo is recorded as x509 so an operator listing keys can tell the two apart — which matters precisely because fingerprint_of_certificate and fingerprint_of_blob share one hash space, so the fingerprint alone cannot say which kind it is.

Not parsed. gunnar does not validate the certificate here and must not: validity, chain and revocation are the TLS handshake’s job, at connection time, against the configured CA. Checking a subset of that here would produce a second, weaker opinion about whether a credential is good — and the weaker opinion is the one an attacker picks.

Source

pub fn register_key( &self, principal_id: &str, openssh: &str, expires: Option<UnixMillis>, ) -> Result<PublicKey>

Register an OpenSSH public key line for principal_id, deriving the fingerprint here so no caller can store one that disagrees with the key.

Source

pub fn add_web_credential( &self, credential: WebCredential, ) -> Result<WebCredential>

Register a web credential (a passkey) on a person (Stage L).

Refuses an unknown principal, a principal that is not a USER (no human credential on a machine principal) and a credential id already registered — to anybody. The key’s shape is the caller’s to have checked; this stores what it is given.

Source

pub fn web_credential(&self, id: &str) -> Result<Option<WebCredential>>

One web credential by its id (base64url), or None.

Source

pub fn web_credentials( &self, principal_id: Option<&str>, ) -> Result<Vec<WebCredential>>

One principal’s web credentials, or every one. A listing path.

Source

pub fn revoke_web_credential(&self, id: &str) -> Result<Option<WebCredential>>

Revoke a web credential and the session keys its logins enrolled, in one update — the person’s console sessions on that passkey end with it. Returns the row removed, if there was one.

Source

pub fn web_login( &self, credential_id: &str, presented: u32, now: UnixMillis, openssh: &str, expires: UnixMillis, ) -> Result<(Principal, WebCredential, PublicKey)>

Commit a verified web login (Stage L), in one update: the passkey’s counter (re-checked here: it must increase when both it and the stored one are non-zero) and last use, and the session key openssh on the passkey’s principal, expiring at expires. No attestation is owed: the verified passkey assertion is the person’s consent. Returns the principal, the updated passkey and the key row.

Source

pub fn register_web_credential( &self, secret_hash: Option<&str>, principal_id: Option<&str>, credential: WebCredential, now: UnixMillis, ) -> Result<(WebCredential, Principal, Option<Level>)>

Register a passkey from a creation ceremony (Stage L), in one update: with secret_hash, the invite it names is redeemed — it must exist, be unexpired, and be for principal_id when one is named — and removed, and its server grant applied; without one, principal_id names the person. The person must be a USER and the credential id free. Returns the stored passkey, the person and the grant applied.

Source

pub fn approve_machine_credential( &self, machine: &str, level: Level, credential: WebCredential, step_up: Option<(&str, u32)>, now: UnixMillis, ) -> Result<(Principal, WebCredential, bool)>

Commit an approved machine enrolment (2026-10-05), in ONE update: the approver’s step-up passkey’s counter (re-checked: it must increase when both it and the stored one are non-zero) and last use; the MACHINE principal machine made when absent (a principal of that id that is not a machine is Error::NotAMachine); its server-wide grant set to level; and credential stored on it (an id already registered is Error::CredentialExists). The ONE way a web credential lands on a machine — AuthTable::add_web_credential and AuthTable::register_web_credential still refuse one. Returns the machine, the stored credential, and whether this update made the machine.

Source

pub fn rename_web_credential( &self, id: &str, label: &str, ) -> Result<Option<WebCredential>>

Change a web credential’s label. Returns the updated row, if there was one.

Source

pub fn create_invite(&self, invite: Invite) -> Result<Invite>

Store an invite (Stage L). The person must exist and be a USER; the id must be free. Who may invite is the caller’s question.

Source

pub fn invites(&self) -> Result<Vec<Invite>>

Every stored invite, expired ones included. A listing path.

Source

pub fn check_invite( &self, secret_hash: &str, now: UnixMillis, ) -> Result<InviteStanding>

What the invite with this secret hash is now (2026-10-19, Access.CheckInvite): valid, used, expired or unknown. Read-only — one load, nothing spent, nothing written.

Source

pub fn revoke_invite(&self, id: &str) -> Result<Option<Invite>>

Withdraw an invite. Returns the row removed, if there was one.

Source

pub fn revoke_key(&self, fingerprint: &Fingerprint) -> Result<Option<PublicKey>>

Revoke a key. Returns the record that was removed, if there was one.

Source

pub fn supersede_certificates( &self, principal_id: &str, spki: &[u8], ) -> Result<Vec<PublicKey>>

Revoke every certificate registered to principal_id that certifies a key OTHER than spki (a DER SubjectPublicKeyInfo), in one update. Returns the rows removed. SSH keys and certificates for spki itself (a renewal: the same key, a new validity window) are kept; a certificate row whose bytes do not parse certifies no key anyone can prove, and goes.

What re-certifying a SINGLE-HOLDER principal means (the twin’s gunnar-twin, twin reborn 2026-09-30): a new key is a new box, and the box it replaces — nuked, its disk possibly copied — must not keep a registered credential beside it.

Source

pub fn keys(&self) -> Result<Vec<PublicKey>>

Every registered key, O(keys). Also a listing path.

Includes expired keys, deliberately: gunnar key list must show a key that has run out, or nobody can see why authentication stopped working. Anything making a decision wants AuthTable::signing_keys instead.

Source

pub fn signing_keys(&self) -> Result<Vec<PublicKey>>

Every key that may still satisfy a signature requirement, as of now.

§Why this exists as its own method

Found 2026-08-05: gunnar-cli’s key_directory — the whole of the ssh-sig gate’s key material (the pre-receive gate) — built itself from AuthTable::keys with no expiry check at all. So a key past its expires could no longer open an SSH session, and could still sign a commit that require_signature accepted. That is one credential with two lifetimes, and the longer one was the one nobody had written down.

A revoked key is a removed row and so cannot reach either path, which is exactly why the bug hid: the revocation case, which is the one everybody tests, was already correct.

The fix is a named method rather than a filter at the call site because the call site is what got it wrong. “Every key” and “every key that may decide something” are different questions, and until now the second one had no way to be asked. AuthTable::principal_by_fingerprint_at answers the same question for authentication, through the same PublicKey::is_valid_at — one predicate, two consumers, so the two lifetimes cannot drift apart again (one implementation).

Source

pub fn signing_keys_at(&self, now: UnixMillis) -> Result<Vec<PublicKey>>

AuthTable::signing_keys at an explicit time, so expiry can be tested without waiting for it.

Source

pub fn attesting_keys_of(&self, principal_id: &str) -> Result<Vec<AttestingKey>>

The keys one principal could attest an addition with: its keys that are valid now and are OpenSSH key material rather than a certificate, since only those can make an sshsig. What crate::key_attestation::Requirement::of is handed.

O(keys on the server), for the reason AuthTable::keys_of is: it sits on Access.AddKey, which happens when a person gets a machine, not on a connection.

Source

pub fn keys_of(&self, principal_id: &str) -> Result<Vec<PublicKey>>

Every key registered to one principal. gunnar-policy needs this to verify an ssh-sig signature (the pre-receive gate).

O(keys on the server), for the reason AuthTable::remove_principal gives. It sits on the signature-verification path of a push, not on a connection.

Source

pub fn recipient_keys_of(&self, principal_id: &str) -> Result<Vec<PublicKey>>

The keys a key ring may be wrapped to (Repos.RecipientKeys, the forge’s gunnar-recipient-keys door): the principal’s keys valid NOW — an expired key is one its holder retired, and a ring wrapped to it opens for whoever holds that private half (wiring tracer B 2026-10-08). A certificate is never a recipient.

Source

pub fn principal_by_fingerprint( &self, fingerprint: &Fingerprint, ) -> Result<Option<Principal>>

The SSH auth answer: who owns this key, as of now?

None for an unregistered key, for a key whose principal has been removed, and for an expired key.

Source

pub fn principal_by_fingerprint_at( &self, fingerprint: &Fingerprint, now: UnixMillis, ) -> Result<Option<Principal>>

AuthTable::principal_by_fingerprint at an explicit time, so expiry can be tested without waiting for it.

Source

pub fn touch_key( &self, fingerprint: &Fingerprint, now: UnixMillis, ) -> Result<()>

Record that a key just authenticated.

Separate from the lookup because it writes, and the lookup is on the connection path: a listener that cannot write must still be able to authenticate.

§DELIBERATELY UNCALLED. Do not “fix” this by adding the call

Nothing in crates/*/src calls this, so PublicKey::last_used is None for every key on the server and Refs/Identity hand a consumer a field that is never written. That is a real gap and it is filed. It is not closed by wiring this into auth_publickey.

The cost is no longer the objection it was: sharding split the identity table, so this rewrites one leaf plus two trie nodes plus the document — single-digit KB below ~30 000 keys — and not the several hundred megabytes the finding was originally written against.

What forbids it is the shape, not the size. [DocRef::update] is a compare-and-swap on one reference, refs/gunnar/auth, with MAX_ATTEMPTS = 8. Calling this per successful authentication puts every login on the server in contention for that single ref, and the failure mode is not slowness: it is an Err after eight lost races, on the authentication path. A key that authenticated correctly would be refused because a bookkeeping write lost. last_used reading None is strictly better than that.

Either of two things closes it, and both are more than a call site: an append-only identity log, where “key K authenticated at T” is one appended row and needs no CAS; or a low-durability sidecar written best-effort off the connection path and allowed to lose updates. No sidecar exists in the tree today, which is why this is still here.

Source

pub fn register_token( &self, principal_id: &str, secret: &str, label: &str, expires: Option<UnixMillis>, ) -> Result<AccessToken>

Register an access token for principal_id, storing only its hash.

The secret is minted by the caller, not here, exactly as AuthTable::register_key is handed an authorized_keys line rather than generating a key pair. That keeps a random number generator out of this crate, and it keeps the one moment the secret exists in a place the operator can see: whoever calls this is the only code that ever holds it, and after this returns nothing in gunnar can produce it again.

Refuses an unknown principal, refuses ANONYMOUS_PRINCIPAL_ID (which cannot be in the table anyway, so this is the belt to AuthTable::add_principal’s braces), and refuses a secret that is already registered.

Source

pub fn revoke_token(&self, hash: &str) -> Result<Option<AccessToken>>

Revoke a token by its hash, which is what listing prints.

Source

pub fn tokens(&self) -> Result<Vec<AccessToken>>

Every registered token, O(tokens). A listing path, never an auth path.

Source

pub fn principal_by_token(&self, secret: &str) -> Result<Option<Principal>>

The HTTP auth answer: who holds this secret, as of now?

None for an unregistered secret, for a token whose principal has been removed, and for an expired token — the same three cases, in the same order, as AuthTable::principal_by_fingerprint, because they are the same question asked with a different credential.

Source

pub fn principal_by_token_at( &self, secret: &str, now: UnixMillis, ) -> Result<Option<Principal>>

AuthTable::principal_by_token at an explicit time, so expiry can be tested without waiting for it.

Source

pub fn touch_token(&self, hash: &str, now: UnixMillis) -> Result<()>

Record that a token just authenticated — at most once per LAST_USED_GRANULARITY per token.

Separate from the lookup for the same reason AuthTable::touch_key is: the lookup is on the request path and must work on a table that cannot be written.

The granularity is the fix for the serving tier’s measured ceiling. last_used lives in ONE control document shared by every principal and every token, so writing it per request made every authenticated HTTP request in the process contend on one compare-and-swap — MEASURED on 2026-08-11 at 573 → 1416 req/s (16-way ls-refs, 4-CPU cpuset) with this call the only difference, and again on 2026-08-12 by gunnar.multiuser_scaling: 102 users plateaued at 82 ops/s with the server stuck below 2 of 32 cores, 8 630 lost CAS attempts and 862 silently dropped updates in one 12-second window. A credential’s “when was this last seen” is a minute-grained fact; a fresh last_used answers the question and the skipped write is the throughput. The first touch of a quiet token still writes immediately.

Source

pub fn bulk_add_users( &self, users: &[BulkUser], now: UnixMillis, ) -> Result<usize>

Import many users — principal, optional access token, grants — in one document transaction.

The per-entity verbs commit one CAS each, and each CAS clones the document and rewrites its skeleton: at an appliance-scale import that is O(users²) work aimed at one shared pointer. This is the batch door the 1→100 000-user story needs: every insert lands in the in-memory document once, dirty shards are flushed once, and the whole population becomes durable in a single compare-and-swap — so the import is also atomic: a refused user refuses the batch, and a batch that lost the CAS race retries as one unit.

Token secrets never enter this crate: callers hand over token_hashes, exactly as AuthTable::register_token stores and AuthTable::principal_by_token looks up.

Source

pub fn grant(&self, grant: Grant) -> Result<Grant>

Add a grant, replacing any existing grant this principal has on the same scope.

Replacing rather than accumulating means a downgrade is a downgrade: if grants piled up, “set them to Read” would leave the old Write in place and the ACL would take the maximum.

Source

pub fn revoke_grant(&self, principal_id: &str, scope: &Scope) -> Result<bool>

Remove a principal’s grant on one scope. true if there was one.

Source

pub fn grants(&self, principal_id: Option<&str>) -> Result<Vec<Grant>>

Every grant, or every grant held by one principal.

Naming a principal is a shard probe; asking for all of them walks the whole ACL, which is what a listing is.

Source

pub fn add_to_group(&self, group_id: &str, member_id: &str) -> Result<()>

Make member_id a member of the group group_id.

The group must exist and be a PrincipalKind::Group; the member must exist, and may be a group itself (nesting). Idempotent: adding a member already in the group is a no-op, so a config that lists a membership twice does not create a duplicate edge.

This is admin-gated at the RPC exactly like AuthTable::grant, and for the same reason: adding a member to a group that holds a grant is giving that member the grant. The gate is the caller’s; here we only refuse memberships that do not typecheck.

Source

pub fn remove_from_group(&self, group_id: &str, member_id: &str) -> Result<bool>

Remove member_id from the group group_id. true if it was a member.

The access the group conferred is gone the moment this returns: the next AuthTable::may resolves the member’s groups afresh from the document this write updated, so there is no cached membership and no window. The tests clone through SSH before and after to prove the applied effect, not the row.

Source

pub fn groups_of(&self, member_id: &str) -> Result<Vec<String>>

The groups member_id belongs to directly (one shard probe).

Source

pub fn members_of(&self, group_id: &str) -> Result<Vec<String>>

Who is directly in this group. A listing, O(members on the server): membership is keyed by member for the connection path, so the inverse question is a walk. Sorted, for a stable listing.

Source

pub fn groups(&self) -> Result<Vec<Principal>>

Every group on the server — the principals whose kind is Group. Walks the principals, like the principal listing does.

Source

pub fn may(&self, principal_id: &str, repo: &Repo, level: Level) -> Result<bool>

May this principal do this to this repository?

The rules, in full:

  1. The anonymous principal — the one an unauthenticated caller is served as — may Read a Visibility::Public repository and may do nothing else, anywhere. It is not in the table, cannot be put in the table (Error::ReservedPrincipalId) and therefore can never hold a grant; this line is the whole of what “anonymous” can ever be allowed, and it is reached before the table is even read.
  2. An unknown principal may do nothing. Not “read-only” — nothing.
  3. A principal is unconditionally admin of its own account: the namespace whose name is its own id, which is the <account> in <host>/<account>/<repo>. See AuthTable::owns_account.
  4. A Visibility::Public repository is readable by any registered principal too, with no grant.
  5. Otherwise the highest grant covering the repository — server, namespace or repository scope — held by this principal or by any group it is (transitively) a member of must be at least level. Level is ordered, so Admin implies Write implies Read.

Groups (punkt 9). A group is a principal (PrincipalKind::Group) that holds no keys; a member inherits the group’s grants and nothing else. Inheritance is of grants only — never of a group’s intrinsic account ownership (rule 2 reads the querying principal’s own id, so being in a group named team does not make you admin of the team namespace). Nesting is resolved transitively and cycle-safely by AuthTable::grant_principals. The maximum over the inherited grants is taken, so a group granted Read can never let a member Write: a read-only group is read-only for everyone in it, by the same ordering that stops a read grant from becoming a write one for a lone user.

A namespace grant matches its namespace exactly: team does not reach team/sub. See Scope::Namespace.

§Why anonymous is a rule here and not a branch at the listener

One implementation. The SSH dispatch, the gRPC gate and anything added later ask this one function; a listener that decided “public, so serve it” for itself would be a second ACL, and the failure mode of two ACLs is that the permissive one wins silently. may is the only place a Visibility is ever read for an access decision.

A server-scope grant stops at a customer’s account (2026-10-05, the operator sees aggregates only): inside a namespace a PERSON owns, a PERSON’s server grant counts for nothing unless the account’s admin approved a look, and then up to Read (operator_access). A machine’s keeps its reach: it is the estate’s plumbing.

Source

pub fn may_at( &self, principal_id: &str, repo: &Repo, level: Level, now: i64, ) -> Result<bool>

AuthTable::may at a given instant (an operator’s look ends on time).

Source

pub fn may_each( &self, principal_id: &str, repos: &[Repo], level: Level, ) -> Result<Vec<bool>>

AuthTable::may for every repository of a listing, under ONE read of the identity table (round 5.3.1, test5 2026-10-08: a listing asked may once per row, and each ask read the table’s pointer from the control store again — two store reads and a handful of shared-cache locks per row, so concurrent listings queued on each other). The same rule, row by row: AuthTable::may_at is this with one row.

Source

pub fn require( &self, principal_id: &str, repo: &Repo, level: Level, ) -> Result<()>

AuthTable::may, as the error the SSH and gRPC layers want to return.

Source

pub fn may_namespace( &self, principal_id: &str, namespace: &str, level: Level, ) -> Result<bool>

May this principal create and delete repositories in this namespace?

Never, for the anonymous principal. Refused by construction rather than by the absence of a grant: absence is already an answer here, but an answer that depends on a table staying empty is a weaker promise than one that does not look at the table at all.

Source

pub fn may_server(&self, principal_id: &str, level: Level) -> Result<bool>

May this principal administer the server (create namespaces, register principals)?

Never, for the anonymous principal, for the reason AuthTable::may_namespace gives.

Source

pub fn holds_server_grant(&self, principal_id: &str) -> Result<bool>

Does this principal hold a SERVER-scope grant — any level, its own or through a group (2026-10-08)? The operator’s mark: a namespace whose owner holds one is the SELLER’s own (Namespace.owner_server_grant), never a customer a selling shop must give a fact. Unlike AuthTable::may_server the gate is not asked: a gated operator is still the seller (a fact given in the meantime would stay after the gate lifts). Only an operator grants Scope::Server; nothing a customer does — signing up, an invite, its own account, an employee’s enrolment, a namespace or repository grant — makes one.

Source

pub fn owns_account(principal_id: &str, namespace: &str) -> bool

Does this principal own this account?

An account is a namespace, and a principal owns the namespace whose name is its own id — github.com/<konto>/<repo>, with <konto> the user.

§Why this is a rule and not a grant

Account admin is intrinsic, not granted. A Grant { scope: Namespace(id), level: Admin } row would look identical from the outside and would be revocable: Access.Revoke would take a user’s own account away from them, and nothing would put it back, because nothing would remember that it was supposed to be there. This function reads no table, so there is no row to remove.

The collision it depends on is closed at the other end: a principal’s account namespace is created with the principal (CoreControlPlane::add_principal), so a namespace that already exists makes the principal fail to be created and a principal that already exists makes the namespace fail to be created. Neither name can be claimed twice, so this rule can never hand one person’s namespace to somebody else.

Associated rather than free so there is one spelling of the rule and both AuthTable::may and AuthTable::may_namespace use it (one implementation).

Source

pub fn gate(&self, gate: AccountGate) -> Result<AccountGate>

Lock an account out. Upserts: the newest writer owns the row.

Upsert rather than refuse-if-present on purpose. The two writers are a purge taking a lease and a person suspending an account, and in every ordering the later decision is the one that should stand — a refusal would mean an operator could not suspend an account a purge is holding, which is precisely when they most want to. What is protected instead is the lift: see AuthTable::lift_gate_held_by.

Returns the row as stored. Idempotent in the sense that matters: gating an already-gated account leaves it gated.

Source

pub fn gate_of(&self, account: &str) -> Result<Option<AccountGate>>

The gate on account, if one is in force.

None covers both “never gated” and “the lease expired”: to everything downstream those are the same fact, and reporting an expired lease as a lockout would tell an operator an account is shut when it is open.

Source

pub fn gate_covering(&self, account: &str) -> Result<Option<AccountGate>>

The gate that closes account, which may be on an ancestor namespace rather than on account itself — see AuthTable::gated, whose walk this is the reporting half of.

AuthTable::gate_of stays an exact lookup because Accounts.Ungate and the purge’s own release name a row, and lifting “whatever covers this” would let an ungate of alice/team silently lift the gate on alice. This one answers the different question a refusal has to answer: why can I not do this.

Source

pub fn gates(&self) -> Result<Vec<AccountGate>>

Every gate in force, by account. The listing path, O(gates).

Source

pub fn lift_gate(&self, account: &str) -> Result<Option<AccountGate>>

Lift the gate on account, whoever holds it. Returns what was there, or None if nothing was.

The operator’s override, and the reason a bricked account is always recoverable: it does not consult the holder, so a lease left behind by a process that no longer exists can still be cleared by hand.

Source

pub fn lift_gate_held_by( &self, account: &str, holder: &str, ) -> Result<Option<AccountGate>>

Lift the gate on account only if holder still holds it.

What a finishing purge calls. If an operator suspended the account while the purge was running, the row now names them, this returns None, and the suspension survives — a purge must not be able to un-suspend an account as a side effect of finishing.

The read and the remove are inside one [DocRef::update], so there is no window between checking the holder and clearing the row.

Source

pub fn renew_gate( &self, account: &str, holder: &str, expires: UnixMillis, ) -> Result<bool>

Push a lease’s deadline out, if holder still holds it.

The heartbeat a running purge makes after each repository it reclaims. Returns whether the row was renewed — false means the purge no longer holds the gate (an operator replaced it, or its lease expired and something else took the account), which the worker logs rather than treats as a failure: it is still emptying an account that is still shut.

Source

pub fn is_empty(&self) -> Result<bool>

Are there no keys at all? A fresh server, in other words.

This is not “has bootstrap happened”. See AuthTable::bootstrap_record.

Source

pub fn bootstrap_record(&self) -> Result<Option<BootstrapRecord>>

Has the unauthenticated enrolment window ever been used?

Some means it is shut, permanently, whatever the identity table now holds. Err means the table could not be read, and a caller must treat that as unknown, never as no: reading “cannot read” as “not bootstrapped” is precisely how the door reopens for real. Every caller in the tree propagates it; bootstrap_persistence.rs corrupts the document and asserts the enrolment RPC answers UNAVAILABLE rather than enrolling.

Source

pub fn counts(&self) -> Result<(usize, usize, usize)>

How many principals, keys and grants there are, for Admin.Stats.

Principals and keys are counted, not walked. Grant rows are not grant entries; one entry holds a principal’s whole list, so the third number costs a walk of the ACL. Admin.Stats is an operator asking a question, not a connection being served.

Source

pub fn enroll_first_user(&self, name: &str, openssh: &str) -> Result<Enrolled>

Enrol the first identity over the wire. The Bootstrap.Enroll RPC.

One name, one public key. The principal that comes out is gunnar-admin (a Server/Admin grant) and intrinsically admin of the account <name>; everything after this is ordinary authenticated administration through Access.*.

Refuses with Error::AlreadyBootstrapped once it has succeeded once, including after every identity it created has been deleted.

Source

pub fn bootstrap_admin(&self, openssh: &str) -> Result<Bootstrap>

Install the first admin from a public key, on an empty table.

This is the answer to “how does the first key get in on a box with no shell” when the operator already has the key at boot time: it comes from --admin-key and is installed here. It closes the enrolment window exactly as AuthTable::enroll_first_user does — otherwise a server started with --admin-key would still be standing there with an open unauthenticated door.

Returns Bootstrap::AlreadyProvisioned and changes nothing if bootstrap has already happened, so it is safe to call on every boot.

Source

pub fn prepare_to_serve(&self, admin_key: Option<&str>) -> Result<Readiness>

The start-up gate. Install the admin key if one was supplied and nothing has been enrolled, then say what state this server is in.

bootstrap recordkeysadmin_keyresult
presentanyanyReadiness::Ready, key ignored
absentanySomeadmin installed, Readiness::Ready
absentnoneNoneReadiness::AwaitingBootstrap — door open
presentnoneanyReadiness::SealedAndEmpty — door stays shut

The third row used to be a refusal to start, which is why gunnar had no first boot; see Readiness.

A read fault propagates as Err. It is never reported as “nothing is enrolled”: that would open the door on a corrupt or unreadable document, which is the single worst failure this design can have.

Source

pub fn bootstrap_window_open(&self) -> Result<bool>

Is Bootstrap.Enroll servable without a credential right now?

The single predicate the gate and the status report both read, so a server can never say “closed” and then enrol, or say “open” and then refuse. Its three clauses are the three refusals AuthTable::install_first_identity performs, in the same order.

Trait Implementations§

Source§

impl Clone for AuthTable

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for AuthTable

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more