Skip to main content

Module accounts

Module accounts 

Source
Expand description

Account and credential self-service (D36).

Adding a user used to mean the operator appending a line to the --auth-file by hand and restarting the daemon, then appending another to the --acl-file, then a third to an authorized_keys. This module is the same three facts issued through the node instead: one record per account holding the BLAKE3 of its token, the grants it was issued with, and its registered SSH keys.

Three properties carry the design.

Issuance is invite-only. There is no registration endpoint. An account exists because a holder of @node write minted a single-use, expiring invite naming it, and the invited person redeemed that invite. The invite is a credential: it is presented as ordinary basic auth, so the node’s 401 wall, its constant-time compare and D33’s per-user buckets all apply to redemption without a line of new code, and an anonymous request still reaches nothing. An unauthenticated redeem route would have been the one unmetered, unattributed way in.

Issued grants are enforced by the D29 table, not beside it. The store renders its grants as ACL lines and hands them to Acl::parse, and Accounts::acl returns the result. The node merges that with the file table, so the git chokepoint, crate::acl::api_denial, crate::acl::filter_response, D33’s @node exemption and the choir-ssh shim all grade an issued credential without any of them knowing this module exists. The store can never grant crate::acl::Scope::Node: node-wide authority stays operator- authored in the ACL file, so self-service cannot mint itself an auditor or a rate-limit exemption.

This is not in the op log, and that is the decision. D35 measured what a new OpKind variant costs: the fold has no tolerant arm, so every reader older than the variant stops materializing the log at the first op it does not know, totally rather than partially. Paying that for credentials would also buy the wrong property — the log is append-only and replayed in full by any @node auditor, and a token hash placed there could never be forgotten, while revocation’s whole contract is that it forgets. So the store is a node-owned file, rewritten in place, and revocation is deletion.

Structs§

Accounts
The credential store: accounts, invites, and the two files it owns.
InviteSummary
What an invite promises its holder, read by the join page (D57).
RequestSummary
One pending request, as the operator’s queue and the asker’s own waiting page read it (D72).
SshKeysOut
Where a generated authorized_keys goes and what the forced command in it should say (D31’s file, written by the node instead of by hand).

Enums§

Principal
Who a set of presented credentials turned out to be.

Constants§

DEFAULT_INVITE_SECS
How long an unredeemed invite stays usable when the caller names no lifetime: one day, which is longer than handing someone a secret takes and shorter than forgetting about it does.
FORMAT_VERSION
Version of the on-disk store. Every persisted struct carries one (invariant 1); new fields are additive and old files still load.
GRANTED_REQUEST_SECS
How long the invite a granted request becomes is good for (D72).
INVITE_PREFIX
Prefix every invite id carries, and a spelling validate_username refuses, so an invite can never name the same principal an account does.
MAX_ABOUT_CHARS
Longest about line a request may carry (D72). One line, not a letter: the operator reads a queue of these, and the decision they are making is whether to let somebody in, not whether the essay was good.
MAX_CREDENTIAL_ID_CHARS
Longest credential id accepted. The spec allows up to 1023 raw bytes; this is that ceiling in base64url, so nothing legitimate is refused and an unbounded string is.
MAX_INVITE_SECS
Longest lifetime an invite may be given. An invite is a bearer secret for an account that does not exist yet, so “expires eventually” is not the same promise as “expires”.
MAX_LABEL_CHARS
Longest passkey label. Long enough to say “work laptop, touch id”, short enough that a roster stays a roster.
MAX_PASSKEYS
Most WebAuthn credentials one account may enrol (D39). A person has a laptop, a phone and a hardware key; a hundred is not a person with many devices, it is a store being filled by something automated.
MAX_PENDING_REQUESTS
The most unanswered requests this node holds at once (D72).
REQUEST_PREFIX
Prefix on an access-request id (D72), so an id says which table it belongs to without a lookup. Disjoint from INVITE_PREFIX because the two live in different maps and a reader comparing a claim link against a log line should not have to guess which one they are holding.
REQUEST_TTL_SECS
How long an unanswered access request survives (D72).

Functions§

grants_acl
The grants in a store, as an Acl file body.
validate_actor_key
Checks an ed25519 actor public key and returns it lowercased.
validate_display_name
Checks a readable display name (D46).
validate_grant
Checks one grant column pair against the real ACL parser and returns it in canonical spelling.
validate_ssh_key
Checks an OpenSSH public key line and returns it without its comment.
validate_username
Checks a name the node will interpolate into an authorized_keys forced command and key on for every authorization decision.