diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 44e582a..369943e 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -975,6 +975,17 @@ func TestGrants(t *testing.T) { if now, _, _ := p.Sealed(); now.Digest != g.Digest { t.Errorf("the list in force moved without a.mod-x: %s", now.Digest) } + // A list in force written before the part kept its users (no sealed-users): checked against the users the + // bus loaded, so the first list after take-over has the same protection. + b.mu.Lock() + delete(b.own[grants.StateUserList], grants.KeyWrittenUsers) + b.mu.Unlock() + if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x would be left out") { + t.Errorf("a pass with no kept users that drops a.mod-x's hash = %+v", res.Sealed) + } + if now, _, _ := p.Sealed(); now.Digest != g.Digest { + t.Errorf("the list in force moved without a.mod-x and no kept users: %s", now.Digest) + } b.put(grants.ReadAccounts, "a.mod-x", json.RawMessage(hash)) if p.Compose(); func() bool { now, _, _ := p.Sealed(); return now.Digest != g.Digest }() { t.Errorf("the list in force moved with every read and hash back") diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index 0c074e0..348c9f7 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -229,7 +229,12 @@ func (p *Part) Compose() Result { if list, n, leftOut, err := ComposeList(recs, in.Hashes); err != nil { res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error()) } else { - p.writeSealed(&res, list, n, leftOut, in) + // The users the bus loaded: what a first list after take-over must keep where the part has not yet written one. + var bus []string + if st.Compare.Unread == "" { + bus = names(loaded) + } + p.writeSealed(&res, list, n, leftOut, in, bus) } ms := Memberships(recs) res.Memberships = len(ms) @@ -342,3 +347,11 @@ func sortedKeys[V any](m map[string]V) []string { sort.Strings(out) return out } + +func names(us []User) []string { + out := make([]string, 0, len(us)) + for _, u := range us { + out = append(out, u.User) + } + return out +} diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index 8038135..f960094 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -71,7 +71,7 @@ func followOf(state string) string { // writeSealed puts the list sealed to the bus node and writes the key, each only when what it says changed. It says in // res what it wrote, or why it wrote nothing. -func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string, in Inputs) { +func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string, in Inputs, bus []string) { cur, _, had := p.Sealed() next := cur next.Follow = followOf(p.SwitchState()) @@ -90,10 +90,10 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string why = "no node runs the bus's module, so the list is sealed to nobody" case key == "": why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)" - case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut) != "": + case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus) != "": // After take-over the list in force is never replaced by one composed past a failed read, or one that drops a user // of it for want of a hash: that user's module would be locked out of the bus (review of the switch). - why = p.holdsBack(in, leftOut) + why = p.holdsBack(in, leftOut, bus) case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode: object, err := delivery.SealUserList(list, key) if err == nil { @@ -265,8 +265,10 @@ func listUsers(list []byte) []string { } // holdsBack says why a list composed now may not replace the one in force after take-over: a read failed this pass, or -// a user of the list in force is left out for want of a hash. Empty when it may. -func (p *Part) holdsBack(in Inputs, leftOut []string) string { +// a user of the list in force is left out for want of a hash. The list in force is the one the part wrote, by the users +// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when +// they could not be read). Empty when it may. +func (p *Part) holdsBack(in Inputs, leftOut, bus []string) string { if f := in.ReadFailures(); len(f) > 0 { return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")" } @@ -275,8 +277,13 @@ func (p *Part) holdsBack(in Inputs, leftOut []string) string { return "held: the users of the list in force cannot be read: " + err.Error() } var had []string - if found { + switch { + case found: _ = json.Unmarshal(raw, &had) + case bus == nil: + return "held: the users of the list in force are not known: the part kept none, and the loaded list could not be read" + default: + had = bus } var lost []string for _, u := range leftOut {