diff --git a/modules/mesh-vault/cmd/mesh-vault/intake.go b/modules/mesh-vault/cmd/mesh-vault/intake.go index 42bb41cd..9e76e0b6 100644 --- a/modules/mesh-vault/cmd/mesh-vault/intake.go +++ b/modules/mesh-vault/cmd/mesh-vault/intake.go @@ -323,9 +323,29 @@ func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) { if origin == "" { origin = OriginAccepted } + if origin == OriginIssued { + // **The vault's mint wins** (issue 774): during the delivery a controller older than the hand-over may still mint + // a covered account and hand it here; it never replaces an account the vault minted, judged under the store's + // lock, so the value and the hash the bus loaded stay the vault's. + return v.keepWith(c.secret, string(value), someoneOr(c.by, caller), caller, origin, keepOptions{refuse: v.vaultMinted}) + } return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, origin) } +// vaultMinted refuses a value handed for a secret holding an account the vault minted itself (mint.go): one is never +// replaced but by the vault's own rotation. +func (v *Vault) vaultMinted(before Record) string { + if before.Origin != OriginMade || before.ChangedBy != "the vault" || before.MovedFrom != nil { + return "" + } + if was, err := openAtRest(before, v); err == nil && BusAccountValue(before.Name, was) { + return before.Name + " holds a bus account the vault minted (generation " + fmt.Sprint(before.Generation) + + "): a value the controller issued never replaces it, so nothing was taken; the vault is the one issuer " + + "(issue 774), and a new value is the vault's rotation" + } + return "" +} + // ---- secret-ask --------------------------------------------------------------------------------------------- // AskOf is what an ask is for, and where. diff --git a/modules/mesh-vault/cmd/mesh-vault/lease.go b/modules/mesh-vault/cmd/mesh-vault/lease.go index 3a399d00..85264172 100644 --- a/modules/mesh-vault/cmd/mesh-vault/lease.go +++ b/modules/mesh-vault/cmd/mesh-vault/lease.go @@ -1,28 +1,20 @@ package main -// The switch on the controller's lease (novox/hq ADR 0561 §3): minting is on only while the lease is handed to a holder -// that is no controller, and off for a controller's value, a key deleted or expired, or one that cannot be read. The -// write that hands it away turns it on and mints every wanted account the vault holds no value for; a lease handed back -// turns it off. No clock and no setting turns it. A key's age passing is heard by no watch, so the lease is read afresh -// before every mint (LeaseRead): a lease that expired since mints nothing. +// 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. When minting turns on or off, every wanted account is judged again: -// minted when on, held back with the new why when off. +// 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.mintingOn() - v.log("minting is %s: %s", map[bool]string{true: "ON", false: "off"}[on], why) - for _, user := range v.wantedUsers() { - if w, ok := v.wanted(user); ok { - if _, err := v.mintWanted(w, "the lease write"); err != nil { - v.log("%s is not minted: %v", user, err) - } - } - } + 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. @@ -39,7 +31,7 @@ func (v *Vault) setLease(raw []byte, found bool, readErr error) bool { return flipped } -// leaseAfresh reads the lease again before a mint, where the vault can: a key that expired since is heard by no watch. +// 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 diff --git a/modules/mesh-vault/cmd/mesh-vault/mint.go b/modules/mesh-vault/cmd/mesh-vault/mint.go index a56b71d1..86f73cfd 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint.go @@ -10,10 +10,14 @@ package main // bcrypt hash into `bus-accounts`; and says `account-issued`, then `secret-changed`. The node's engine asks `own` again // on `secret-changed` and composes. // -// **One issuer at every moment** (§3): minting is on only while the controller's lease is handed to a holder that is no -// controller (mesh-sdk LeaseHandedAway). Until then the controller mints, the vault keeps hashing what it is handed, and -// vault_status names every wanted account it holds back and why. **Nothing is minted anew** (§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. +// **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 +// 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. // **Never two values for one user** (§7): a user whose hash the vault already wrote from another of its secrets is held // back, said, and never replaced but by a rotation. @@ -43,8 +47,9 @@ const ( // AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued). const AccountIssuedEvent = buscompose.AccountIssuedEvent -// minting is the switch and 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 and the lease are the record, never this. +// 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. type minting struct { mu sync.Mutex on bool @@ -76,41 +81,77 @@ type Minted struct { Hashed bool `json:"hashed"` } -// MintState is what vault_status says of minting. +// 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. type MintState struct { - On bool `json:"on"` - Why string `json:"why"` + 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"` Wanted int `json:"wanted"` HeldBack map[string]string `json:"held_back"` Minted []Minted `json:"minted"` } -// MintState answers how minting stands. +// mintingWhy is why the vault mints, whoever holds the lease. +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)" + +// 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() - defer m.mu.Unlock() - out := MintState{On: m.on, Why: m.why, Since: m.since, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}} - if out.Why == "" { - out.Why = "the controller's lease has not been read yet, so nothing is minted" + 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 } + wants := map[string]buscompose.WantedAccount{} for u, why := range m.heldBack { out.HeldBack[u] = why + if w, ok := m.wants[u]; ok { + wants[u] = w + } } for _, x := range m.minted { out.Minted = append(out.Minted, x) } + m.mu.Unlock() + for u, w := range wants { + if v.holdsAccount(w) { + delete(out.HeldBack, u) + } + } sort.Slice(out.Minted, func(i, j int) bool { return out.Minted[i].User < out.Minted[j].User }) return out } -// mintingOn says whether the vault mints now, and why. -func (v *Vault) mintingOn() (bool, string) { +// holdsAccount says the vault holds a bus account as w's own secret: a value that is an account. +func (v *Vault) holdsAccount(w buscompose.WantedAccount) bool { + secret, err := AccountSecret(w) + if err != nil { + return false + } + rec, held, err := v.Store.Read(secret) + if err != nil || !held { + return false + } + value, err := openAtRest(rec, v) + 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, "the controller's lease has not been read yet, so nothing is minted" + return false, leaseUnread } return v.minting.on, v.minting.why } @@ -248,11 +289,6 @@ func (v *Vault) mintOnce(w buscompose.WantedAccount, by string) (Minted, error) v.holdBack(w.User, why) return Minted{}, errHeldBack{why} } - v.leaseAfresh() - if on, why := v.mintingOn(); !on { - v.holdBack(w.User, "held back: "+why) - return Minted{}, errHeldBack{why} - } raw := make([]byte, 30) if _, err := rand.Read(raw); err != nil { return Minted{}, err @@ -360,8 +396,9 @@ func heldBackOrMinted(m Minted, err error) (any, error) { } // IssueAccount is the seat's verb issue-account (ADR 0561 §9): one wanted user minted now, its key read afresh from the -// grants part's state. The recovery when a want was not heard: it mints nothing a want would not, and is refused while -// minting is off, for a node's account and the controller's (never minted here, §4), and for a user nobody wants. +// grants part's state. The recovery when a want was not heard, and what the controller's `module issue` forwards to +// (issue 774): it mints nothing a want would not, answers "held already" for an account it holds, and is refused for a +// node's account and the controller's (never minted here, §4) and for a user nobody wants. func (v *Vault) IssueAccount(caller, user string) (any, error) { user = strings.TrimSpace(user) if user == "" { @@ -371,10 +408,6 @@ func (v *Vault) IssueAccount(caller, user string) (any, error) { return nil, fmt.Errorf("%s is never minted by the vault: a node's account and the controller's keep the hash they "+ "have (ADR 0561 §4)", user) } - v.leaseAfresh() - if on, why := v.mintingOn(); !on { - return nil, fmt.Errorf("not minted: %s", why) - } raw, found, err := []byte(nil), false, error(nil) if v.WantRead != nil { raw, found, err = v.WantRead(user) diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go index 133c3131..821a03c7 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go @@ -5,9 +5,9 @@ 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 only once the controller's lease is handed away: 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, and a lease handed back mints nothing. +// own. A want written to the grants part's state is minted whoever holds the controller's lease (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. import ( "bufio" @@ -297,7 +297,7 @@ func eventually(t *testing.T, what string, ok func() bool) { } // 774/AC12. -func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) { +func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) dir := t.TempDir() @@ -358,21 +358,16 @@ func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) { _ = json.Unmarshal(raw, &s) return s.Minting } - // The controller holds the lease: a want is held back, nothing is minted. + // 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}) - x, y, z := aWant("mod-x"), aWant("mod-y"), aWant("mod-z") + 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 want held back under the controller's lease", func() bool { - return strings.Contains(status().HeldBack[x.User], "controller holds the lease") - }) - if p.hashOf(t, x.User) != nil { - t.Fatal("minted under the controller's lease") + 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 { + t.Errorf("vault_status says %+v", st) } - // The write that hands the lease away mints it. - p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"}) - eventually(t, "the account minted at the lease write", func() bool { return p.hashOf(t, x.User) != nil }) nodePub, nodePriv := aSealingKey(t) if _, err := p.call(Seat+".node-key", "node.ace", map[string]any{"key": nodePub}); err != nil { t.Fatal(err) @@ -410,13 +405,4 @@ func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) { if !strings.Contains(said, AccountIssuedEvent+` {"`) || !strings.Contains(said, `"user":"ace.mod-x"`) || strings.Contains(said, held.Password) { t.Errorf("the vault said: %s", said) } - // Handed back to a controller: a new want mints nothing. - p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/2/2026-10-28", "epoch": 58}) - p.put(t, "mesh-grants.accounts", z.User, z) - eventually(t, "the new want held back once the lease is the controller's again", func() bool { - return strings.Contains(status().HeldBack[z.User], "controller holds the lease") - }) - if p.hashOf(t, z.User) != nil { - t.Error("minted after the lease was handed back") - } } diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_test.go index 9f7d2687..7ec6151c 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_test.go @@ -106,51 +106,55 @@ func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) { } } -// 774/AC8 (novox/hq ADR 0561 §3): while the controller holds the lease nothing is minted and vault_status says why and -// names each wanted account held back (never an adopted one); the write that hands the lease away mints them; a lease -// handed back, expired or unreadable turns minting off again, read afresh before every mint. -func TestNothingIsMintedUntilTheLeaseIsHandedAway(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 +// 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) 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) v.WantHeard(w.User, wantRaw(t, w), false) } - st := v.MintState() - if st.On || !strings.Contains(st.Why, "controller holds the lease") || len(st.HeldBack) != 1 || - !strings.Contains(st.HeldBack["ace.mod-x"], "controller holds the lease") { - t.Fatalf("under the controller's lease vault_status says %+v", st) - } - if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); held { - t.Fatal("minted while the controller holds the lease") - } - // Handed away: the wanted account is minted at that write, the adopted one is not. - lease := []byte(`{"handed-to":"the operator's hand-over"}`) - v.LeaseRead = func() ([]byte, bool, error) { return lease, true, nil } - v.LeaseHeard(lease, true, nil) - if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); !held { - t.Fatalf("not minted at the lease write: %+v", v.MintState()) + minted, held, _ := v.Store.Read("own/ace/mod-x/broker") + if !held { + t.Fatalf("not minted under the controller's lease: %+v", v.MintState()) } if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held { - t.Error("an adopted account was minted at the lease write") + t.Error("an adopted account was minted") } - if st := v.MintState(); !st.On || len(st.HeldBack) != 0 { - t.Errorf("after the lease write vault_status says %+v", st) + 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) } - // The key expired (no watch hears a key's age pass): read afresh before the next mint, nothing is minted. - v.LeaseRead = func() ([]byte, bool, error) { return nil, false, nil } + // An older controller's covered account, handed with origin issued: refused, the vault's value kept. + x := aWant("mod-x") + theirs, _ := buscompose.HeldCredential(x, "the-controllers-password") + if _, err := handIssued(t, v, ControllerCaller, "own/ace/mod-x/broker", string(theirs)); err == nil || + !strings.Contains(err.Error(), "the vault minted") { + t.Errorf("a controller's issued value over the vault's mint: %v", err) + } + if rec, _, _ := v.Store.Read("own/ace/mod-x/broker"); rec.Generation != minted.Generation || rec.ChangedBy != "the vault" { + t.Errorf("the vault's minted account was replaced: %+v", rec) + } + // Held back, and then held (moved in, handed): no longer said held back. z := aWant("mod-z") - v.WantHeard(z.User, wantRaw(t, z), false) - if _, held, _ := v.Store.Read("own/ace/mod-z/broker"); held { - t.Error("minted after the lease's key expired") + v.minting.mu.Lock() + v.minting.wants[z.User] = z + v.minting.mu.Unlock() + v.holdBack(z.User, "held back for a reason that has passed") + value, _ := buscompose.HeldCredential(z, "moved-in") + if _, err := v.keep("own/ace/mod-z/broker", string(value), "the move", ControllerCaller, OriginMade); err != nil { + t.Fatal(err) } - if st := v.MintState(); st.On || !strings.Contains(st.HeldBack["ace.mod-z"], "no lease is written") { - t.Errorf("after the key expired vault_status says %+v", st) + if st := v.MintState(); st.HeldBack[z.User] != "" { + t.Errorf("an account the vault holds is said held back: %+v", st.HeldBack) } } -// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh; it is refused while -// minting is off, for a node's account and the controller's, and for a user nobody wants; a user held already is said so. +// 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) w := aWant("mod-x") @@ -162,7 +166,6 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) { return nil, false, nil } for user, says := range map[string]string{ - "ace.mod-x": "controller holds the lease", "node.ace": "never minted", "controller": "never minted", } { @@ -170,7 +173,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) { t.Errorf("%s: %v, want a refusal saying %q", user, err, says) } } - v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil) + // Minted while the controller still holds the lease (issue 774): `module issue` forwards here. if _, err := v.IssueAccount("person.jochen", "ace.nobody"); err == nil || !strings.Contains(err.Error(), "no wanted account") { t.Errorf("a user nobody wants: %v", err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/ownnode.go b/modules/mesh-vault/cmd/mesh-vault/ownnode.go index 648fc6fe..84fec61e 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ownnode.go +++ b/modules/mesh-vault/cmd/mesh-vault/ownnode.go @@ -8,7 +8,6 @@ package main // provisioner is left out: its client fills its secrets from `get` (ADR 0349 §17). import ( - "encoding/json" "errors" "fmt" "slices" @@ -38,6 +37,11 @@ type OwnAnswer struct { Own []NodeOwnSecret `json:"own"` Credentials []NodeCredential `json:"credentials,omitempty"` Missing []CredentialAsked `json:"missing,omitempty"` + // Handed is, by module of the machine, whether the vault judged it handed to the provisioner (catalogue.AssignmentHands + // of the plan in its assignment), and so left its own secrets out of Own: the node composing itself hands exactly + // those (mesh-sdk compose/frombus HandedReader, issue 774). Every module it holds a secret of, and every module + // placed there whose plan reads; one it could not judge is not named, and the node judges it as before. + Handed map[string]bool `json:"handed,omitempty"` } // CredentialAsked names one pair credential by its ends, as a node composing its own asks for it (mesh-sdk @@ -163,6 +167,15 @@ func (p *planReader) placedOn(module string) (bool, error) { return slices.Contains(p.placed, module), p.placedErr } +// placedModules is the machine's placed modules, read once for the answer. +func (p *planReader) placedModules() ([]string, error) { + if p.placement == nil { + return nil, errors.New("the machine's placement is not read here") + } + p.placedOn("") + return p.placed, p.placedErr +} + // read is module's assignment on the machine, nil when there is none, or why the vault reads none. func (p *planReader) read(module string) ([]byte, string, error) { if p.why != "" { @@ -283,7 +296,7 @@ func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e } plan := v.plansOf(node) var handings []Handing - out := OwnAnswer{Node: node, Key: key} + out := OwnAnswer{Node: node, Key: key, Handed: map[string]bool{}} for _, r := range all { parts := strings.Split(r.Name, "/") if len(parts) != 4 || parts[0] != "own" || parts[1] != node { @@ -293,6 +306,7 @@ func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e if err != nil { return OwnAnswer{}, err } + out.Handed[parts[2]] = handed if handed { continue } @@ -308,6 +322,17 @@ func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e out.Own = append(out.Own, NodeOwnSecret{Module: parts[2], Name: parts[3], Origin: composeOrigin(r.Origin), Generation: r.Generation, Sealed: sealed}) } + // And every module placed there that it holds no secret of yet, where its plan reads: the node hands it as judged here. + if placed, err := plan.placedModules(); err == nil { + for _, module := range placed { + if _, judged := out.Handed[module]; judged { + continue + } + if handed, err := plan.handed(module); err == nil { + out.Handed[module] = handed + } + } + } sort.Slice(out.Own, func(i, j int) bool { if out.Own[i].Module != out.Own[j].Module { return out.Own[i].Module < out.Own[j].Module @@ -392,15 +417,7 @@ func (p *planReader) handed(module string) (bool, error) { // handed) hands nothing, and that module's files and own secrets stay its node's composition. Whether a plan exists // is never the rule; a reader that asks only who binds whom reads the provisions and says so ("not handing", which // the catalogue check checks/planhands holds every module to). -func handsOf(raw []byte) (bool, error) { - var a struct { - Provisioning *catalogue.Plan `json:"provisioning"` - } - if err := json.Unmarshal(raw, &a); err != nil { - return false, err - } - return a.Provisioning != nil && a.Provisioning.Hands(), nil -} +func handsOf(raw []byte) (bool, error) { return catalogue.AssignmentHands(raw) } // hands is handsOf for a value already read as the controller's: one that cannot be read hands nothing. func hands(raw []byte) bool { diff --git a/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go b/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go index 7a957ff1..461d7203 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "testing" @@ -286,4 +287,8 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) { if len(got.Own) != 1 || got.Own[0].Module != "grafana" || got.Own[0].Name != "admin" { t.Errorf("anchor was answered %+v; want grafana's admin alone (its plan keys a route and hands nothing)", got.Own) } + // And it says how it judged each, so the node composing itself hands exactly those (issue 774, postgres on novox). + if want := map[string]bool{"grafana": false, "web": true, "db": true}; !reflect.DeepEqual(got.Handed, want) { + t.Errorf("own judged %v handed, want %v", got.Handed, want) + } } diff --git a/modules/mesh-vault/cmd/mesh-vault/retire.go b/modules/mesh-vault/cmd/mesh-vault/retire.go index ea694aa9..e0ea2960 100644 --- a/modules/mesh-vault/cmd/mesh-vault/retire.go +++ b/modules/mesh-vault/cmd/mesh-vault/retire.go @@ -136,7 +136,7 @@ func (v *Vault) RetirePass() { return } v.leaseAfresh() - on, leaseWhy := v.mintingOn() + on, leaseWhy := v.leaseHanded() m := &v.minting m.mu.Lock() read := m.retiring.wantsRead @@ -239,7 +239,7 @@ func (v *Vault) RetireAccounts(caller string, users []string, why string) (any, // 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.mintingOn(); !on { + if on, why := v.leaseHanded(); !on { return nil, fmt.Errorf("nothing was retired: %s", why) } v.minting.mu.Lock() diff --git a/modules/mesh-vault/cmd/mesh-vault/rotation.go b/modules/mesh-vault/cmd/mesh-vault/rotation.go index 31244f97..9f1d6312 100644 --- a/modules/mesh-vault/cmd/mesh-vault/rotation.go +++ b/modules/mesh-vault/cmd/mesh-vault/rotation.go @@ -42,7 +42,7 @@ 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.mintingOn(); !on { + if on, why := v.leaseHanded(); !on { return Rotating{}, fmt.Errorf("no rotation begins: %s", why) } v.minting.mu.Lock() diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index 0f575e8f..ee282f7d 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -306,9 +306,9 @@ 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. Reads the wanted key afresh from mesh-grants.accounts and mints it as a want would: only while " + - "the controller's lease is handed away, 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 " + + "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 " + + "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")}, run: func(caller string, a map[string]any) (any, error) { @@ -633,8 +633,9 @@ func definitions(env *Env) []toolDef { {Name: "vault_status", 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 §3, §9): whether the vault mints bus accounts now and why (only while the " + - "controller's lease is handed away), every wanted account it holds back with why, and those it minted.", + "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 { return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil diff --git a/modules/mesh-vault/go.mod b/modules/mesh-vault/go.mod index 057326ca..49715173 100644 --- a/modules/mesh-vault/go.mod +++ b/modules/mesh-vault/go.mod @@ -3,7 +3,7 @@ module mesh-vault go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/modules/mesh-vault/go.sum b/modules/mesh-vault/go.sum index df73018a..c925fcba 100644 --- a/modules/mesh-vault/go.sum +++ b/modules/mesh-vault/go.sum @@ -1,5 +1,9 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535 h1:Bgrm3YfWCxr+FEm03kEP5fDIe9hNbe+j0CDVatGAGKg= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082933-085f17e935db h1:PJ1V9tDjNqp/aogm/r52RRePxcHyW58f7lYBceMMY9c= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082933-085f17e935db/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=