Skip to main content

Accounts

Struct Accounts 

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

The credential store: accounts, invites, and the two files it owns.

Implementations§

Source§

impl Accounts

Source

pub fn open( path: PathBuf, keys_out: Option<SshKeysOut>, reserved: BTreeSet<String>, ) -> Result<Self, String>

Opens the store at path, creating an empty one if absent, and writes the generated authorized_keys if one is configured.

reserved is the set of names that already exist elsewhere — the --auth-file users — and can therefore never be issued.

§Errors

Returns a message when the file cannot be read, does not parse, or cannot be written back. Fatal by design: a store that half-loaded would silently drop somebody’s credential, and the failure would look like a revocation nobody performed.

Source

pub fn binding_actor_keys_into(self, path: PathBuf) -> Self

Lets a redemption bind one actor key by appending it to the operator’s trusted-keys file at path (D51).

Opt-in, and off unless the operator passed --invite-binds-keys. What it removes is the clerical half of admission: the operator still decides who is admitted, by issuing the invite, and the invite still carries the grants. What it stops requiring is a second out-of-band round trip in which a newcomer pastes a hex string to a human who pastes it into a file.

The key lands in the same file an operator would have edited, so nothing downstream learns a new source of trust: the existing mtime reload picks it up, allowed_signers is regenerated from it, and revoking is still deleting one line.

Source

pub fn path(&self) -> &Path

Where the store is persisted. The choir-ssh shim is pointed at it through the D31 handoff file.

Source

pub fn len(&self) -> usize

How many accounts have been issued.

Source

pub fn is_empty(&self) -> bool

Whether no account has been issued yet.

Source

pub fn generation(&self) -> u64

Counter bumped by every mutation, for callers caching anything derived from the store.

Source

pub fn acl(&self) -> Acl

The grants this store holds, as an Acl the node merges with the file table.

Built by rendering ACL lines and parsing them, rather than by constructing grants directly, so the two paths cannot drift into meaning different things by the same words.

Source

pub fn authenticate(&self, user: &str, secret: &str) -> Option<Principal>

Identifies a presented user:secret pair, in constant time against the stored hash, or None when it matches nothing.

An expired invite matches nothing, so expiry needs no sweep to take effect.

Source

pub fn invite(&self, issuer: &str, body: &Value) -> (u16, String)

Mints an invite for the account described by body, as issuer.

Returns the API’s (status, json). The secret half is in the response and nowhere else: only its hash is stored, so an invite that is lost is reissued rather than recovered.

Source

pub fn request_access(&self, body: &Value) -> (u16, String)

Records an access request and returns its claim link halves (D72).

The one write on this node an unauthenticated caller can perform. What makes that safe is that the row it writes authorizes nothing: it is not a credential, it does not appear in Accounts::acl, and Accounts::authenticate will not return it. Until an operator grants it, holding the secret proves only that you are the person who asked.

The queue is capped and swept here rather than by a timer, so the bound holds without anything having to be running.

Source

pub fn pending_request(&self, id: &str, secret: &str) -> Option<RequestSummary>

The request id, if it is pending and secret is its secret (D72).

Deliberately not part of Accounts::authenticate. That function answers “who is this”, and the honest answer for a pending request is nobody: it reaches no route, carries no grant and passes no check. Keeping it out means no arm anywhere else on this node has to remember to refuse it.

Source

pub fn pending_requests(&self) -> Vec<RequestSummary>

Every pending request, oldest first, for the operator’s queue (D72).

Oldest first because a queue is worked from the front, and because the id order is random — sorting by it would shuffle the list on every render for no reason a reader could follow.

Source

pub fn grant_request(&self, issuer: &str, body: &Value) -> (u16, String)

Turns a pending request into a live invite, as issuer (D72).

The id and the secret hash are carried over unchanged. That is the whole mechanism: the link the asker already holds is the link that starts working, so a grant needs no message sent, no address stored, and no second artefact to lose.

The account handle is minted here rather than at asking, so the string the op log carries forever is chosen by this node at the moment an operator said yes – never by the stranger, and never before anybody agreed to it (D46).

Source

pub fn decline_request(&self, body: &Value) -> (u16, String)

Drops a pending request (D72).

The asker’s link then renders exactly the page a link that was never valid renders – the one refusal the join page gives for every reason an invite does not work. Declining says nothing back: a node that distinguished “declined” from “never existed” would let a stranger probe which of their guesses had been read.

Source

pub fn redeem(&self, invite_id: &str, body: &Value) -> (u16, String)

Redeems invite_id, creating its account and minting its token.

The token is in the response and nowhere else. An optional ssh_key registers a key at the same time, which is what makes the SSH transport self-service rather than a second errand.

Source

pub fn mint_token(&self, user: &str) -> (u16, String)

Mints a fresh token for user, replacing any it had (D75).

The credential git and the CLI need, for an account whose sign-in credential is a passkey. Made on request rather than at redemption, because a secret nobody asked for is a secret nobody looks after – and because the person who wants one is, by then, somebody this node has already authenticated.

It replaces. One account, one token, so a rotation is the same act as a first mint and there is no set of live credentials to keep track of. The caller is told, because the old one stops working the moment this returns.

Source

pub fn revoke(&self, body: &Value) -> (u16, String)

Removes an account and any invite outstanding for the same name.

Deletion rather than a tombstone: the token stops authenticating on the next request, the grants leave the merged table, and the key leaves the generated authorized_keys. That is the property an append-only log could not have provided.

Source

pub fn enroll_passkey(&self, user: &str, body: &Value) -> (u16, String)

Enrols a WebAuthn credential on user’s own account (D39).

The caller is the account: this never takes a user from the body, so holding a credential is the whole authorization story and there is no way to spell “enrol a key on someone else”.

What this does not check, said plainly rather than implied by silence: possession. D39 scoped out a CBOR reader, so nothing here parses or verifies an attestation object; the node takes the public key the authenticated caller sends. Enrolling a key you do not hold gains you nothing — you still cannot sign with it — but a stolen token can enrol an attacker’s own authenticator and keep it. Two things bound that: the roster lists every enrolled credential, so it is visible rather than silent, and revocation deletes the account and its keys with it.

§Errors

400 for a missing or malformed field, a public key that is not a P-256 SubjectPublicKeyInfo, or 409 for a credential id already enrolled anywhere on this node; 404 when the caller has no account record, which is the case for an --auth-file operator.

Source

pub fn remove_passkey(&self, user: &str, body: &Value) -> (u16, String)

Removes one of user’s own enrolled credentials (D39).

The mirror of enrolment, and it exists for the same reason revocation does: a credential that cannot be withdrawn is not a credential, it is a permanent fact. A lost authenticator has to be removable by the person who still holds the token.

§Errors

400 without credential_id, and 404 when this account has no such credential enrolled.

Source

pub fn account_for_credential( &self, credential_id: &str, ) -> Option<(String, Vec<u8>)>

The SubjectPublicKeyInfo DER of one enrolled credential, ready for choir_identity::verify_webauthn_assertion (D39).

Keyed by (user, credential_id) rather than by credential id alone: an assertion names a credential, but the request already names a principal, and looking the key up under that principal is The account holding credential_id, and that credential’s public key (D71).

Sign-in runs the lookup the other way round from every other passkey path: an assertion arrives before anyone has said who they are, and the credential id is the only name in it. WebAuthn’s own answer is a discoverable credential carrying a user handle, which this deliberately does not depend on – a credential enrolled before that was asked for would then be one its owner could sign with but not sign in with, and the person holding it would have no way to tell those apart.

A credential id is enrolled at most once across the store, which Accounts::enroll_passkey enforces, so the first match is the only match.

Source

pub fn passkey_spki(&self, user: &str, credential_id: &str) -> Option<Vec<u8>>

what stops one account’s assertion from being spent as another’s.

Source

pub fn passkeys_json(&self, user: &str) -> Vec<Value>

One account’s own enrolled credentials, for the page that account manages them on (D39).

Separate from Accounts::list_json because the questions differ: the roster is the operator’s view of everyone and needs a node-wide read, while this is a person looking at their own keys and needs only their own credential. Returning an empty list for an unknown name is deliberate — an --auth-file operator has no account record, and that is “nothing enrolled”, not an error.

Source

pub fn has_token(&self, user: &str) -> bool

Whether user holds a token at all (D75).

False for a passwordless account, and false for a name with no account record. The account page asks so it can say “make one” rather than “replace the one you have”.

Source

pub fn has_account(&self, user: &str) -> bool

Whether this name has an account record at all, which is what decides between “you have no passkeys yet” and “this credential cannot hold one”.

Source

pub fn invite_summary(&self, invite_id: &str) -> Option<InviteSummary>

What an invite promises, for the page that shows a holder what they are about to accept (D57).

Only ever call this for an invite whose secret has already authenticated. Nothing here checks possession, so a caller that reaches it with an id alone has built an oracle: Some versus None would tell an anonymous stranger which invite ids exist and which accounts are pending on this node. The one caller is the join page, which gets the id from a successful Accounts::authenticate and never from the request.

Expiry is re-checked rather than assumed, so this cannot be the place a stale invite is presented as a live one.

Source

pub fn display_name(&self, user: &str) -> Option<String>

What to call user in something a person reads (D46).

None means “call it by its own name”, which is the answer in three different situations a caller must not try to tell apart: an account issued before D46, one issued with an explicit user, and one whose display name has been deleted. The third is the point of the field, so a caller that renders “unknown” or “deleted” for it undoes the deletion by announcing it. Render the handle and say nothing.

Source

pub fn roster(&self) -> BTreeMap<String, String>

Every handle the store can name, as (handle, display name).

The rendering half of the D46 ACL decision: grants are written against handles, and choir acl render regenerates the trailing comments from this. Accounts with no display name are omitted rather than listed as themselves — a comment repeating the handle is noise, and the file already says it.

Source

pub fn list_json(&self) -> Value

Everything the store holds except the secrets: who has an account, what they were granted, which public keys are registered, and which invites are outstanding.

Trait Implementations§

Source§

impl Debug for Accounts

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<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<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

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> Same for T

Source§

type Output = T

Should always be Self
Source§

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

Source§

type Error = Infallible

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

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.