package inventory import ( "context" "crypto/rand" "encoding/base64" "errors" "fmt" "sync/atomic" "time" "github.com/jackc/pgx/v5" "golang.org/x/crypto/bcrypt" ) // The bus's own users, as records. // // **Only the credential is kept here.** A user's *authority* is derived from what its module // declares, every time the file is written (novox/hq ADR 0043) — a stored copy of a permission list // would be a second account of a user's authority, able to disagree with the first, and the // disagreement would be invisible until somebody compared a composed file with a manifest. // // What cannot be derived is the password, and on the bus being built it has to outlive its own // minting: the whole user list is one file, rewritten whenever any of it changes, so a person's // access change would blank every module's password if the mesh kept nothing (design 25 §4, and the // migration beside this). // busPasswordCostForTest, when set, is the bcrypt cost a bus password is hashed at instead of the // library's default. Only ForTest sets it, to bcrypt's least (novox/hq issue 591): at the default // cost one hash takes most of a second under the race detector, and a hash at the least cost is the // same kind of hash, checked by the same comparison and by the bus the same way. var busPasswordCostForTest atomic.Int32 // busPasswordCost is the bcrypt cost a bus password is hashed at: the library's default, but in a test. func busPasswordCost() int { if c := busPasswordCostForTest.Load(); c != 0 { return int(c) } return bcrypt.DefaultCost } // hashBusPassword is the hash a bus password is kept as, minted, recorded or seeded alike. func hashBusPassword(password string) ([]byte, error) { return bcrypt.GenerateFromPassword([]byte(password), busPasswordCost()) } // BusUser is one user of the bus, as the mesh records it. type BusUser struct { Username string Kind string Node string Module string // PasswordHash is what the composed file carries. The plaintext is returned once, by Mint, and // then exists only where it was sealed. PasswordHash string // Sealed says its credential reached its node: an issue's mint is not sealed until then (ADR 0543 §2). Sealed bool } // The kinds of bus user the mesh records. The same words the composer uses, so a row and a // principal do not need a translation table between them. const ( BusController = "controller" BusNode = "node" BusModule = "module" BusEnrolment = "enrolment" BusPerson = "person" // BusNodeTools is a machine's tool runtime (novox/hq ADR 0175): named like the module it // stands for, recorded as what it is. BusNodeTools = "node-tools" // BusProvisioner is a machine's provisioner client (novox/hq ADR 0349 clause 11, design 57 P3): an account of // its own, named for the machine. BusProvisioner = "provisioner" // BusProcess is one process of a module that declares bus (novox/hq ADR 0436 §1, §2): an account of its // own, named for the machine, the module and the process. BusProcess = "process" ) // MintBusPassword makes a bus password and records its hash under a username, replacing whatever was // there, and returns the plaintext **once**. // // **Once is the whole contract.** The caller seals it to whoever will use it — into an enrolment // reply, into a module's sealed environment — and the mesh keeps only the hash, so a credential is // never recoverable from the store. A caller that loses it must mint again, which is a rotation and // is meant to feel like one. // RecordBusPassword records a hash for a password the caller already holds. // // **For the one credential the mesh does not choose**: an enrolment token's secret is the password // of the user that presents it (novox/hq ADR 0004, design 25 §6), so the token cannot be given a // minted password — it already has one, and the machine will connect with exactly that string. // Everything else goes through Mint, which chooses and returns the plaintext once. func (i *Inventory) RecordBusPassword(ctx context.Context, u BusUser, password string) error { if u.Username == "" || u.Kind == "" { return errors.New("a bus user needs a username and a kind") } if password == "" { return errors.New("a bus user needs a password") } hash, err := hashBusPassword(password) if err != nil { return fmt.Errorf("cannot hash a bus password: %w", err) } return i.writeBusUser(ctx, u, string(hash)) } // writeBusUser is the row, whoever chose the password, taken as sealed. func (i *Inventory) writeBusUser(ctx context.Context, u BusUser, hash string) error { return i.writeBusUserSealed(ctx, u, hash, true) } // writeBusUserSealed is the row, sealed now or not yet. func (i *Inventory) writeBusUserSealed(ctx context.Context, u BusUser, hash string, sealed bool) error { if _, err := i.store.Pool().Exec(ctx, `insert into bus_user (username, kind, node, module, password_hash, sealed_at) values ($1, $2, $3, $4, $5, case when $6 then now() end) on conflict (username) do update set kind = excluded.kind, node = excluded.node, module = excluded.module, password_hash = excluded.password_hash, minted_at = now(), sealed_at = excluded.sealed_at`, u.Username, u.Kind, u.Node, u.Module, hash, sealed); err != nil { return fmt.Errorf("cannot record the bus user %s: %w", u.Username, err) } return nil } // LockBusAccount holds the store's lock on one account until the answered release is called, so two issuers of it // (the assign act, the assignments' watch, a registration's process accounts) never interleave their look, mint and // seal: the hash the bus holds is always the password sealed to the node (novox/hq ADR 0543 §2, issue 1945). func (i *Inventory) LockBusAccount(ctx context.Context, username string) (func(), error) { conn, err := i.store.Pool().Acquire(ctx) if err != nil { return nil, fmt.Errorf("the lock on %s cannot be taken: %w", username, err) } if _, err := conn.Exec(ctx, `select pg_advisory_lock(hashtext($1))`, "bus_user/"+username); err != nil { conn.Release() return nil, fmt.Errorf("the lock on %s cannot be taken: %w", username, err) } return func() { _, _ = conn.Exec(context.WithoutCancel(ctx), `select pg_advisory_unlock(hashtext($1))`, "bus_user/"+username) conn.Release() }, nil } // ForgetBusAccountsOf forgets the bus accounts of module on node, and the credentials sealed to the node for them (its // `broker`, each process's `broker-`, its provisioner client's): an unassigned module keeps no account, so an // assignment again mints and seals afresh, never trusting a row whose credential the node no longer holds (issue 1945, // live on 2026-10-25 at 20:04: ace's mesh-build-go rows from 17:19 survived its unassignment, nothing was minted or // sealed again, and the module found no credential). func (i *Inventory) ForgetBusAccountsOf(ctx context.Context, node, module string) error { if node == "" || module == "" { return errors.New("forgetting the bus accounts of no node or no module would forget others' accounts") } tx, err := i.store.Pool().Begin(ctx) if err != nil { return err } defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }() if _, err := tx.Exec(ctx, `delete from bus_user where node = $1 and module = $2 and kind <> $3`, node, module, BusController); err != nil { return fmt.Errorf("the bus accounts of %s on %s are not forgotten: %w", module, node, err) } if _, err := tx.Exec(ctx, `delete from module_secret s using node n where s.node = n.id and n.name = $1 and s.module = $2 and (s.name = 'broker' or s.name like 'broker-%' or s.name = 'provisioner-broker')`, node, module); err != nil { return fmt.Errorf("the bus credentials of %s on %s are not forgotten: %w", module, node, err) } return tx.Commit(ctx) } // MintBusPasswordUnsealed mints u's password, replacing whatever stands, with no seal yet: the account is not issued // until MarkBusAccountSealed says its credential reached the node (ADR 0543 §2). func (i *Inventory) MintBusPasswordUnsealed(ctx context.Context, u BusUser) (string, error) { return i.mintBusPassword(ctx, u, false) } // MarkBusAccountSealed says the credential last minted for username is sealed to its node. func (i *Inventory) MarkBusAccountSealed(ctx context.Context, username string) error { _, err := i.store.Pool().Exec(ctx, `update bus_user set sealed_at = now() where username = $1`, username) return err } // BusAccountIssued says whether an account is minted, and whether its credential is sealed to its node. func (i *Inventory) BusAccountIssued(ctx context.Context, username string) (minted, sealed bool, err error) { var at *time.Time err = i.store.Pool().QueryRow(ctx, `select sealed_at from bus_user where username = $1`, username).Scan(&at) if errors.Is(err, pgx.ErrNoRows) { return false, false, nil } return err == nil, err == nil && at != nil, err } func (i *Inventory) MintBusPassword(ctx context.Context, u BusUser) (string, error) { return i.mintBusPassword(ctx, u, true) } // mintBusPassword mints, marked sealed when the caller seals it itself in the same act (every mint but an issue's). func (i *Inventory) mintBusPassword(ctx context.Context, u BusUser, sealed bool) (string, error) { if u.Username == "" || u.Kind == "" { return "", errors.New("a bus user needs a username and a kind") } raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("cannot generate a bus password: %w", err) } password := base64.RawURLEncoding.EncodeToString(raw) // The cost the server will pay on every connection. Left at the library's default rather than // raised: a node reconnecting after a network blip pays it, and the mesh's own links reconnect // far more often than a person logs in anywhere. hash, err := hashBusPassword(password) if err != nil { return "", fmt.Errorf("cannot hash a bus password: %w", err) } if err := i.writeBusUserSealed(ctx, u, string(hash), sealed); err != nil { return "", err } return password, nil } // BusUsers is every user the composed file should contain, by username. // // Returned as a map because the composer asks by username: the principals are derived from records // elsewhere, and this is only what each one's password is. A principal with no row here has no // password, and the composer refuses it rather than writing a user anybody is. func (i *Inventory) BusUsers(ctx context.Context) (map[string]BusUser, error) { rows, err := i.store.Pool().Query(ctx, `select username, kind, node, module, password_hash, sealed_at is not null from bus_user order by username`) if err != nil { return nil, err } defer rows.Close() out := map[string]BusUser{} for rows.Next() { var u BusUser if err := rows.Scan(&u.Username, &u.Kind, &u.Node, &u.Module, &u.PasswordHash, &u.Sealed); err != nil { return nil, err } out[u.Username] = u } return out, rows.Err() } // BusUserHash is one user's hash, or false when the mesh has never minted one for it. func (i *Inventory) BusUserHash(ctx context.Context, username string) (string, bool, error) { var hash string err := i.store.Pool().QueryRow(ctx, `select password_hash from bus_user where username = $1`, username).Scan(&hash) if errors.Is(err, pgx.ErrNoRows) { return "", false, nil } return hash, err == nil, err } // ForgetBusUser removes one user, so the next composition does not contain it. // // **Removal is what makes revocation real here.** On a bus with a management call, deleting an // account ends its connections; here the credential stops working when the file no longer names it, // which is the next composition — so forgetting the row and composing are one act, and a caller // that does the first without the second has revoked nothing. func (i *Inventory) ForgetBusUser(ctx context.Context, username string) error { _, err := i.store.Pool().Exec(ctx, `delete from bus_user where username = $1`, username) return err } // ForgetBusUsersOf removes every user belonging to one node — its host's, and every module assigned // to it. What a forgotten node leaves behind on the bus is otherwise a set of credentials for a // machine the mesh no longer knows. func (i *Inventory) ForgetBusUsersOf(ctx context.Context, node string) error { if node == "" { return errors.New("forgetting the bus users of no node would forget every user that has none") } _, err := i.store.Pool().Exec(ctx, `delete from bus_user where node = $1`, node) return err } // SeedBusUser records a hash of a credential the mesh did not mint, so a composition contains it. // // **Genesis is the reason this exists.** The controller's own user is created before the controller // runs — by the installer, at a well-known bootstrap password, the way the store's and the old bus's // are (`postgres:bootstrap`, `guest:guest`). Nothing minted it, so nothing recorded a hash for it, and // the controller's first composition would leave itself out of the very file it was writing: a bus // nothing can connect to, produced by the thing connected to it. // // Idempotent, and it does not overwrite. A credential the mesh *did* mint is the one that counts, so // once there is a row this does nothing — otherwise a restart would put the bootstrap password back // over a rotated one. func (i *Inventory) SeedBusUser(ctx context.Context, u BusUser, password string) error { if u.Username == "" || u.Kind == "" || password == "" { return errors.New("a bus user needs a username, a kind and the credential it is using") } hash, err := hashBusPassword(password) if err != nil { return fmt.Errorf("cannot hash a bus password: %w", err) } _, err = i.store.Pool().Exec(ctx, `insert into bus_user (username, kind, node, module, password_hash) values ($1, $2, $3, $4, $5) on conflict (username) do nothing`, u.Username, u.Kind, u.Node, u.Module, string(hash)) return err } // A person who may call the mesh's tools (novox/hq design 25 §7). // // **Their authority is a list of tools and nothing else.** Not a module: they hold no seat, nothing is // addressed to them, nothing is delivered to them, and they have no consumer to acknowledge. What // they have is permission to ask. // Person is somebody who may reach the mesh's tools. type Person struct { Name string // Invokes are the tools they may call, each `.`, or the single entry `*` for an // administrator. Invokes []string } // RecordPerson adds somebody, or changes what they may call. // // Replacing rather than merging: what a person may call is stated in full, so a change that meant to // remove a tool does remove it. A list that could only grow is a permission nobody can take back. func (i *Inventory) RecordPerson(ctx context.Context, p Person) error { if p.Name == "" { return errors.New("a person needs a name: it becomes their user on the bus") } if len(p.Invokes) == 0 { return fmt.Errorf( "%s may call nothing, so there is no reason for them to reach the mesh. Name the tools, "+ "or `*` for an administrator", p.Name) } _, err := i.store.Pool().Exec(ctx, `insert into person (name, invokes) values ($1, $2) on conflict (name) do update set invokes = excluded.invokes`, p.Name, p.Invokes) return err } // People is everybody who may reach the mesh's tools. func (i *Inventory) People(ctx context.Context) ([]Person, error) { rows, err := i.store.Pool().Query(ctx, `select name, invokes from person order by name`) if err != nil { return nil, err } defer rows.Close() var out []Person for rows.Next() { var p Person if err := rows.Scan(&p.Name, &p.Invokes); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } // ForgetPerson removes somebody and the credential they were given. // // **Both, or neither is a revocation.** A person's row gone and their bus user left behind is a // credential that still works and that nothing derives, which is the worst of both: it keeps working // and nobody can explain why. func (i *Inventory) ForgetPerson(ctx context.Context, name string) error { if name == "" { return errors.New("forgetting nobody would forget everybody") } if _, err := i.store.Pool().Exec(ctx, `delete from person where name = $1`, name); err != nil { return err } return i.ForgetBusUser(ctx, "person."+name) } // PutBusMembership records a machine's membership for the new bus, sealed to it (design 28, 5.2). // Replaces any earlier one: a machine has one membership per bus, and re-minting is re-telling. func (i *Inventory) PutBusMembership(ctx context.Context, nodeName, sealed string) error { node, err := i.NodeByName(ctx, nodeName) if err != nil { return err } _, err = i.store.Pool().Exec(ctx, `insert into bus_membership (node, sealed) values ($1, $2) on conflict (node) do update set sealed = excluded.sealed, since = now()`, node.ID, sealed) return err } // BusMemberships is every machine's sealed membership for the new bus, by node name. func (i *Inventory) BusMemberships(ctx context.Context) (map[string]string, error) { rows, err := i.store.Pool().Query(ctx, `select n.name, b.sealed from bus_membership b join node n on n.id = b.node`) if err != nil { return nil, err } defer rows.Close() out := map[string]string{} for rows.Next() { var name, sealed string if err := rows.Scan(&name, &sealed); err != nil { return nil, err } out[name] = sealed } return out, rows.Err() }