diff --git a/modules/mesh-vault/cmd/mesh-vault/ledgers.go b/modules/mesh-vault/cmd/mesh-vault/ledgers.go new file mode 100644 index 00000000..287019f1 --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/ledgers.go @@ -0,0 +1,69 @@ +package main + +// The ledger accounts of a machine and the tool runner's own secret `ledgers` (novox/hq ADR 0597 §9, design 25 § 12.4, +// issue 1966 V1 and V2). Nothing here is the custody ledger of ledger.go (ADR 0085): this file is about the bus account +// of kind ledger, one per module assignment that asks for a ledger. +// +// The tool runner of a machine holds its machine's ledger accounts together as its own secret `ledgers` +// (`own//node-tools/ledgers`): a JSON map of module to credential, `{}` when there is none, so its composition +// never refuses a secret the vault did not make. The vault makes it as `{}` for every machine whose tool runner is +// wanted, and rewrites it on each ledger account minted or retired, saying `secret-changed` once per rewrite. + +import ( + "encoding/json" + "sort" + + "git.novox.be/novox/mesh-sdk/go/buscompose" +) + +// toolRunnerModule is the module whose own secret `ledgers` is: the tool runner (mesh-tools node-tools). +const toolRunnerModule = "node-tools" + +// ledgersSecret is the vault's name of node's `ledgers`. +func ledgersSecret(node string) (string, error) { + return OwnSecret(node, toolRunnerModule, buscompose.LedgersSecret) +} + +// toolRunnerNodes are the machines whose tool runner is wanted, sorted. +func (v *Vault) toolRunnerNodes() []string { + v.minting.mu.Lock() + defer v.minting.mu.Unlock() + var out []string + for _, w := range v.minting.wants { + if w.Kind == buscompose.KindNodeTools { + out = append(out, w.Node) + } + } + sort.Strings(out) + return out +} + +// LedgersPass makes `ledgers` as `{}` on every machine whose tool runner is wanted and for which the vault holds none, +// at the vault's minute pass (and when the tool runner's want is heard). +func (v *Vault) LedgersPass() { + for _, node := range v.toolRunnerNodes() { + v.ensureLedgers(node) + } +} + +// ensureLedgers keeps node's `ledgers` as `{}` when the vault holds none: a held one is never touched here. +func (v *Vault) ensureLedgers(node string) { + secret, err := ledgersSecret(node) + if err != nil { + v.log("%s's ledgers are not made: %v", node, err) + return + } + _, held, err := v.Store.Read(secret) + if err != nil || held { + if err != nil { + v.log("%s is not read, so its ledgers are not made: %v", secret, err) + } + return + } + empty, _ := json.Marshal(map[string]json.RawMessage{}) + if _, err := v.keepLocked(secret, string(empty), "the vault", "", OriginMade, keepOptions{neverReplace: true}); err != nil { + v.log("%s is not made, tried again at the next pass: %v", secret, err) + return + } + v.log("made %s as {}: no ledger account is held for its machine yet", secret) +} diff --git a/modules/mesh-vault/cmd/mesh-vault/ledgers_test.go b/modules/mesh-vault/cmd/mesh-vault/ledgers_test.go new file mode 100644 index 00000000..ec8e424c --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/ledgers_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "testing" + + "git.novox.be/novox/mesh-sdk/go/buscompose" + "git.novox.be/novox/mesh-sdk/go/seal" +) + +// Ledger accounts and the tool runner's own secret `ledgers` (novox/hq ADR 0597 §9, issue 1966). Not the custody +// ledger of ledger.go (ADR 0085), which is another thing. + +// nodeToolsWant is the wanted account of a machine's tool runner, as the grants part writes it. +func nodeToolsWant(node string) buscompose.WantedAccount { + w := aWant("node-tools") + w.Kind, w.Node, w.User = buscompose.KindNodeTools, node, node+".node-tools" + return w +} + +// ledgerWant is the wanted ledger account of a module on a machine. +func ledgerWant(node, module string) buscompose.WantedAccount { + w := aWant(module) + w.Kind, w.Node, w.User, w.Claims = buscompose.KindLedger, node, "ledger."+node+"."+module, nil + return w +} + +// ledgersOf is what the vault's `own` answers the machine as node-tools' `ledgers`: the map of module to credential, +// and whether the secret is there at all. +func ledgersOf(t *testing.T, v testVault, priv string) (map[string]json.RawMessage, bool) { + t.Helper() + own, err := v.OwnOfNode("node.ace") + if err != nil { + t.Fatal(err) + } + for _, o := range own.Own { + if o.Module == "node-tools" && o.Name == buscompose.LedgersSecret { + value, err := seal.Open(o.Sealed, priv) + if err != nil { + t.Fatal(err) + } + got := map[string]json.RawMessage{} + if err := json.Unmarshal([]byte(value), &got); err != nil { + t.Fatalf("ledgers is not a JSON map: %v", err) + } + return got, true + } + } + return nil, false +} + +// changesOf counts the secret-changed events said for name since the first of events. +func changesOf(v testVault, since int, name string) int { + n := 0 + for _, e := range v.bus.events[since:] { + if e["event"] == ChangedEvent && e["name"] == name { + n++ + } + } + return n +} diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index e4e06e00..3333b3c0 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -350,6 +350,7 @@ func main() { } env.Vault.HashOwed() env.Vault.MintOwed() + env.Vault.LedgersPass() env.Vault.RetirePass() env.Vault.RotationPass() env.Vault.LoginsPass() diff --git a/modules/mesh-vault/cmd/mesh-vault/mint.go b/modules/mesh-vault/cmd/mesh-vault/mint.go index 3b20a47e..21ed60cb 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint.go @@ -193,6 +193,9 @@ func (v *Vault) WantHeard(user string, raw []byte, deleted bool) { v.minting.wants[user] = w v.minting.mu.Unlock() v.wantBack(user) + if w.Kind == buscompose.KindNodeTools { + v.ensureLedgers(w.Node) + } if _, err := v.mintWanted(w, "the grants part's want of "+user); err != nil { v.log("%s is not minted: %v", user, err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_test.go index 8ca556f6..edd36ba1 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_test.go @@ -99,6 +99,36 @@ func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) { if st := v.MintState(); len(st.HeldBack) != 0 || len(st.Minted) != 1 { t.Errorf("vault_status says %+v: an adopted user is not held back, one account minted", st) } + t.Run("ledgers", func(t *testing.T) { + for _, c := range []struct { + name string + run func(t *testing.T, v testVault, priv string) + }{ + // 1966/AC24 (issue 1966 V1): a machine carrying node-tools, with no ledger account, has `ledgers` as {}. + {"an empty ledgers on a machine with node-tools", func(t *testing.T, v testVault, priv string) { + if _, ok := ledgersOf(t, v, priv); ok { + t.Fatal("ledgers is there before node-tools is wanted") + } + w := nodeToolsWant("ace") + v.WantHeard(w.User, wantRaw(t, w), false) + before := len(v.bus.events) + v.LedgersPass() + got, ok := ledgersOf(t, v, priv) + if !ok || len(got) != 0 { + t.Fatalf("ledgers is %v (held %v), want {}", got, ok) + } + v.LedgersPass() + if n := changesOf(v, before, "own/ace/node-tools/ledgers"); n != 0 { + t.Errorf("a second pass said secret-changed %d times for a value that did not change", n) + } + }}, + } { + t.Run(c.name, func(t *testing.T) { + v, _, priv := mintingVault(t) + c.run(t, v, priv) + }) + } + }) } // 774/AC8, after research 072's gap "vault mints after the lease": the vault is the one issuer, so a wanted account is