Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/rotation.go
T

95 lines
3.9 KiB
Go

package main
// The record of a bus account's rotation, and its one rule (novox/hq ADR 0561 §4; issue 774, the lead's decision of
// 2026-10-27). A module's, a process's or a provisioner client's account rotates over two credentials (ADR 0114 §10):
// a second login, `<user>--b`, carrying the first login's permissions verbatim, which the bus composer composes only while
// the account's key in `bus-accounts` names it in `rotating`. The vault starts at most one rotation per revision of the
// user list the bus loaded. What composes and delivers the second login is a subtask of its own, before the lease write;
// until it is built nothing begins a rotation, and `rotate` of a covered account stays refused, so nothing rotates in
// place.
import (
"fmt"
"strings"
"time"
)
// SecondLoginSuffix makes an account's second login: reserved, so no module or process may be named to end in it.
const SecondLoginSuffix = "--b"
// Rotating is a rotation under way, as the account's key in bus-accounts says it: the second login, the generation of
// the value it carries, since when, and the revision of the loaded user list it began at.
type Rotating struct {
Login string `json:"login"`
Generation int `json:"generation"`
Since time.Time `json:"since"`
Revision uint64 `json:"revision"`
}
// LoadedListHeard keeps the revision of the user list the bus node says it loaded (its have of the bus module).
func (v *Vault) LoadedListHeard(revision uint64) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
if revision > v.minting.loadedRevision {
v.minting.loadedRevision = revision
}
}
// BeginRotation records a rotation of user's account at the loaded list's revision, under the one rule: none for a
// node's account or the controller's, none while minting is off, none of an account the vault did not make or was not
// handed, none twice for one account, and none when another began at this revision.
func (v *Vault) BeginRotation(user string) (Rotating, error) {
if nodeOrController(user) {
return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user)
}
if on, why := v.leaseHanded(); !on {
return Rotating{}, fmt.Errorf("no rotation begins: %s", why)
}
v.minting.mu.Lock()
revision := v.minting.loadedRevision
v.minting.mu.Unlock()
if revision == 0 {
return Rotating{}, fmt.Errorf("no rotation begins: the revision of the user list the bus loaded is not known yet")
}
users, err := v.Accounts.Users()
if err != nil {
return Rotating{}, err
}
var mine *AccountHash
began := ""
for _, u := range users {
a, err := v.Accounts.Get(u)
if err != nil {
return Rotating{}, err
}
if a == nil {
continue
}
if a.Rotating != nil && a.Rotating.Revision == revision {
began = u
}
if u == user {
mine = a
}
}
switch {
case mine == nil || mine.Adopted || mine.Of == "":
return Rotating{}, fmt.Errorf("the vault holds no account of %s it made or was handed, so it rotates none", user)
case mine.Rotating != nil:
return Rotating{}, fmt.Errorf("%s is rotating already, since %s (second login %s)", user, mine.Rotating.Since.Format(time.RFC3339), mine.Rotating.Login)
case strings.HasSuffix(user, SecondLoginSuffix):
return Rotating{}, fmt.Errorf("%s is a second login, which never rotates of its own", user)
case began != "":
return Rotating{}, fmt.Errorf("no rotation of %s begins: %s's began at the loaded list's revision %d, and one "+
"rotation begins per revision (ADR 0561 §4)", user, began, revision)
}
r := Rotating{Login: user + SecondLoginSuffix, Generation: mine.Generation + 1, Since: v.Now().UTC(), Revision: revision}
next := *mine
next.Rotating = &r
if err := v.Accounts.Put(next); err != nil {
return Rotating{}, err
}
v.log("a rotation of %s begins at the loaded list's revision %d: second login %s", user, revision, r.Login)
return r, nil
}