Merge pull request 'mesh-grants keeps what the bus loaded while it composes only part of the mesh (issue 1966, bootstrap recovery)' (#171) from fix/1966-grants-keeps-what-is-loaded into main
This commit was merged in pull request #171.
This commit is contained in:
@@ -2414,9 +2414,15 @@ func TestGrants(t *testing.T) {
|
|||||||
b.loaded = append(b.loaded, grants.User{User: "c.old"})
|
b.loaded = append(b.loaded, grants.User{User: "c.old"})
|
||||||
b.put(grants.ReadHave, "b.mod-y", runs)
|
b.put(grants.ReadHave, "b.mod-y", runs)
|
||||||
hash("b.mod-y")
|
hash("b.mod-y")
|
||||||
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "would drop c.old of the list in force") {
|
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "would take from loaded users: c.old (left out)") ||
|
||||||
|
!slices.Contains(res.KeptLoaded, "c.old") {
|
||||||
t.Errorf("a list dropping a loaded user was not held: %+v", res.Sealed)
|
t.Errorf("a list dropping a loaded user was not held: %+v", res.Sealed)
|
||||||
}
|
}
|
||||||
|
// With its hash, the loaded user the haves do not compose is kept as the loaded list gives it, and the list sealed.
|
||||||
|
hash("c.old")
|
||||||
|
if res := p.Compose(); res.Sealed == nil || res.Sealed.Why != "" || !strings.Contains(string(b.own[grants.StateUserList][grants.KeyWrittenUsers]), `"c.old"`) {
|
||||||
|
t.Errorf("the loaded user was not kept in the list sealed: %+v", res.Sealed)
|
||||||
|
}
|
||||||
// The view answers: the assignments alone places again.
|
// The view answers: the assignments alone places again.
|
||||||
delete(b.refused, grants.ReadAssignments)
|
delete(b.refused, grants.ReadAssignments)
|
||||||
if in := grants.ReadInputs(b, &grants.Manifests{}); in.ByHaves != "" || slices.Contains(in.Assigned["a"], "mesh-delivery-assigner") {
|
if in := grants.ReadInputs(b, &grants.Manifests{}); in.ByHaves != "" || slices.Contains(in.Assigned["a"], "mesh-delivery-assigner") {
|
||||||
@@ -2424,6 +2430,184 @@ func TestGrants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Issue 1966, the list sealed 2026-11-07 23:09: composed from the bootstrapped seats alone, it took the node seats from the
|
||||||
|
// tool runners, the vault's reads from the node-engines, the seat queues from the build modules, and left out the ledger
|
||||||
|
// account of a held module. A pass that composes part of the mesh (the seat list unread, or placed by the haves) seals
|
||||||
|
// each user of the loaded list and of the list in force with the union of what it composes and what those give it, so it
|
||||||
|
// only adds (the indexer's serving of mesh-catalog included); one that would still take a permission is held, naming it.
|
||||||
|
t.Run("a pass composing part of the mesh keeps what the bus loaded, adding only (issue 1966)", func(t *testing.T) {
|
||||||
|
indexer, err := os.ReadFile("../mesh-delivery-indexer/module.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const (
|
||||||
|
nodeSeat = "mesh.seat.node-files.call.*.a.*"
|
||||||
|
vaultRead = "$JS.API.DIRECT.GET.KV_mesh-vault_signers.>"
|
||||||
|
buildPull = "$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_BUILD_GO.workers"
|
||||||
|
)
|
||||||
|
ledgerX := strings.Replace(xManifest, `"state"`, `"requires":[{"name":"records","storage":"ledger","template":"ledger",`+
|
||||||
|
`"types":[{"name":"decision","schema":{},"operations":[{"name":"propose","effect":"set-fields"}]}]}],"state"`, 1)
|
||||||
|
catalogSeat := func(s string) bool { return strings.HasPrefix(s, "mesh.seat.mesh-catalog.") }
|
||||||
|
// Review round 1: the list in force gives a wide publish and a deny, the pass composes the user narrower with the
|
||||||
|
// composer's empty deny: the user kept keeps the deny, and dropping it counts as a loss.
|
||||||
|
for _, d := range []struct {
|
||||||
|
name string
|
||||||
|
composed, kept grants.User
|
||||||
|
loss string
|
||||||
|
}{
|
||||||
|
{"a deny of the list in force is kept",
|
||||||
|
grants.User{User: "a.mod-v", Publish: []string{"mesh.a"}, Subscribe: []string{}, PublishDeny: []string{}},
|
||||||
|
grants.User{User: "a.mod-v", Publish: []string{"mesh.>", "mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.seat.mesh-vault.desk.>"}},
|
||||||
|
"deny dropped mesh.seat.mesh-vault.desk.>"},
|
||||||
|
{"a deny the pass composes is kept beside it",
|
||||||
|
grants.User{User: "a.mod-v", Publish: []string{"mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.b"}},
|
||||||
|
grants.User{User: "a.mod-v", Publish: []string{"mesh.>", "mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.b", "mesh.seat.mesh-vault.desk.>"}},
|
||||||
|
"deny mesh.b"},
|
||||||
|
} {
|
||||||
|
inForce := grants.User{User: "a.mod-v", Publish: []string{"mesh.>"}, Subscribe: []string{}, PublishDeny: []string{"mesh.seat.mesh-vault.desk.>"}}
|
||||||
|
got := grants.KeepLoaded([]grants.User{d.composed}, nil, map[string]grants.User{"a.mod-v": inForce})["a.mod-v"]
|
||||||
|
if !slices.Equal(got.Publish, d.kept.Publish) || !slices.Equal(got.PublishDeny, d.kept.PublishDeny) {
|
||||||
|
t.Errorf("%s: kept %+v, want %+v", d.name, got, d.kept)
|
||||||
|
}
|
||||||
|
if !slices.Contains(grants.Losses(d.composed, inForce), d.loss) {
|
||||||
|
t.Errorf("%s: losses %v name no %q", d.name, grants.Losses(d.composed, inForce), d.loss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range []struct {
|
||||||
|
name string
|
||||||
|
bend func(*fakeBus, *grants.Part)
|
||||||
|
// held is what the pass's sealed says it holds, "" when it seals.
|
||||||
|
held string
|
||||||
|
}{
|
||||||
|
{"the seat list unread", func(b *fakeBus, p *grants.Part) {
|
||||||
|
p.SeatList = &seatGate{bootstrapped: []string{meshbroker.Seat, meshcatalog.Seat}}
|
||||||
|
}, ""},
|
||||||
|
{"placed by the haves", func(b *fakeBus, p *grants.Part) {
|
||||||
|
b.refused[grants.ReadAssignments] = true
|
||||||
|
runs := nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}
|
||||||
|
b.put(grants.ReadHave, "a.mod-x", runs)
|
||||||
|
b.put(grants.ReadHave, "b.mod-y", runs)
|
||||||
|
b.put(grants.ReadHave, "a.mesh-delivery-indexer", runs)
|
||||||
|
}, ""},
|
||||||
|
// The ledger of a held module stays with its module's users, whatever the seat list (held.go).
|
||||||
|
{"the ledger of a module whose manifest is refused", func(b *fakeBus, p *grants.Part) {
|
||||||
|
b.refuse("mod-x", ledgerX, "it is held")
|
||||||
|
}, ""},
|
||||||
|
// The union cannot give a login a loaded user has no hash for: held, naming it.
|
||||||
|
{"a loaded user with no hash, the seat list unread", func(b *fakeBus, p *grants.Part) {
|
||||||
|
p.SeatList = &seatGate{bootstrapped: []string{meshbroker.Seat, meshcatalog.Seat}}
|
||||||
|
delete(b.states[grants.ReadAccounts], "b.mod-y")
|
||||||
|
}, "would take from loaded users: b.mod-y (left out)"},
|
||||||
|
} {
|
||||||
|
t.Run(row.name, func(t *testing.T) {
|
||||||
|
b := fixture()
|
||||||
|
b.manifestOf("mod-x", ledgerX)
|
||||||
|
b.manifestOf("mesh-delivery-indexer", string(indexer))
|
||||||
|
b.assign("a", "mesh-delivery-indexer")
|
||||||
|
busKey, err := seal.GenerateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
|
||||||
|
UserList: &nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: 8, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5,
|
||||||
|
Key: busKey.PublicKey, Follows: grantsstate.FollowGrants}})
|
||||||
|
whole := shadowOf(t, b)
|
||||||
|
for _, u := range whole {
|
||||||
|
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1, "fingerprint": "sha256:1"})
|
||||||
|
}
|
||||||
|
p := partOn(b)
|
||||||
|
p.Objects = &fakeObjects{}
|
||||||
|
// The list in force, composed whole.
|
||||||
|
p.Compose()
|
||||||
|
if g, _, _ := p.Sealed(); g.Digest == "" {
|
||||||
|
t.Fatal("no list in force sealed")
|
||||||
|
}
|
||||||
|
// The indexer's membership as published serves a tool more than any pass composes now (a tool runner's
|
||||||
|
// seat, say): a pass composing part of the mesh keeps it as published, a whole one publishes what it composes.
|
||||||
|
const extra = `{"subject":"mesh.tools.a.mesh-delivery-indexer.extra"}`
|
||||||
|
{
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var e any
|
||||||
|
_ = json.Unmarshal([]byte(extra), &e)
|
||||||
|
m["serves"] = append(m["serves"].([]any), e)
|
||||||
|
b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], _ = json.Marshal(m)
|
||||||
|
}
|
||||||
|
servesExtra := func() bool {
|
||||||
|
return strings.Contains(string(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"]), "mesh-delivery-indexer.extra")
|
||||||
|
}
|
||||||
|
// The bus loaded it with what a whole seat list gives besides, and without the indexer's serving yet.
|
||||||
|
var loaded []grants.User
|
||||||
|
for _, u := range whole {
|
||||||
|
switch u.User {
|
||||||
|
case "a.mod-x":
|
||||||
|
u.Subscribe = append(slices.Clone(u.Subscribe), nodeSeat)
|
||||||
|
case "node.a":
|
||||||
|
u.Subscribe = append(slices.Clone(u.Subscribe), vaultRead)
|
||||||
|
case "b.mod-y":
|
||||||
|
u.Publish = append(slices.Clone(u.Publish), buildPull)
|
||||||
|
case "a.mesh-delivery-indexer":
|
||||||
|
u.Subscribe = slices.DeleteFunc(slices.Clone(u.Subscribe), catalogSeat)
|
||||||
|
}
|
||||||
|
loaded = append(loaded, u)
|
||||||
|
}
|
||||||
|
if !slices.ContainsFunc(loaded, func(u grants.User) bool { return u.User == "ledger.a.mod-x" }) {
|
||||||
|
t.Fatalf("the fixture composes no ledger user: %v", names(loaded))
|
||||||
|
}
|
||||||
|
b.loaded = loaded
|
||||||
|
row.bend(b, p)
|
||||||
|
res := p.Compose()
|
||||||
|
if g, _, _ := p.Sealed(); true {
|
||||||
|
t.Logf("sealed %d users (%s), kept with the loaded list's grants %v, said %q", g.Users, short(g.Digest), res.KeptLoaded, res.Sealed.Why)
|
||||||
|
}
|
||||||
|
partial := row.name != "the ledger of a module whose manifest is refused"
|
||||||
|
if servesExtra() != partial || partial != slices.ContainsFunc(res.NotPublished, func(n string) bool {
|
||||||
|
return strings.HasPrefix(n, "a.mesh-delivery-indexer: kept as published: ") && strings.Contains(n, "mesh-delivery-indexer.extra")
|
||||||
|
}) {
|
||||||
|
t.Errorf("the published membership serving more: kept %v in a pass composing part of the mesh %v; not published %v", servesExtra(), partial, res.NotPublished)
|
||||||
|
}
|
||||||
|
if row.held != "" {
|
||||||
|
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, row.held) {
|
||||||
|
t.Fatalf("not held for %q: %+v", row.held, res.Sealed)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g, _, _ := p.Sealed()
|
||||||
|
if res.Sealed == nil || res.Sealed.Why != "" || g.Digest == "" {
|
||||||
|
t.Fatalf("not sealed: %+v (kept %v, refused %v)", res.Sealed, res.KeptLoaded, res.Refused)
|
||||||
|
}
|
||||||
|
sealedAs := func(user string) (grants.User, bool) {
|
||||||
|
raw, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(g.Digest, user))
|
||||||
|
var u grants.User
|
||||||
|
return u, ok && json.Unmarshal(raw, &u) == nil
|
||||||
|
}
|
||||||
|
if row.name == "the ledger of a module whose manifest is refused" {
|
||||||
|
// The seat list read, the pass composes the whole mesh and keeps no loaded permission beyond it; the held
|
||||||
|
// module's users, its ledger's among them, stay as the list in force holds them.
|
||||||
|
if u, ok := sealedAs("ledger.a.mod-x"); !ok || len(grants.Losses(u, whole[slices.IndexFunc(whole, func(u grants.User) bool { return u.User == "ledger.a.mod-x" })])) > 0 {
|
||||||
|
t.Errorf("the held module's ledger account is not kept: %+v (%v)", u, ok)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, l := range loaded {
|
||||||
|
u, ok := sealedAs(l.User)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s of the loaded list is not in the list sealed", l.User)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lost := grants.Losses(u, l); len(lost) > 0 {
|
||||||
|
t.Errorf("%s loses %v in the list sealed", l.User, lost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ix, _ := sealedAs("a.mesh-delivery-indexer"); !slices.ContainsFunc(ix.Subscribe, catalogSeat) {
|
||||||
|
t.Errorf("the indexer does not gain its serving of mesh-catalog: %v", ix.Subscribe)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ADR 0590 clause 5's test for this repository (issue 1126): no module.json here names mesh-controller, the lease
|
// ADR 0590 clause 5's test for this repository (issue 1126): no module.json here names mesh-controller, the lease
|
||||||
// read and the builder's consume of artifacts-gone included. Read as text, as mesh-catalog's controllerdeps check reads
|
// read and the builder's consume of artifacts-gone included. Read as text, as mesh-catalog's controllerdeps check reads
|
||||||
// its own: mesh-sdk names no module, so it holds no such check (issue 774, the operator 2026-11-03).
|
// its own: mesh-sdk names no module, so it holds no such check (issue 774, the operator 2026-11-03).
|
||||||
@@ -3657,3 +3841,7 @@ func (g *seatGate) MayAct(seats ...string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func short(d string) string {
|
||||||
|
return strings.TrimPrefix(d, "sha256:")[:min(12, len(strings.TrimPrefix(d, "sha256:")))]
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ const HeldModuleCondition = "mesh-grants.module-held"
|
|||||||
|
|
||||||
// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that
|
// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that
|
||||||
// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of
|
// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of
|
||||||
// a module on a node is `<node>.<module>` or `<node>.<module>.<process>` (buscompose.Principal.Username). Pure.
|
// a module on a node is `<node>.<module>`, `<node>.<module>.<process>` or its ledger's `ledger.<node>.<module>`
|
||||||
|
// (buscompose.Principal.Username). Pure.
|
||||||
func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
||||||
held := map[string]string{}
|
held := map[string]string{}
|
||||||
for _, g := range in.Refused {
|
for _, g := range in.Refused {
|
||||||
@@ -55,7 +56,8 @@ func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
|||||||
state = "unreadable"
|
state = "unreadable"
|
||||||
}
|
}
|
||||||
for _, user := range candidates {
|
for _, user := range candidates {
|
||||||
if user == prefix || strings.HasPrefix(user, prefix+".") {
|
// And its ledger account, `ledger.<node>.<module>` (issue 1966: dropped from the list sealed 2026-11-07 23:09).
|
||||||
|
if user == prefix || strings.HasPrefix(user, prefix+".") || user == "ledger."+prefix {
|
||||||
out[user] = fmt.Sprintf("the manifest of %s is %s: %s", module, state, why)
|
out[user] = fmt.Sprintf("the manifest of %s is %s: %s", module, state, why)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ type Inputs struct {
|
|||||||
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
|
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
|
||||||
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
|
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
|
||||||
Refused []Gap
|
Refused []Gap
|
||||||
|
// SeatListUnread says the seat list has not been read (the gate says so, seatlist.Reader.MayAct with no seat): only the
|
||||||
|
// bootstrapped seats compose, so the pass keeps what the bus loaded (keeploaded.go).
|
||||||
|
SeatListUnread bool
|
||||||
// ByHaves says why this pass placed the modules where the node-engines' haves say they run, the assignments view not
|
// ByHaves says why this pass placed the modules where the node-engines' haves say they run, the assignments view not
|
||||||
// answering (bootstrap only, issue 1966); empty when the assignments placed them. Such a pass retires nothing
|
// answering (bootstrap only, issue 1966); empty when the assignments placed them. Such a pass retires nothing
|
||||||
// (ReadFailures) and seals no list dropping a user of the list in force (holdsBack).
|
// (ReadFailures) and seals no list dropping a user of the list in force (holdsBack).
|
||||||
@@ -321,6 +324,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
|||||||
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{}, Fingerprints: map[string]string{},
|
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{}, Fingerprints: map[string]string{},
|
||||||
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
|
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
|
||||||
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
|
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
|
||||||
|
in.SeatListUnread = gate != nil && !gate.MayAct()
|
||||||
nodes := map[string]bool{}
|
nodes := map[string]bool{}
|
||||||
// **The seat's assignments alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
|
// **The seat's assignments alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
|
||||||
// assignments are read no more, and the seats held once for the mesh are held where it says. Not read is a failed
|
// assignments are read no more, and the seats held once for the mesh are held where it says. Not read is a failed
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package grants
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A pass that cannot compose the whole mesh keeps what the bus loaded** (issue 1966, the registry delivery's sealed list of
|
||||||
|
// 2026-11-07 23:09). While the seat list is unread (only the bootstrapped seats compose, ADR 0636 §1) or a pass is placed by
|
||||||
|
// the node-engines' haves (Inputs.ByHaves), what the composer derives is a part of the mesh: that list sealed took the node
|
||||||
|
// seats from every tool runner, the vault's reads from the node-engines and the seat queues from the build modules. Such a
|
||||||
|
// pass composes each user of the loaded list and of the list in force as the union of what it composes and what those lists
|
||||||
|
// give it (publish, subscribe, the allowance of responses, and every deny any of them says), so it
|
||||||
|
// only adds, the indexer's serving of the seat that reads the list included; and the list is held where it would still take
|
||||||
|
// a permission from a loaded user (holdsBack, LoadedLosses).
|
||||||
|
|
||||||
|
// keepsLoaded says why this pass keeps what the bus loaded, or "" when it composes the whole mesh.
|
||||||
|
func (in Inputs) keepsLoaded() string {
|
||||||
|
switch {
|
||||||
|
case in.ByHaves != "":
|
||||||
|
return in.ByHaves
|
||||||
|
case in.SeatListUnread:
|
||||||
|
return "the seat list is unread, so only the bootstrapped seats compose"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeepLoaded is, by user, the union of each user of loaded and of inForce with what composed gives it, for every such user
|
||||||
|
// composed gives less than either list (or not at all). Pure.
|
||||||
|
func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]User {
|
||||||
|
byUser := map[string]User{}
|
||||||
|
for _, u := range composed {
|
||||||
|
byUser[u.User] = u
|
||||||
|
}
|
||||||
|
others := map[string][]User{}
|
||||||
|
for _, u := range loaded {
|
||||||
|
others[u.User] = append(others[u.User], u)
|
||||||
|
}
|
||||||
|
for name, u := range inForce {
|
||||||
|
others[name] = append(others[name], u)
|
||||||
|
}
|
||||||
|
out := map[string]User{}
|
||||||
|
for name, from := range others {
|
||||||
|
c, composedHere := byUser[name]
|
||||||
|
short := !composedHere
|
||||||
|
for _, o := range from {
|
||||||
|
if len(Losses(c, o)) > 0 {
|
||||||
|
short = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !short {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Every deny any list says is kept: a deny the composer drops while it composes part of the mesh would widen the
|
||||||
|
// user (the composer gives every composed user a deny, empty or not, so an intersection would drop the list in
|
||||||
|
// force's).
|
||||||
|
u := User{User: name, Publish: c.Publish, Subscribe: c.Subscribe, AllowResponses: c.AllowResponses, PublishDeny: c.PublishDeny}
|
||||||
|
for _, o := range from {
|
||||||
|
u.Publish = set(append(slices.Clone(u.Publish), o.Publish...))
|
||||||
|
u.Subscribe = set(append(slices.Clone(u.Subscribe), o.Subscribe...))
|
||||||
|
u.AllowResponses = u.AllowResponses || o.AllowResponses
|
||||||
|
if o.PublishDeny != nil || u.PublishDeny != nil {
|
||||||
|
u.PublishDeny = set(append(slices.Clone(u.PublishDeny), o.PublishDeny...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.Publish == nil {
|
||||||
|
u.Publish = []string{}
|
||||||
|
}
|
||||||
|
if u.Subscribe == nil {
|
||||||
|
u.Subscribe = []string{}
|
||||||
|
}
|
||||||
|
out[name] = u
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Losses are how now differs from had where had's word is lost: each publish and subscribe had gives that now does not,
|
||||||
|
// the allowance of responses, and, where had's denies are known, each deny now adds and each deny of had now drops (a deny
|
||||||
|
// dropped widens the user past what had held it to). Pure.
|
||||||
|
func Losses(now, had User) []string {
|
||||||
|
var out []string
|
||||||
|
for _, s := range minus(had.Publish, now.Publish) {
|
||||||
|
out = append(out, "publish "+s)
|
||||||
|
}
|
||||||
|
for _, s := range minus(had.Subscribe, now.Subscribe) {
|
||||||
|
out = append(out, "subscribe "+s)
|
||||||
|
}
|
||||||
|
if had.AllowResponses && !now.AllowResponses {
|
||||||
|
out = append(out, "responses")
|
||||||
|
}
|
||||||
|
if had.PublishDeny != nil {
|
||||||
|
for _, s := range minus(now.PublishDeny, had.PublishDeny) {
|
||||||
|
out = append(out, "deny "+s)
|
||||||
|
}
|
||||||
|
for _, s := range minus(had.PublishDeny, now.PublishDeny) {
|
||||||
|
out = append(out, "deny dropped "+s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadedLosses names, sorted, each loaded user the list composed now (listed: its users; composed: their grants) would
|
||||||
|
// take a permission from or leave out, with how many it loses. Pure.
|
||||||
|
func LoadedLosses(listed []string, composed, loaded []User) []string {
|
||||||
|
byUser := map[string]User{}
|
||||||
|
for _, u := range composed {
|
||||||
|
byUser[u.User] = u
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, l := range loaded {
|
||||||
|
c, ok := byUser[l.User]
|
||||||
|
switch {
|
||||||
|
case !slices.Contains(listed, l.User) || !ok:
|
||||||
|
out = append(out, l.User+" (left out)")
|
||||||
|
default:
|
||||||
|
if n := len(Losses(c, l)); n > 0 {
|
||||||
|
out = append(out, fmt.Sprintf("%s (%d permissions)", l.User, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return slices.Compact(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MembershipNarrows names what the membership last published (was) gives that the one composed now does not, field by field
|
||||||
|
// (`<field> <value>`), the private network, the contributions received and the bindings aside, which the pass carries over
|
||||||
|
// or the bus module writes; none when now covers was. Both are memberships as JSON. Pure.
|
||||||
|
func MembershipNarrows(was, now []byte) []string {
|
||||||
|
var a, b map[string]any
|
||||||
|
if json.Unmarshal(was, &a) != nil || json.Unmarshal(now, &b) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, field := range sortedKeys(a) {
|
||||||
|
switch field {
|
||||||
|
case "mesh", "receives", "bindings":
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, uncovered(field, a[field], b[field])...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// uncovered names each part of a that b does not hold: a key of an object by key, an element of a list by its value.
|
||||||
|
func uncovered(path string, a, b any) []string {
|
||||||
|
switch av := a.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
bv, _ := b.(map[string]any)
|
||||||
|
var out []string
|
||||||
|
for _, k := range sortedKeys(av) {
|
||||||
|
out = append(out, uncovered(path+"."+k, av[k], bv[k])...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case []any:
|
||||||
|
bv, _ := b.([]any)
|
||||||
|
var out []string
|
||||||
|
for _, e := range av {
|
||||||
|
ej, _ := json.Marshal(e)
|
||||||
|
if !slices.ContainsFunc(bv, func(x any) bool { xj, _ := json.Marshal(x); return string(xj) == string(ej) }) {
|
||||||
|
out = append(out, path+" "+string(ej))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
aj, _ := json.Marshal(a)
|
||||||
|
bj, _ := json.Marshal(b)
|
||||||
|
if string(aj) != string(bj) {
|
||||||
|
return []string{path + " " + string(aj)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -105,7 +106,10 @@ type Result struct {
|
|||||||
Memberships int `json:"memberships"`
|
Memberships int `json:"memberships"`
|
||||||
MembershipsWritten int `json:"memberships_written"`
|
MembershipsWritten int `json:"memberships_written"`
|
||||||
Published []string `json:"published,omitempty"`
|
Published []string `json:"published,omitempty"`
|
||||||
NotPublished []string `json:"not_published,omitempty"`
|
// KeptLoaded are the users a pass composing part of the mesh gave the union of what it composes and what the loaded list
|
||||||
|
// and the list in force give them (keeploaded.go, issue 1966), read with grants_user_list's last pass.
|
||||||
|
KeptLoaded []string `json:"kept_loaded,omitempty"`
|
||||||
|
NotPublished []string `json:"not_published,omitempty"`
|
||||||
// Retired is, by `<node>.<module>`, the seat verbs its last published membership served that mesh-sdk's seat
|
// Retired is, by `<node>.<module>`, the seat verbs its last published membership served that mesh-sdk's seat
|
||||||
// catalogue no longer has (Membership.Retired): the pass keeps it, since the state memberships holds the bare
|
// catalogue no longer has (Membership.Retired): the pass keeps it, since the state memberships holds the bare
|
||||||
// membership alone (issue 774).
|
// membership alone (issue 774).
|
||||||
@@ -331,7 +335,27 @@ func (p *Part) Compose() Result {
|
|||||||
}
|
}
|
||||||
p.sayPrincipalsRefused(refused, kept)
|
p.sayPrincipalsRefused(refused, kept)
|
||||||
p.sayHeldModules(heldUsers, kept)
|
p.sayHeldModules(heldUsers, kept)
|
||||||
users = WithHashes(withKept(users, kept), in.Hashes)
|
users = withKept(users, kept)
|
||||||
|
// A pass that composes part of the mesh keeps what the bus loaded and the list in force give each user (keeploaded.go).
|
||||||
|
if why := in.keepsLoaded(); why != "" {
|
||||||
|
if union := KeepLoaded(users, loaded, inForce); len(union) > 0 {
|
||||||
|
merged := make(map[string]string, len(allRefused)+len(union))
|
||||||
|
keptAll := make(map[string]User, len(kept)+len(union))
|
||||||
|
for u, w := range allRefused {
|
||||||
|
merged[u] = w
|
||||||
|
}
|
||||||
|
for u, k := range kept {
|
||||||
|
keptAll[u] = k
|
||||||
|
}
|
||||||
|
for u, k := range union {
|
||||||
|
merged[u], keptAll[u] = "keeps what the loaded list gives it: "+why, k
|
||||||
|
}
|
||||||
|
users = withKept(slices.DeleteFunc(users, func(u User) bool { _, ok := union[u.User]; return ok }), union)
|
||||||
|
allRefused, kept = merged, keptAll
|
||||||
|
res.KeptLoaded = sortedKeys(union)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users = WithHashes(users, in.Hashes)
|
||||||
st.Digest, st.Users = Digest(users), len(users)
|
st.Digest, st.Users = Digest(users), len(users)
|
||||||
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
|
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
|
||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
@@ -381,7 +405,7 @@ func (p *Part) Compose() Result {
|
|||||||
bus = names(loaded)
|
bus = names(loaded)
|
||||||
lost = holdings.LastHolders(holdings.ByUsers(loaded), holdings.ByUsers(users))
|
lost = holdings.LastHolders(holdings.ByUsers(loaded), holdings.ByUsers(users))
|
||||||
}
|
}
|
||||||
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users)
|
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users, loaded)
|
||||||
}
|
}
|
||||||
ms := Memberships(recs)
|
ms := Memberships(recs)
|
||||||
res.Memberships = len(ms)
|
res.Memberships = len(ms)
|
||||||
@@ -410,6 +434,17 @@ func (p *Part) Compose() Result {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m, now := publish(was, found, ms[key], holdings, seated)
|
m, now := publish(was, found, ms[key], holdings, seated)
|
||||||
|
// A pass that composes part of the mesh never narrows a published membership: it keeps the one published
|
||||||
|
// (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it serves.
|
||||||
|
if why := in.keepsLoaded(); now && found && why != "" {
|
||||||
|
if body, err := json.Marshal(m.Membership); err == nil {
|
||||||
|
if bare, err := membership.BareGrantsMembership(was); err == nil {
|
||||||
|
if narrower := MembershipNarrows(bare, body); len(narrower) > 0 {
|
||||||
|
m.Published, now = "kept as published: "+why+", and this pass composes it without "+strings.Join(narrower, ", "), false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
|
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
|
||||||
// published, so a membership held back never reaches a reader.
|
// published, so a membership held back never reaches a reader.
|
||||||
if now {
|
if now {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (p *Part) Sealed() (grantsstate.GrantsUserList, uint64, bool) {
|
|||||||
|
|
||||||
// writeSealed puts the list sealed to the bus node and writes the key, each only when what it says changed. It says in
|
// 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.
|
// res what it wrote, or why it wrote nothing.
|
||||||
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped []string, in Inputs, bus []string, lost []string, composed []User) {
|
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped []string, in Inputs, bus []string, lost []string, composed, loaded []User) {
|
||||||
cur, _, had := p.Sealed()
|
cur, _, had := p.Sealed()
|
||||||
next := cur
|
next := cur
|
||||||
// The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment
|
// The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment
|
||||||
@@ -104,10 +104,10 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
|
|||||||
why = "no node runs the bus's module, so the list is sealed to nobody"
|
why = "no node runs the bus's module, so the list is sealed to nobody"
|
||||||
case key == "":
|
case key == "":
|
||||||
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
|
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
|
||||||
case cur.Digest != digest && p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), digest) != "":
|
case cur.Digest != digest && p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), composed, loaded, digest) != "":
|
||||||
// The list in force is never replaced by one composed past a failed read, or one that drops a user of it for want
|
// 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).
|
// of a hash: that user's module would be locked out of the bus (review of the switch).
|
||||||
why = p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), digest)
|
why = p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), composed, loaded, digest)
|
||||||
held = why
|
held = why
|
||||||
p.keepHashHold(res, digest)
|
p.keepHashHold(res, digest)
|
||||||
case cur.Digest != digest || cur.SealedTo != grantsstate.SealingKeyID(key) || cur.Node != in.BusNode:
|
case cur.Digest != digest || cur.SealedTo != grantsstate.SealingKeyID(key) || cur.Node != in.BusNode:
|
||||||
@@ -299,10 +299,10 @@ func listUsers(list []byte) []string {
|
|||||||
// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when
|
// 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.
|
// they could not be read). Empty when it may.
|
||||||
//
|
//
|
||||||
// In a pass placed by the node-engines' haves (in.ByHaves, issue 1966) the list is held, too, while it would leave out any user
|
// In a pass that composes part of the mesh (placed by the node-engines' haves, or the seat list unread; keeploaded.go, issue
|
||||||
// of the list in force or of the list the bus loaded (listed are the users of the list composed now): a bootstrap placement
|
// 1966) the list is held, too, while it would leave out any user of the list in force or take any permission from a user of
|
||||||
// adds logins and never takes one away.
|
// the list the bus loaded (listed are the users of the list composed now, composed their grants): such a pass only adds.
|
||||||
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string, digest string) string {
|
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string, composed, loaded []User, digest string) string {
|
||||||
p.hashHold = nil
|
p.hashHold = nil
|
||||||
if f := in.ReadFailed(); len(f) > 0 {
|
if f := in.ReadFailed(); len(f) > 0 {
|
||||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||||
@@ -324,7 +324,10 @@ func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string
|
|||||||
default:
|
default:
|
||||||
had = bus
|
had = bus
|
||||||
}
|
}
|
||||||
if in.ByHaves != "" {
|
if why := in.keepsLoaded(); why != "" {
|
||||||
|
if losses := LoadedLosses(listed, composed, loaded); len(losses) > 0 {
|
||||||
|
return "held: " + why + ", and the list composed so would take from loaded users: " + strings.Join(losses, ", ")
|
||||||
|
}
|
||||||
var gone []string
|
var gone []string
|
||||||
for _, u := range append(slices.Clone(had), bus...) {
|
for _, u := range append(slices.Clone(had), bus...) {
|
||||||
if !slices.Contains(listed, u) && !slices.Contains(gone, u) {
|
if !slices.Contains(listed, u) && !slices.Contains(gone, u) {
|
||||||
@@ -333,7 +336,7 @@ func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string
|
|||||||
}
|
}
|
||||||
if len(gone) > 0 {
|
if len(gone) > 0 {
|
||||||
slices.Sort(gone)
|
slices.Sort(gone)
|
||||||
return "held: " + in.ByHaves + ", and the list composed so would drop " + strings.Join(gone, ", ") + " of the list in force"
|
return "held: " + why + ", and the list composed so would drop " + strings.Join(gone, ", ") + " of the list in force"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var unhashed []string
|
var unhashed []string
|
||||||
|
|||||||
Reference in New Issue
Block a user