Replace a held bus-account value that is no account by a minted account when its user is wanted (issue 774)

The three random values kept before the vault judged them (own/novox/nats/broker-nats-tools,
own/novox/mesh-controller/broker-address and broker-management) are held, so minting refused them ("held already")
and their users stay off the bus. A value that is no account has no hash and logs in nowhere, so replacing it
breaks no login the grants part composed. When the grants part wants the user (every want is heard at the vault's
start), the vault now mints an account in its place, judged again under the store's lock, keeping the value it
replaces beside it. A held account is still never minted anew (ADR 0561 §4).

Quick fix: let an operator rotate the three by hand. Thorough fix: the mint path replaces any such value, so no
other one stays behind. Chose thorough.
This commit is contained in:
2026-10-30 18:29:48 +01:00
parent 683fd547b2
commit b7f878226e
2 changed files with 51 additions and 5 deletions
+24 -5
View File
@@ -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 {
@@ -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)
}
}