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
impl AuthTable
Sourcepub fn sso_provider(&self, account: &str) -> Result<Option<SsoProvider>>
pub fn sso_provider(&self, account: &str) -> Result<Option<SsoProvider>>
An account’s identity provider, if it has one.
Sourcepub fn sso_providers(&self) -> Result<Vec<SsoProvider>>
pub fn sso_providers(&self) -> Result<Vec<SsoProvider>>
Every account’s identity provider. A listing.
Sourcepub fn sso_account_for_domain(&self, domain: &str) -> Result<Option<String>>
pub fn sso_account_for_domain(&self, domain: &str) -> Result<Option<String>>
The account an email domain leads to.
Sourcepub fn set_sso_provider(&self, provider: SsoProvider) -> Result<SsoProvider>
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.
Sourcepub fn remove_sso_provider(&self, account: &str) -> Result<Option<SsoProvider>>
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).
Sourcepub fn sso_user(&self, principal_id: &str) -> Result<Option<SsoUser>>
pub fn sso_user(&self, principal_id: &str) -> Result<Option<SsoUser>>
One directory user, by principal id.
Sourcepub fn sso_users_of(&self, account: &str) -> Result<Vec<SsoUser>>
pub fn sso_users_of(&self, account: &str) -> Result<Vec<SsoUser>>
An account’s directory (deleted users included; the caller filters).
Sourcepub fn sso_principal_for_subject(
&self,
account: &str,
subject: &str,
) -> Result<Option<String>>
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.
Sourcepub fn sso_login(&self, login: &SsoLogin) -> Result<SsoLoginDone>
pub fn sso_login(&self, login: &SsoLogin) -> Result<SsoLoginDone>
Commit one verified SSO login. In one update:
- the subject is looked up in THIS account’s index; a bound user must be an employee of the account and active;
- 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
externalIdis 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; - 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.
Sourcepub fn sso_provision(&self, provision: &SsoProvision) -> Result<SsoUser>
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.
Sourcepub fn sso_user_update(
&self,
account: &str,
principal_id: &str,
patch: &SsoUserPatch,
actor: &str,
now: UnixMillis,
) -> Result<(SsoUser, bool)>
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.
Sourcepub fn sso_user_delete(
&self,
account: &str,
principal_id: &str,
actor: &str,
now: UnixMillis,
) -> Result<SsoUser>
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.
Sourcepub fn create_scim_token(&self, token: ScimToken) -> Result<ScimToken>
pub fn create_scim_token(&self, token: ScimToken) -> Result<ScimToken>
Store a new SCIM bearer (its hash).
Sourcepub fn scim_token_by_hash(&self, hash: &str) -> Result<Option<ScimToken>>
pub fn scim_token_by_hash(&self, hash: &str) -> Result<Option<ScimToken>>
The bearer a secret’s hash names, if any. One probe.
Sourcepub fn scim_tokens_of(&self, account: &str) -> Result<Vec<ScimToken>>
pub fn scim_tokens_of(&self, account: &str) -> Result<Vec<ScimToken>>
An account’s SCIM bearers.
Sourcepub fn revoke_scim_token(
&self,
account: &str,
id: &str,
) -> Result<Option<ScimToken>>
pub fn revoke_scim_token( &self, account: &str, id: &str, ) -> Result<Option<ScimToken>>
Revoke one of an account’s bearers by its public id.
Sourcepub fn touch_scim_token(&self, hash: &str, now: UnixMillis) -> Result<()>
pub fn touch_scim_token(&self, hash: &str, now: UnixMillis) -> Result<()>
Note a bearer’s use, at most once a minute.
Sourcepub fn put_sso_group(&self, group: SsoGroup) -> Result<SsoGroup>
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.
Sourcepub fn remove_sso_group(
&self,
account: &str,
id: &str,
) -> Result<Option<SsoGroup>>
pub fn remove_sso_group( &self, account: &str, id: &str, ) -> Result<Option<SsoGroup>>
Remove one of an account’s groups.
Sourcepub fn sso_live_sessions(
&self,
principal_id: &str,
now: UnixMillis,
) -> Result<usize>
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
impl AuthTable
Sourcepub fn clans_of(&self, principal: &str) -> Result<Vec<Clan>>
pub fn clans_of(&self, principal: &str) -> Result<Vec<Clan>>
The clans principal is in or invited to, by name.
Sourcepub fn clan_role(
&self,
principal: &str,
namespace: &str,
) -> Result<Option<ClanRole>>
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.
Sourcepub fn contributed_by(&self, account: &str) -> Result<u64>
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.
Sourcepub fn room_left(&self, account: &str, room: AccountRoom) -> Result<Option<u64>>
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.
Sourcepub fn clan_for(&self, clan: &str, by: &str) -> Result<Clan>
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).
Sourcepub fn create_clan(
&self,
name: &str,
creator: &str,
contribution: u64,
room: AccountRoom,
) -> Result<Clan>
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).
Sourcepub fn invite_to_clan(
&self,
clan: &str,
by: &str,
principal: &str,
) -> Result<Clan>
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.
Sourcepub fn answer_clan_invite(
&self,
clan: &str,
principal: &str,
accept: bool,
contribution: u64,
room: AccountRoom,
) -> Result<Clan>
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.
Sourcepub fn withdraw_clan_invite(
&self,
clan: &str,
by: &str,
principal: &str,
) -> Result<Clan>
pub fn withdraw_clan_invite( &self, clan: &str, by: &str, principal: &str, ) -> Result<Clan>
An admin withdraws an invitation it (or another admin) made.
Sourcepub fn set_clan_contribution(
&self,
clan: &str,
member: &str,
bytes: u64,
room: AccountRoom,
used: Option<u64>,
lapsed: &[String],
) -> Result<Clan>
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.
Sourcepub fn leave_clan(
&self,
clan: &str,
member: &str,
used: Option<u64>,
lapsed: &[String],
) -> Result<Clan>
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.
Sourcepub fn remove_from_clan(
&self,
clan: &str,
by: &str,
member: &str,
used: Option<u64>,
lapsed: &[String],
) -> Result<Clan>
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.
Sourcepub fn set_clan_admin(
&self,
clan: &str,
by: &str,
member: &str,
admin: bool,
) -> Result<Clan>
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.
Sourcepub fn dissolve_clan(&self, clan: &str, by: &str) -> Result<Clan>
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.
Sourcepub fn offer_clan_repo(
&self,
clan: &str,
by: &str,
repo: &str,
to: &str,
) -> Result<Clan>
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.
Sourcepub fn clan_repo_offer(
&self,
clan: &str,
repo: &str,
) -> Result<Option<ClanRepoOffer>>
pub fn clan_repo_offer( &self, clan: &str, repo: &str, ) -> Result<Option<ClanRepoOffer>>
The open offer of clan/repo, if any.
Sourcepub fn clan_offers_to(&self, to: &str) -> Result<Vec<(String, ClanRepoOffer)>>
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.
Sourcepub fn withdraw_clan_repo_offer(
&self,
clan: &str,
by: &str,
repo: &str,
) -> Result<Clan>
pub fn withdraw_clan_repo_offer( &self, clan: &str, by: &str, repo: &str, ) -> Result<Clan>
The clan’s admin withdraws an offer.
Sourcepub fn close_clan_repo_offer(
&self,
clan: &str,
repo: &str,
to: &str,
) -> Result<ClanRepoOffer>
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
impl AuthTable
Sourcepub fn shielded(&self, principal_id: &str, namespace: &str) -> Result<bool>
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.
Sourcepub fn shielded_at(
&self,
principal_id: &str,
namespace: &str,
now: i64,
) -> Result<bool>
pub fn shielded_at( &self, principal_id: &str, namespace: &str, now: i64, ) -> Result<bool>
AuthTable::shielded at a given instant.
Sourcepub fn account_of_namespace(&self, namespace: &str) -> Result<Option<String>>
pub fn account_of_namespace(&self, namespace: &str) -> Result<Option<String>>
The account namespace belongs to, if a person owns it.
Sourcepub fn account_of_principal(&self, principal_id: &str) -> Result<Option<String>>
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.
Sourcepub fn role_in_account(
&self,
actor: &str,
account: &str,
) -> Result<&'static str>
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.
Sourcepub fn is_account_admin(
&self,
principal_id: &str,
account: &str,
) -> Result<bool>
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.
Sourcepub fn request_operator_access(
&self,
account: &str,
operator: &str,
reason: &str,
minutes: u32,
now: i64,
) -> Result<OperatorAccess>
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.
Sourcepub fn decide_operator_access(
&self,
id: &str,
approver: &str,
approve: bool,
minutes: Option<u32>,
now: i64,
) -> Result<OperatorAccess>
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).
Sourcepub fn decide_operator_access_stepped(
&self,
id: &str,
approver: &str,
approve: bool,
minutes: Option<u32>,
step_up: Option<(&str, u32)>,
now: i64,
) -> Result<OperatorAccess>
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.
Sourcepub fn revoke_operator_access(
&self,
id: &str,
by: &str,
now: i64,
) -> Result<OperatorAccess>
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.
Sourcepub fn operator_access(
&self,
account: Option<&str>,
) -> Result<Vec<OperatorAccess>>
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.
Sourcepub fn operator_access_at(
&self,
account: Option<&str>,
now: i64,
) -> Result<Vec<OperatorAccess>>
pub fn operator_access_at( &self, account: Option<&str>, now: i64, ) -> Result<Vec<OperatorAccess>>
AuthTable::operator_access at a given instant.
Sourcepub fn operator_access_by_id(&self, id: &str) -> Result<Option<OperatorAccess>>
pub fn operator_access_by_id(&self, id: &str) -> Result<Option<OperatorAccess>>
One request by its id, as it stands now.
Source§impl AuthTable
impl AuthTable
Sourcepub fn new(store: Arc<dyn ControlStore>) -> Result<Self>
pub fn new(store: Arc<dyn ControlStore>) -> Result<Self>
Open the identity table held in store (the certificate-identity plan).
Sourcepub fn history(&self) -> Result<Vec<DocVersion>>
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.
Sourcepub fn add_principal(&self, principal: Principal) -> Result<Principal>
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.
Sourcepub fn rename_principal(&self, id: &str, name: &str) -> Result<Principal>
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.
Sourcepub fn principal_by_name(&self, name: &str) -> Result<Option<Principal>>
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.
Sourcepub fn remove_principal(&self, id: &str) -> Result<Principal>
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.
Sourcepub fn enroll_employee(
&self,
account: &str,
principal: Principal,
) -> Result<Principal>
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.
Sourcepub fn enroll_with_room(
&self,
account: &str,
principal: Option<Principal>,
ask: &RoomAsk,
) -> Result<RoomAnswer>
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.
Sourcepub fn seats_of(&self, account: &str) -> Result<(u32, u32)>
pub fn seats_of(&self, account: &str) -> Result<(u32, u32)>
The account’s seats: every seat granted, and the open ones.
Sourcepub fn room_of(&self, principal_id: &str) -> Result<Option<EmployeeRoom>>
pub fn room_of(&self, principal_id: &str) -> Result<Option<EmployeeRoom>>
The room an employee holds, if any.
Sourcepub fn employer_of(&self, principal_id: &str) -> Result<Option<String>>
pub fn employer_of(&self, principal_id: &str) -> Result<Option<String>>
The account that enrolled this principal, if it is an employee.
Sourcepub fn employees_of(&self, account: &str) -> Result<Vec<Principal>>
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.
Sourcepub fn is_enterprise(&self, account: &str) -> Result<bool>
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.
Sourcepub fn principals(&self) -> Result<Vec<Principal>>
pub fn principals(&self) -> Result<Vec<Principal>>
Every principal. The listing path, O(principals).
Sourcepub fn register_certificate(
&self,
principal_id: &str,
der: &[u8],
expires: Option<UnixMillis>,
) -> Result<PublicKey>
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.
Sourcepub fn register_key(
&self,
principal_id: &str,
openssh: &str,
expires: Option<UnixMillis>,
) -> Result<PublicKey>
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.
Sourcepub fn add_web_credential(
&self,
credential: WebCredential,
) -> Result<WebCredential>
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.
Sourcepub fn web_credential(&self, id: &str) -> Result<Option<WebCredential>>
pub fn web_credential(&self, id: &str) -> Result<Option<WebCredential>>
One web credential by its id (base64url), or None.
Sourcepub fn web_credentials(
&self,
principal_id: Option<&str>,
) -> Result<Vec<WebCredential>>
pub fn web_credentials( &self, principal_id: Option<&str>, ) -> Result<Vec<WebCredential>>
One principal’s web credentials, or every one. A listing path.
Sourcepub fn revoke_web_credential(&self, id: &str) -> Result<Option<WebCredential>>
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.
Sourcepub fn web_login(
&self,
credential_id: &str,
presented: u32,
now: UnixMillis,
openssh: &str,
expires: UnixMillis,
) -> Result<(Principal, WebCredential, PublicKey)>
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.
Sourcepub fn register_web_credential(
&self,
secret_hash: Option<&str>,
principal_id: Option<&str>,
credential: WebCredential,
now: UnixMillis,
) -> Result<(WebCredential, Principal, Option<Level>)>
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.
Sourcepub fn approve_machine_credential(
&self,
machine: &str,
level: Level,
credential: WebCredential,
step_up: Option<(&str, u32)>,
now: UnixMillis,
) -> Result<(Principal, WebCredential, bool)>
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.
Sourcepub fn rename_web_credential(
&self,
id: &str,
label: &str,
) -> Result<Option<WebCredential>>
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.
Sourcepub fn create_invite(&self, invite: Invite) -> Result<Invite>
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.
Sourcepub fn invites(&self) -> Result<Vec<Invite>>
pub fn invites(&self) -> Result<Vec<Invite>>
Every stored invite, expired ones included. A listing path.
Sourcepub fn check_invite(
&self,
secret_hash: &str,
now: UnixMillis,
) -> Result<InviteStanding>
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.
Sourcepub fn revoke_invite(&self, id: &str) -> Result<Option<Invite>>
pub fn revoke_invite(&self, id: &str) -> Result<Option<Invite>>
Withdraw an invite. Returns the row removed, if there was one.
Sourcepub fn revoke_key(&self, fingerprint: &Fingerprint) -> Result<Option<PublicKey>>
pub fn revoke_key(&self, fingerprint: &Fingerprint) -> Result<Option<PublicKey>>
Revoke a key. Returns the record that was removed, if there was one.
Sourcepub fn supersede_certificates(
&self,
principal_id: &str,
spki: &[u8],
) -> Result<Vec<PublicKey>>
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.
Sourcepub fn keys(&self) -> Result<Vec<PublicKey>>
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.
Sourcepub fn signing_keys(&self) -> Result<Vec<PublicKey>>
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).
Sourcepub fn signing_keys_at(&self, now: UnixMillis) -> Result<Vec<PublicKey>>
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.
Sourcepub fn attesting_keys_of(&self, principal_id: &str) -> Result<Vec<AttestingKey>>
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.
Sourcepub fn keys_of(&self, principal_id: &str) -> Result<Vec<PublicKey>>
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.
Sourcepub fn recipient_keys_of(&self, principal_id: &str) -> Result<Vec<PublicKey>>
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.
Sourcepub fn principal_by_fingerprint(
&self,
fingerprint: &Fingerprint,
) -> Result<Option<Principal>>
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.
Sourcepub fn principal_by_fingerprint_at(
&self,
fingerprint: &Fingerprint,
now: UnixMillis,
) -> Result<Option<Principal>>
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.
Sourcepub fn touch_key(
&self,
fingerprint: &Fingerprint,
now: UnixMillis,
) -> Result<()>
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.
Sourcepub fn register_token(
&self,
principal_id: &str,
secret: &str,
label: &str,
expires: Option<UnixMillis>,
) -> Result<AccessToken>
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.
Sourcepub fn revoke_token(&self, hash: &str) -> Result<Option<AccessToken>>
pub fn revoke_token(&self, hash: &str) -> Result<Option<AccessToken>>
Revoke a token by its hash, which is what listing prints.
Sourcepub fn tokens(&self) -> Result<Vec<AccessToken>>
pub fn tokens(&self) -> Result<Vec<AccessToken>>
Every registered token, O(tokens). A listing path, never an auth path.
Sourcepub fn principal_by_token(&self, secret: &str) -> Result<Option<Principal>>
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.
Sourcepub fn principal_by_token_at(
&self,
secret: &str,
now: UnixMillis,
) -> Result<Option<Principal>>
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.
Sourcepub fn touch_token(&self, hash: &str, now: UnixMillis) -> Result<()>
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.
Sourcepub fn bulk_add_users(
&self,
users: &[BulkUser],
now: UnixMillis,
) -> Result<usize>
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.
Sourcepub fn grant(&self, grant: Grant) -> Result<Grant>
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.
Sourcepub fn revoke_grant(&self, principal_id: &str, scope: &Scope) -> Result<bool>
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.
Sourcepub fn grants(&self, principal_id: Option<&str>) -> Result<Vec<Grant>>
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.
Sourcepub fn add_to_group(&self, group_id: &str, member_id: &str) -> Result<()>
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.
Sourcepub fn remove_from_group(&self, group_id: &str, member_id: &str) -> Result<bool>
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.
Sourcepub fn groups_of(&self, member_id: &str) -> Result<Vec<String>>
pub fn groups_of(&self, member_id: &str) -> Result<Vec<String>>
The groups member_id belongs to directly (one shard probe).
Sourcepub fn members_of(&self, group_id: &str) -> Result<Vec<String>>
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.
Sourcepub fn groups(&self) -> Result<Vec<Principal>>
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.
Sourcepub fn may(&self, principal_id: &str, repo: &Repo, level: Level) -> Result<bool>
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:
- The anonymous principal — the one an unauthenticated caller
is served as — may
ReadaVisibility::Publicrepository 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. - An unknown principal may do nothing. Not “read-only” — nothing.
- 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>. SeeAuthTable::owns_account. - A
Visibility::Publicrepository is readable by any registered principal too, with no grant. - 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.Levelis ordered, soAdminimpliesWriteimpliesRead.
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.
Sourcepub fn may_at(
&self,
principal_id: &str,
repo: &Repo,
level: Level,
now: i64,
) -> Result<bool>
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).
Sourcepub fn may_each(
&self,
principal_id: &str,
repos: &[Repo],
level: Level,
) -> Result<Vec<bool>>
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.
Sourcepub fn require(
&self,
principal_id: &str,
repo: &Repo,
level: Level,
) -> Result<()>
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.
Sourcepub fn may_namespace(
&self,
principal_id: &str,
namespace: &str,
level: Level,
) -> Result<bool>
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.
Sourcepub fn may_server(&self, principal_id: &str, level: Level) -> Result<bool>
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.
Sourcepub fn holds_server_grant(&self, principal_id: &str) -> Result<bool>
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.
Sourcepub fn owns_account(principal_id: &str, namespace: &str) -> bool
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).
Sourcepub fn gate(&self, gate: AccountGate) -> Result<AccountGate>
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.
Sourcepub fn gate_of(&self, account: &str) -> Result<Option<AccountGate>>
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.
Sourcepub fn gate_covering(&self, account: &str) -> Result<Option<AccountGate>>
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.
Sourcepub fn gates(&self) -> Result<Vec<AccountGate>>
pub fn gates(&self) -> Result<Vec<AccountGate>>
Every gate in force, by account. The listing path, O(gates).
Sourcepub fn lift_gate(&self, account: &str) -> Result<Option<AccountGate>>
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.
Sourcepub fn lift_gate_held_by(
&self,
account: &str,
holder: &str,
) -> Result<Option<AccountGate>>
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.
Sourcepub fn renew_gate(
&self,
account: &str,
holder: &str,
expires: UnixMillis,
) -> Result<bool>
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.
Sourcepub fn is_empty(&self) -> Result<bool>
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.
Sourcepub fn bootstrap_record(&self) -> Result<Option<BootstrapRecord>>
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.
Sourcepub fn counts(&self) -> Result<(usize, usize, usize)>
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.
Sourcepub fn enroll_first_user(&self, name: &str, openssh: &str) -> Result<Enrolled>
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.
Sourcepub fn bootstrap_admin(&self, openssh: &str) -> Result<Bootstrap>
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.
Sourcepub fn prepare_to_serve(&self, admin_key: Option<&str>) -> Result<Readiness>
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 record | keys | admin_key | result |
|---|---|---|---|
| present | any | any | Readiness::Ready, key ignored |
| absent | any | Some | admin installed, Readiness::Ready |
| absent | none | None | Readiness::AwaitingBootstrap — door open |
| present | none | any | Readiness::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.
Sourcepub fn bootstrap_window_open(&self) -> Result<bool>
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.