mesh-vault: retire and rotate without the controller's lease
The controller ended by its unassignment and its lease key expired, so the gate on a handed lease kept retirement, rotation and node-login rotation off for good (issue 1126, A3). Drop the gate, the lease watch and the manifest's read of mesh-controller.lease; vault_status no longer reports a lease. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -72,7 +72,7 @@ func (l *loginsState) Keys() ([]string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// enrolling is a vault that mints, with the lease handed away, a loaded user list at revision 570, the bus on novox, the
|
||||
// enrolling is a vault that mints, with no lease heard (issue 1126), a loaded user list at revision 570, the bus on novox, the
|
||||
// nodes g14, ace and novox placed, their existing logins adopted, and hand acts of person.jochen's.
|
||||
type enrolling struct {
|
||||
testVault
|
||||
@@ -87,7 +87,6 @@ func newEnrolling(t *testing.T) *enrolling {
|
||||
e := &enrolling{testVault: v, accounts: &fakeAccounts{keys: map[string]AccountHash{}}, logins: &loginsState{keys: map[string]NodeLogin{}}, acts: map[string]HandActSeen{}}
|
||||
v.Accounts, v.Logins = e.accounts, e.logins
|
||||
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "g14": {"web"}, "ace": {"shop"}})
|
||||
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
|
||||
v.LoadedListHeard(570)
|
||||
v.TokenBase = func() (TokenBase, error) {
|
||||
return TokenBase{Broker: "tls://10.77.0.1:4222", Fingerprint: "sha256:bus"}, nil
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package main
|
||||
|
||||
// The controller's lease as the vault reads it (novox/hq ADR 0561 §3): handed away only to a holder that is no
|
||||
// controller, and not for a controller's value, a key deleted or expired, or one that cannot be read. It turns retirement
|
||||
// (retire.go) and rotation (rotation.go), never minting: the vault mints every wanted account it holds no value for,
|
||||
// whoever holds the lease (mint.go, issue 774). No clock and no setting turns it. A key's age passing is heard by no
|
||||
// watch, so the lease is read afresh before a retirement or a rotation (LeaseRead).
|
||||
|
||||
import "git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
|
||||
// LeaseHeard is the watch of the controller's lease: it records whether it is handed away, and says when that turned.
|
||||
func (v *Vault) LeaseHeard(raw []byte, found bool, readErr error) {
|
||||
if !v.setLease(raw, found, readErr) {
|
||||
return
|
||||
}
|
||||
on, why := v.leaseHanded()
|
||||
v.log("the controller's lease is %s: %s", map[bool]string{true: "HANDED AWAY", false: "not handed away"}[on], why)
|
||||
}
|
||||
|
||||
// setLease records what the lease says, and answers whether minting turned on or off by it.
|
||||
func (v *Vault) setLease(raw []byte, found bool, readErr error) bool {
|
||||
on, why := buscompose.LeaseHandedAway(raw, found, readErr)
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
flipped := on != m.on || m.why == ""
|
||||
if flipped {
|
||||
m.since = v.Now().UTC()
|
||||
}
|
||||
m.on, m.why = on, why
|
||||
return flipped
|
||||
}
|
||||
|
||||
// leaseAfresh reads the lease again before a retirement or a rotation, where the vault can: a key that expired since is heard by no watch.
|
||||
func (v *Vault) leaseAfresh() {
|
||||
if v.LeaseRead == nil {
|
||||
return
|
||||
}
|
||||
raw, found, err := v.LeaseRead()
|
||||
v.setLease(raw, found, err)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func build() *Env {
|
||||
watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) }
|
||||
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store},
|
||||
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant,
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant,
|
||||
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{},
|
||||
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")),
|
||||
Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")),
|
||||
@@ -229,34 +229,6 @@ func readWant(user string) ([]byte, bool, error) {
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// readLease is the controller's lease key, read through the runtime under the vault's own account (its manifest reads it).
|
||||
func readLease() ([]byte, bool, error) {
|
||||
e, err := stdio.State(LeaseRead).Get(LeaseKey)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// watchLease turns minting on and off as the controller's lease is written (lease.go, ADR 0561 §3): a watch, never a
|
||||
// poll; and the lease is read afresh before every mint. It answers whether the watch began.
|
||||
func watchLease(env *Env) bool {
|
||||
if env.Vault == nil {
|
||||
return true
|
||||
}
|
||||
env.Vault.LeaseHeard(readLease())
|
||||
err := stdio.State(LeaseRead).Watch(LeaseKey, func(c stdio.StateChange) error {
|
||||
deleted := c.Op == "delete" || c.Op == "purge" || len(c.Value) == 0
|
||||
env.Vault.LeaseHeard(c.Value, !deleted, nil)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
say("the controller's lease is not watched, so minting stays as last read; begun again at the next pass: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// adoptOperatorKey takes the operator key from the export the mesh writes to the vault's root (what
|
||||
// `operator key set` recorded; the controller keeps only a public half there), and seals again any
|
||||
// operator copy not yet sealed to the current key. Run every minute, so a failure partway is finished. It
|
||||
@@ -319,7 +291,6 @@ func main() {
|
||||
// The operator key first: nothing is kept without it, so a want heard before it would not be minted.
|
||||
env.pass()
|
||||
watched, watching := watchHandings(env), watchAssignments()
|
||||
lease := watchLease(env)
|
||||
wants := watchWants(env)
|
||||
leavers := watchLeavers(env)
|
||||
for {
|
||||
@@ -332,9 +303,6 @@ func main() {
|
||||
if !watching {
|
||||
watching = watchAssignments()
|
||||
}
|
||||
if !lease {
|
||||
lease = watchLease(env)
|
||||
}
|
||||
if !wants {
|
||||
wants = watchWants(env)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ package main
|
||||
// on `secret-changed` and composes.
|
||||
//
|
||||
// **The vault is the one issuer** (issue 774, research 072's gap "vault mints after the lease"): it mints every wanted
|
||||
// account it holds no value for, whoever holds the controller's lease, and the controller mints none once a vault is
|
||||
// installed (mesh-controller forwards `module issue` and its assignment's issue to issue-account). No account depends
|
||||
// on a node reporting to the controller. The lease still turns retirement and rotation (retire.go, rotation.go), and
|
||||
// vault_status names it. **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose
|
||||
// account it holds no value for, and the controller mints none once a vault is installed (mesh-controller forwards
|
||||
// `module issue` and its assignment's issue to issue-account). No account depends on a node reporting to the
|
||||
// controller. No lease turns minting, retirement or rotation: the controller ended by its unassignment (issue 1126).
|
||||
// **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose
|
||||
// hash it adopted, is never minted; the node accounts and the controller's own are never wanted; a value the controller
|
||||
// moves in never replaces one held (move.go), and one it hands with origin issued never replaces an account the vault
|
||||
// minted (AcceptTake): during the delivery an older controller may still mint, and the vault's value wins.
|
||||
@@ -38,23 +38,13 @@ import (
|
||||
// WantedRead is the grants part's state of wanted accounts, as the vault's manifest reads it.
|
||||
const WantedRead = buscompose.GrantsAccounts
|
||||
|
||||
// LeaseRead is the controller's lease, as the vault's manifest reads it, and LeaseKey its one key.
|
||||
const (
|
||||
LeaseRead = buscompose.ControllerLease
|
||||
LeaseKey = buscompose.ControllerLeaseKey
|
||||
)
|
||||
|
||||
// AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued).
|
||||
const AccountIssuedEvent = buscompose.AccountIssuedEvent
|
||||
|
||||
// minting is what the vault knows of the wanted accounts and of the controller's lease, kept in memory and read again
|
||||
// from the bus at every start: the grants part's state and the lease are the record, never this. on, why and since are
|
||||
// the lease (handed away or not), which turns retirement and rotation, never minting.
|
||||
// minting is what the vault knows of the wanted accounts, kept in memory and read again from the bus at every start:
|
||||
// the grants part's state is the record, never this.
|
||||
type minting struct {
|
||||
mu sync.Mutex
|
||||
on bool
|
||||
why string
|
||||
since time.Time
|
||||
mu sync.Mutex
|
||||
// wants are the wanted accounts, by user, as last heard.
|
||||
wants map[string]buscompose.WantedAccount
|
||||
// heldBack are the wanted users the vault did not mint, with why (vault_status).
|
||||
@@ -81,34 +71,25 @@ type Minted struct {
|
||||
Hashed bool `json:"hashed"`
|
||||
}
|
||||
|
||||
// MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774), and the lease,
|
||||
// which turns retirement and rotation.
|
||||
// MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774).
|
||||
type MintState struct {
|
||||
On bool `json:"on"`
|
||||
Why string `json:"why"`
|
||||
// Lease is whether the controller's lease is handed away, LeaseWhy why, and Since when that last turned.
|
||||
Lease bool `json:"lease_handed_away"`
|
||||
LeaseWhy string `json:"lease_why"`
|
||||
Since time.Time `json:"since,omitzero"`
|
||||
On bool `json:"on"`
|
||||
Why string `json:"why"`
|
||||
Wanted int `json:"wanted"`
|
||||
HeldBack map[string]string `json:"held_back"`
|
||||
Minted []Minted `json:"minted"`
|
||||
}
|
||||
|
||||
// mintingWhy is why the vault mints, whoever holds the lease.
|
||||
// mintingWhy is why the vault mints.
|
||||
const mintingWhy = "the vault is the one issuer of bus accounts: it mints every wanted account it holds no value for " +
|
||||
"and adopted no hash of, whoever holds the controller's lease (issue 774)"
|
||||
"and adopted no hash of (issue 774)"
|
||||
|
||||
// MintState answers how minting stands. A user held back whose account the vault holds a value for meanwhile (moved in,
|
||||
// handed, or minted by another path) is not said held back: it is not (issue 774, 2026-10-31).
|
||||
func (v *Vault) MintState() MintState {
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
out := MintState{On: true, Why: mintingWhy, Lease: m.on, LeaseWhy: m.why, Since: m.since, Wanted: len(m.wants),
|
||||
HeldBack: map[string]string{}, Minted: []Minted{}}
|
||||
if out.LeaseWhy == "" {
|
||||
out.LeaseWhy = leaseUnread
|
||||
}
|
||||
out := MintState{On: true, Why: mintingWhy, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}}
|
||||
wants := map[string]buscompose.WantedAccount{}
|
||||
for u, why := range m.heldBack {
|
||||
out.HeldBack[u] = why
|
||||
@@ -143,19 +124,6 @@ func (v *Vault) holdsAccount(w buscompose.WantedAccount) bool {
|
||||
return err == nil && BusAccountValue(secret, value)
|
||||
}
|
||||
|
||||
// leaseUnread is why nothing is retired or rotated before the lease is read.
|
||||
const leaseUnread = "the controller's lease has not been read yet, so nothing is retired or rotated"
|
||||
|
||||
// leaseHanded says whether the controller's lease is handed away, and why: what retirement and rotation wait for.
|
||||
func (v *Vault) leaseHanded() (bool, string) {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
if v.minting.why == "" {
|
||||
return false, leaseUnread
|
||||
}
|
||||
return v.minting.on, v.minting.why
|
||||
}
|
||||
|
||||
func (v *Vault) holdBack(user, why string) {
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
|
||||
@@ -5,7 +5,7 @@ package main
|
||||
// The vault whole against a real bus (novox/hq ADR 0561 §1 to §4, issue 774's acceptance 12): the built binary, played
|
||||
// over its real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.*
|
||||
// and mesh/publish answered from a real nats-server's key-value buckets and subjects), its store in a directory of its
|
||||
// own. A want written to the grants part's state is minted whoever holds the controller's lease (issue 774: the vault is
|
||||
// own. A want written to the grants part's state is minted with no lease asked (issue 774: the vault is
|
||||
// the one issuer): the hash is in bus-accounts, own answers the value sealed to the node, the value is the controller's
|
||||
// shape and its hash's password, account-issued is said; an adopted user is never minted.
|
||||
|
||||
@@ -330,7 +330,7 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-controller_lease",
|
||||
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts",
|
||||
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -372,14 +372,13 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
|
||||
_ = json.Unmarshal(raw, &s)
|
||||
return s.Minting
|
||||
}
|
||||
// The controller holds the lease, and the want is minted all the same.
|
||||
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
|
||||
// No lease key: the want is minted all the same (issue 1126).
|
||||
x, y := aWant("mod-x"), aWant("mod-y")
|
||||
p.put(t, "mesh-vault.bus-accounts", y.User, AccountHash{User: y.User, Hash: "$2a$04$adoptedadoptedadoptedadoptedadoptedadoptedadopte", Adopted: true})
|
||||
p.put(t, "mesh-grants.accounts", x.User, x)
|
||||
p.put(t, "mesh-grants.accounts", y.User, y)
|
||||
eventually(t, "the account minted under the controller's lease", func() bool { return p.hashOf(t, x.User) != nil })
|
||||
if st := status(); !st.On || st.Lease || len(st.HeldBack) != 0 {
|
||||
eventually(t, "the account minted", func() bool { return p.hashOf(t, x.User) != nil })
|
||||
if st := status(); !st.On || len(st.HeldBack) != 0 {
|
||||
t.Errorf("vault_status says %+v", st)
|
||||
}
|
||||
nodePub, nodePriv := aSealingKey(t)
|
||||
|
||||
@@ -24,9 +24,9 @@ func wantRaw(t *testing.T, w buscompose.WantedAccount) []byte {
|
||||
return raw
|
||||
}
|
||||
|
||||
// mintingVault is a vault with a state of bus accounts, ace's sealing key stated, ace's modules assigned, and minting
|
||||
// turned on or off as on says.
|
||||
func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) {
|
||||
// mintingVault is a vault with a state of bus accounts, ace's sealing key stated and ace's modules assigned. No lease is
|
||||
// heard: none turns minting, retirement or rotation (issue 1126).
|
||||
func mintingVault(t *testing.T) (testVault, *fakeAccounts, string) {
|
||||
t.Helper()
|
||||
plans := assignments{"ace.mod-x": `{"node":"ace","module":"mod-x"}`, "ace.mod-y": `{"node":"ace","module":"mod-y"}`}
|
||||
v := newVault(t, nil)
|
||||
@@ -37,11 +37,6 @@ func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) {
|
||||
if err := v.recordNodeKey("ace", pub, "node.ace"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if on {
|
||||
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
|
||||
} else {
|
||||
v.LeaseHeard([]byte(`{"instance":"novox/1/2026-10-27","epoch":57}`), true, nil)
|
||||
}
|
||||
return v, accounts, priv
|
||||
}
|
||||
|
||||
@@ -49,7 +44,7 @@ func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) {
|
||||
// shape as the module's broker, its hash in bus-accounts, said account-issued before secret-changed, and answered by own
|
||||
// sealed to the node; a value held or a hash adopted is never minted.
|
||||
func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) {
|
||||
v, accounts, nodePriv := mintingVault(t, true)
|
||||
v, accounts, nodePriv := mintingVault(t)
|
||||
w := aWant("mod-x")
|
||||
before := len(v.bus.events)
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
@@ -107,11 +102,11 @@ func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) {
|
||||
}
|
||||
|
||||
// 774/AC8, after research 072's gap "vault mints after the lease": the vault is the one issuer, so a wanted account is
|
||||
// minted while the controller still holds the lease (never an adopted one), vault_status says minting is on and the
|
||||
// lease apart, and a want held back is not said held back once the vault holds its value. During the delivery an older
|
||||
// minted with no lease asked (never an adopted one), vault_status says minting is on and names no lease (issue 1126),
|
||||
// and a want held back is not said held back once the vault holds its value. During the delivery an older
|
||||
// controller may still mint: its value handed with origin issued never replaces the account the vault minted.
|
||||
func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, false)
|
||||
v, accounts, _ := mintingVault(t)
|
||||
accounts.keys = map[string]AccountHash{"ace.mod-y": {User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}}
|
||||
for _, m := range []string{"mod-x", "mod-y"} {
|
||||
w := aWant(m)
|
||||
@@ -119,14 +114,14 @@ func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
|
||||
}
|
||||
minted, held, _ := v.Store.Read("own/ace/mod-x/broker")
|
||||
if !held {
|
||||
t.Fatalf("not minted under the controller's lease: %+v", v.MintState())
|
||||
t.Fatalf("not minted: %+v", v.MintState())
|
||||
}
|
||||
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held {
|
||||
t.Error("an adopted account was minted")
|
||||
}
|
||||
st := v.MintState()
|
||||
if !st.On || st.Lease || !strings.Contains(st.LeaseWhy, "controller holds the lease") || len(st.HeldBack) != 0 {
|
||||
t.Errorf("under the controller's lease vault_status says %+v", st)
|
||||
if !st.On || strings.Contains(st.Why, "lease") || len(st.HeldBack) != 0 {
|
||||
t.Errorf("vault_status says %+v", st)
|
||||
}
|
||||
// An older controller's covered account, handed with origin issued: refused, the vault's value kept.
|
||||
x := aWant("mod-x")
|
||||
@@ -156,7 +151,7 @@ func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
|
||||
// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh, whoever holds the lease;
|
||||
// it is refused for a node's account and the controller's, and for a user nobody wants; a user held already is said so.
|
||||
func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
|
||||
v, _, _ := mintingVault(t, false)
|
||||
v, _, _ := mintingVault(t)
|
||||
w := aWant("mod-x")
|
||||
raw := wantRaw(t, w)
|
||||
v.WantRead = func(user string) ([]byte, bool, error) {
|
||||
@@ -191,7 +186,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
|
||||
// over the second login `<user>--b`, never of a node's account, the controller's or an adopted hash, and `rotate` of a
|
||||
// covered account stays refused, so nothing rotates in place.
|
||||
func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
v, accounts, _ := mintingVault(t)
|
||||
for _, m := range []string{"mod-x", "mod-y"} {
|
||||
w := aWant(m)
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
@@ -227,7 +222,7 @@ func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) {
|
||||
|
||||
// A mint that failed (here: no operator key yet, so nothing can be kept) is tried again at the pass, never lost.
|
||||
func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
v, accounts, _ := mintingVault(t)
|
||||
key, _ := v.Store.OperatorKey()
|
||||
if err := v.Store.setOperatorKey("", "the test", v.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -250,7 +245,7 @@ func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) {
|
||||
// broker-management on 2026-10-30, issue 774) is replaced by a minted account when the grants part wants its user;
|
||||
// a held account still never is.
|
||||
func TestAHeldValueThatIsNoAccountIsReplacedByAMintedOne(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
v, accounts, _ := mintingVault(t)
|
||||
w := aWant("mod-x")
|
||||
if _, err := v.Put("own/ace/mod-x/broker", `{"password":"pw"}`, "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -268,10 +268,6 @@ func (v *Vault) RotateNode(caller string, a RotateNodeArgs) (RotateNodeAnswer, e
|
||||
if v.Accounts == nil {
|
||||
return RotateNodeAnswer{}, errors.New("the vault has no state of bus accounts to write to")
|
||||
}
|
||||
v.leaseAfresh()
|
||||
if on, why := v.leaseHanded(); !on {
|
||||
return RotateNodeAnswer{}, fmt.Errorf("no rotation begins: %s", why)
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
revision := v.minting.loadedRevision
|
||||
v.minting.mu.Unlock()
|
||||
@@ -596,10 +592,6 @@ func (v *Vault) RotationPass() {
|
||||
if err != nil || len(running) > 0 {
|
||||
return
|
||||
}
|
||||
v.leaseAfresh()
|
||||
if on, _ := v.leaseHanded(); !on {
|
||||
return
|
||||
}
|
||||
bus, err := v.busNode()
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -7,8 +7,8 @@ package main
|
||||
// retired in its store, keeping it (a value is deleted only by a person, ADR 0230). As ADR 0230 §15 and §18 have it, the
|
||||
// same set of accounts gone must stand for five passes and ten minutes first, and a set of more than three, or more than
|
||||
// half of the accounts held, waits for a person, who retires them with `account_retire`. Wanted again, an account is
|
||||
// re-enabled at once: the mark taken off and its hash written again. Nothing is retired while the controller holds the
|
||||
// lease (it forgets the accounts it mints), or before the wanted accounts are read whole.
|
||||
// re-enabled at once: the mark taken off and its hash written again. Nothing is retired before the wanted accounts are read
|
||||
// whole. No lease turns it: the controller ended by its unassignment (issue 1126).
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -135,8 +135,6 @@ func (v *Vault) RetirePass() {
|
||||
if v.Accounts == nil {
|
||||
return
|
||||
}
|
||||
v.leaseAfresh()
|
||||
on, leaseWhy := v.leaseHanded()
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
read := m.retiring.wantsRead
|
||||
@@ -150,9 +148,6 @@ func (v *Vault) RetirePass() {
|
||||
case !read:
|
||||
reset("the wanted accounts are not read whole yet, so none is known to be gone")
|
||||
return
|
||||
case !on:
|
||||
reset("nothing is retired: " + leaseWhy)
|
||||
return
|
||||
}
|
||||
gone, held, err := v.goneAccounts()
|
||||
if err != nil {
|
||||
@@ -239,12 +234,7 @@ func (v *Vault) RetireAccounts(caller string, users []string, why string) (any,
|
||||
if len(users) == 0 {
|
||||
return nil, errors.New("account_retire names the accounts (`users`), as vault_status lists them waiting")
|
||||
}
|
||||
// As the passes: never while the controller holds the lease (it forgets the accounts it mints), nor before the wanted
|
||||
// accounts are read whole (an account not read yet is not known to be gone).
|
||||
v.leaseAfresh()
|
||||
if on, why := v.leaseHanded(); !on {
|
||||
return nil, fmt.Errorf("nothing was retired: %s", why)
|
||||
}
|
||||
// As the passes: never before the wanted accounts are read whole (an account not read yet is not known to be gone).
|
||||
v.minting.mu.Lock()
|
||||
read := v.minting.retiring.wantsRead
|
||||
v.minting.mu.Unlock()
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// more than three, or more than half of those held, at once: past that it waits for a person (account_retire). Wanted
|
||||
// again, it is re-enabled at once; and a retired account is not hashed again at a start.
|
||||
func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
v, accounts, _ := mintingVault(t)
|
||||
clock := time.Date(2026, 10, 28, 9, 0, 0, 0, time.UTC)
|
||||
v.Now = func() time.Time { return clock }
|
||||
modules := []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8"}
|
||||
@@ -65,14 +65,7 @@ func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m2"}, "test"); err == nil {
|
||||
t.Error("retired by hand an account still wanted")
|
||||
}
|
||||
// Never while the controller holds the lease, nor before the wants are read whole.
|
||||
held := v.LeaseRead
|
||||
v.LeaseRead = func() ([]byte, bool, error) { return []byte(`{"instance":"novox/1","epoch":57}`), true, nil }
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") {
|
||||
t.Errorf("retired by hand under the controller's lease: %v", err)
|
||||
}
|
||||
v.LeaseRead = held
|
||||
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
|
||||
// Never before the wants are read whole; no lease is asked (issue 1126).
|
||||
v.minting.mu.Lock()
|
||||
v.minting.retiring.wantsRead = false
|
||||
v.minting.mu.Unlock()
|
||||
|
||||
@@ -4,9 +4,8 @@ package main
|
||||
// 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.
|
||||
// user list the bus loaded, and no lease turns it (issue 1126). `rotate` of a covered account stays refused, so nothing
|
||||
// rotates in place.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -42,9 +41,6 @@ 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()
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts", "mesh-controller_lease",
|
||||
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts",
|
||||
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -90,7 +90,8 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
|
||||
}
|
||||
go p.serve(stdout)
|
||||
|
||||
// The bus node (novox, running nats) and the node that rotates (g14); the lease handed away; g14's login adopted from
|
||||
// The bus node (novox, running nats) and the node that rotates (g14); no lease key anywhere (issue 1126: the controller
|
||||
// ended by its unassignment); g14's login adopted from
|
||||
// the loaded list, its password known to the engine.
|
||||
for node, modules := range map[string][]string{"novox": {"nats"}, "g14": {"web"}} {
|
||||
pl := delivery.Placement{V: delivery.Version, Node: node}
|
||||
@@ -108,7 +109,6 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
oldPassword := "the-old-password-of-g14"
|
||||
oldHash, _ := bcrypt.GenerateFromPassword([]byte(oldPassword), bcrypt.MinCost)
|
||||
p.put(t, "mesh-vault.bus-accounts", "node.g14", AccountHash{User: "node.g14", Hash: string(oldHash), Adopted: true})
|
||||
|
||||
@@ -307,7 +307,7 @@ func definitions(env *Env) []toolDef {
|
||||
{Name: Seat + ".issue-account",
|
||||
Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): the recovery when the vault did not " +
|
||||
"hear a want, and what the controller's `module issue` forwards to (issue 774). Reads the wanted key afresh from " +
|
||||
"mesh-grants.accounts and mints it as a want would, whoever holds the controller's lease: never for a node's " +
|
||||
"mesh-grants.accounts and mints it as a want would: never for a node's " +
|
||||
"account or the controller's, never a user nobody wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " +
|
||||
"whether it minted, the secret and generation, never the value or the hash.",
|
||||
Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")},
|
||||
@@ -645,7 +645,6 @@ func definitions(env *Env) []toolDef {
|
||||
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
|
||||
"key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " +
|
||||
"minting (novox/hq ADR 0561 §9, issue 774): the vault mints every wanted bus account it holds no value for, " +
|
||||
"whoever holds the controller's lease; whether that lease is handed away (it turns retirement and rotation), " +
|
||||
"every wanted account it holds back with why (never one whose value it holds), and those it minted.",
|
||||
run: func(string, map[string]any) (any, error) {
|
||||
if env.Vault == nil {
|
||||
|
||||
@@ -69,9 +69,7 @@ type Vault struct {
|
||||
WantRead func(user string) ([]byte, bool, error)
|
||||
// Placement reads the seat's placement (placed.go): where modules stand once the assignments are gone.
|
||||
Placement PlacementReader
|
||||
// LeaseRead reads the controller's lease afresh before a mint (lease.go): nil reads it only as the watch hands it.
|
||||
LeaseRead func() ([]byte, bool, error)
|
||||
// minting is the switch on the lease and what the vault knows of the wanted bus accounts (mint.go).
|
||||
// minting is what the vault knows of the wanted bus accounts (mint.go).
|
||||
minting minting
|
||||
// Signers is the vault's state `signers`, where it publishes its signing halves (signing.go, ADR 0550 §2): nil writes
|
||||
// none.
|
||||
|
||||
@@ -135,7 +135,7 @@ func filesUnder(t *testing.T, dir string) map[string]string {
|
||||
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
|
||||
// seat mesh-provisioner's plans, for what each node's plan names (ADR 0570 clause 2), (askers.go, design 57 V4), the node-engines' haves, where
|
||||
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted
|
||||
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the seat's placement (placed.go), in
|
||||
// accounts (mint.go, ADR 0561 §2), and the seat's placement (placed.go), in
|
||||
// main.go alone.
|
||||
func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
@@ -151,9 +151,9 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
"buckets are the accounts' hashes (ADR 0547 clause 9), the signing keys' public halves (ADR 0550 §2) and the "+
|
||||
"credentials' outcomes, names and generations alone (ADR 0570 clauses 3, 6) and each node's login in use (ADR 0583)", s)
|
||||
}
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` {
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` {
|
||||
t.Errorf("the manifest reads %s: the vault reads the seat mesh-provisioner's plans and provisions, the haves, the "+
|
||||
"grants part's wanted accounts, the lease and the placement alone", r)
|
||||
"grants part's wanted accounts and the placement alone, never the controller's lease (issue 1126)", r)
|
||||
}
|
||||
sources, _ := filepath.Glob("*.go")
|
||||
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
|
||||
@@ -195,9 +195,6 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
|
||||
// And the controller's lease, one read and one watch (ADR 0561 §3).
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Get(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Watch(", "", 1))
|
||||
// And the seat's placement, where modules stand (placed.go): its keys and one key, read.
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1))
|
||||
@@ -922,7 +919,7 @@ func TestTheRecordOfHandOutsIsBounded(t *testing.T) {
|
||||
// broker-nats-tools, wanted as a process account, has the random value the controller moved in replaced by a minted
|
||||
// account, and a value that is no account is refused for it from then on.
|
||||
func TestABusAccountIsKeptOnlyAsAnAccount(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
v, accounts, _ := mintingVault(t)
|
||||
const tools, address = "own/novox/nats/broker-nats-tools", "own/novox/mesh-controller/broker-address"
|
||||
for _, row := range []struct {
|
||||
name, value string
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"reads": [
|
||||
"node-engine.have",
|
||||
"mesh-grants.accounts",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-provisioner.plans",
|
||||
"mesh-provisioner.provisions"
|
||||
|
||||
Reference in New Issue
Block a user