Let the vault mint every wanted bus account whoever holds the controller's lease, keep its mint over a controller's issued value, and tell a node which modules it judged handed, so no account waits on a node reporting to the controller (issue 774)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user