mesh-grants: a partial pass keeps every deny and every published membership
mesh/delivery merged: source.main moved to novox/mesh-delivery@cd7f85c74a75
mesh/delivery merged: source.main moved to novox/mesh-delivery@cd7f85c74a75
Review round 1 of issue 1966's keep-what-is-loaded fix. - KeepLoaded intersected the denies, and the composer gives every composed user an empty deny, so a user in force with a wide publish and a deny was sealed with no deny. The union of every deny any list says is kept now, and Losses counts a deny dropped. - A pass composing part of the mesh (the seat list unread, or placed by the haves) no longer narrows a published membership: one whose published value serves, hears or gives more than the pass composes is kept as published and said so (not_published), so a tool runner or a seat holder goes on serving what it serves while keeping its login. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
@@ -2448,6 +2448,31 @@ func TestGrants(t *testing.T) {
|
||||
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)
|
||||
@@ -2497,6 +2522,22 @@ func TestGrants(t *testing.T) {
|
||||
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 {
|
||||
@@ -2521,6 +2562,12 @@ func TestGrants(t *testing.T) {
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
// 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; a deny only where every list that says one denies it), so it
|
||||
// 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).
|
||||
|
||||
@@ -52,18 +53,16 @@ func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]Use
|
||||
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}
|
||||
denyKnown := composedHere && c.PublishDeny != nil
|
||||
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
|
||||
switch {
|
||||
case o.PublishDeny == nil:
|
||||
case !denyKnown:
|
||||
u.PublishDeny, denyKnown = set(o.PublishDeny), true
|
||||
default:
|
||||
u.PublishDeny = slices.DeleteFunc(slices.Clone(u.PublishDeny), func(d string) bool { return !slices.Contains(o.PublishDeny, d) })
|
||||
if o.PublishDeny != nil || u.PublishDeny != nil {
|
||||
u.PublishDeny = set(append(slices.Clone(u.PublishDeny), o.PublishDeny...))
|
||||
}
|
||||
}
|
||||
if u.Publish == nil {
|
||||
@@ -77,8 +76,9 @@ func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]Use
|
||||
return out
|
||||
}
|
||||
|
||||
// Losses are the permissions had gives that now does not: each publish and subscribe, the allowance of responses, and
|
||||
// each deny now adds where had's denies are known. Pure.
|
||||
// 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) {
|
||||
@@ -94,6 +94,9 @@ func Losses(now, had User) []string {
|
||||
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
|
||||
}
|
||||
@@ -120,3 +123,51 @@ func LoadedLosses(listed []string, composed, loaded []User) []string {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -434,6 +434,17 @@ func (p *Part) Compose() Result {
|
||||
continue
|
||||
}
|
||||
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, so a membership held back never reaches a reader.
|
||||
if now {
|
||||
|
||||
Reference in New Issue
Block a user