diff --git a/modules/mesh-vault/cmd/mesh-vault/mint.go b/modules/mesh-vault/cmd/mesh-vault/mint.go index 06bb80a7..ebfd3aa8 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint.go @@ -189,12 +189,17 @@ func (v *Vault) mintOnce(w buscompose.WantedAccount, by string) (Minted, error) v.holdBack(w.User, err.Error()) return Minted{}, err } - if _, held, err := v.Store.Read(secret); err != nil { + replacing := false + if rec, held, err := v.Store.Read(secret); err != nil { return Minted{}, err } else if held { - // Its value is held (the controller's, handed in, or one the vault minted): never minted anew (§4). - v.noLongerHeld(w.User) - return Minted{}, errHeldBack{secret + " is held already: an account the vault holds a value for is never minted anew (ADR 0561 §4)"} + // A held value that is no account (kept before the vault judged it, the sweep of 2026-10-30) has no hash and + // logs in nowhere on the bus: it is replaced by a minted account. Any other held value is never minted anew (§4). + if value, err := openAtRest(rec, v); err != nil || BusAccountValue(secret, value) { + v.noLongerHeld(w.User) + return Minted{}, errHeldBack{secret + " is held already: an account the vault holds a value for is never minted anew (ADR 0561 §4)"} + } + replacing = true } if v.Accounts == nil { v.holdBack(w.User, "the vault has no state of bus accounts to write the hash to") @@ -230,11 +235,25 @@ func (v *Vault) mintOnce(w buscompose.WantedAccount, by string) (Minted, error) v.holdBack(w.User, err.Error()) return Minted{}, err } - c, err := v.keepLocked(secret, string(value), "the vault", "", OriginMade, keepOptions{neverReplace: true, quiet: true}) + opt := keepOptions{neverReplace: true, quiet: true} + if replacing { + // Only the value that is no account is replaced, judged again under the store's lock; the one replaced is kept. + opt = keepOptions{quiet: true, keepPrevious: true, refuse: func(before Record) string { + if was, err := openAtRest(before, v); err != nil || BusAccountValue(secret, was) { + return secret + " is held as an account meanwhile: it is never minted anew (ADR 0561 §4)" + } + return "" + }} + } + c, err := v.keepLocked(secret, string(value), "the vault", "", OriginMade, opt) if err != nil { v.holdBack(w.User, "its value could not be kept: "+err.Error()) return Minted{}, err } + if replacing { + v.log("%s held a value that is no bus account, which no hash was taken of and which logs in nowhere: replaced by a "+ + "minted account (generation %d), the value it replaces kept beside it", secret, c.Generation) + } out := Minted{User: w.User, Secret: secret, Generation: c.Generation, At: c.At} // The hash first, then account-issued, then secret-changed: the grants part composes the user before the node asks. if _, err := v.HashAccount(secret, false); err != nil { diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_test.go index e36454e5..9f7d2687 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_test.go @@ -242,3 +242,30 @@ func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) { t.Errorf("not minted at the pass after the failure: %+v", v.MintState()) } } + +// A held value that is no account (kept before the vault judged it: broker-nats-tools, broker-address and +// 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) + w := aWant("mod-x") + if _, err := v.Put("own/ace/mod-x/broker", `{"password":"pw"}`, "the test"); err != nil { + t.Fatal(err) + } + heldPlain(t, v.Vault, "own/ace/mod-x/broker", "a-random-value-the-controller-made") + v.WantHeard(w.User, wantRaw(t, w), false) + rec, _, _ := v.Store.Read("own/ace/mod-x/broker") + value, err := openAtRest(rec, v.Vault) + if err != nil || !BusAccountValue("own/ace/mod-x/broker", value) || rec.Generation != 3 || rec.Previous == nil { + t.Fatalf("not replaced by a minted account: generation %d, previous kept %v, %v", rec.Generation, rec.Previous != nil, err) + } + var got struct{ Password string } + _ = json.Unmarshal([]byte(value), &got) + if !accounts.hashOf(w.User, got.Password, 3) { + t.Errorf("%s's hash is not the minted password's: %+v", w.User, accounts.keys[w.User]) + } + v.WantHeard(w.User, wantRaw(t, w), false) + if again, _, _ := v.Store.Read("own/ace/mod-x/broker"); again.Generation != 3 { + t.Errorf("a held account was minted anew: generation %d", again.Generation) + } +}