package manifest import ( "context" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "slices" "strings" "sync" "testing" "time" "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-grants/internal/grants" ) // fakeBus is the bus as the part sees it: other parts' states to read (refusing those named), the bus module's read // tools, the part's own states and its events. It records every write, call and event, so a row can prove nothing was // published or asserted. type fakeBus struct { mu sync.Mutex states map[string]map[string][]byte refused map[string]bool loaded []grants.User streams []map[string]any own map[string]map[string][]byte revs map[string]uint64 refuseTo map[string]bool calls []string events []string writes []string // The assignments stream: each subject's last membership and sequence, every publish, and whether it is refused. stream map[string][]byte seqs map[string]uint64 seq uint64 publishes []string refusePub bool // hold, when set, keeps every nats_users call waiting until it is closed: a pass waiting on the bus. hold chan struct{} // creates are the streams the part asked to create, refused when refuseCreate. creates []string refuseCreate bool } func (b *fakeBus) LastMembership(node, module string) ([]byte, uint64, bool, error) { b.mu.Lock() defer b.mu.Unlock() subject := "mesh.assignment." + node + "." + module v, ok := b.stream[subject] return v, b.seqs[subject], ok, nil } func (b *fakeBus) PublishMembership(node, module string, body []byte, after uint64) error { b.mu.Lock() defer b.mu.Unlock() subject := "mesh.assignment." + node + "." + module if b.refusePub { return errors.New("Permissions Violation for Publish to " + subject) } if b.seqs[subject] != after { return fmt.Errorf("wrong last sequence for %s: %d", subject, b.seqs[subject]) } if b.stream == nil { b.stream, b.seqs = map[string][]byte{}, map[string]uint64{} } b.seq++ b.stream[subject], b.seqs[subject] = body, b.seq b.publishes = append(b.publishes, subject) return nil } // CreateDeclared is the bus's create of a stream: refused when refuseCreate, held when the bus holds it, and otherwise // held from now on, as nats_streams then reads it. func (b *fakeBus) CreateDeclared(cfg jetstream.StreamConfig) error { b.mu.Lock() defer b.mu.Unlock() b.creates = append(b.creates, cfg.Name) if b.refuseCreate { return errors.New("Permissions Violation for Publish to $JS.API.STREAM.CREATE." + cfg.Name) } for _, s := range b.streams { if s["name"] == cfg.Name { return grants.ErrHeld } } b.streams = append(b.streams, map[string]any{"name": cfg.Name, "subjects": cfg.Subjects, "retention": "limits", "max_age_hours": int(cfg.MaxAge.Hours()), "max_per_subject": int(cfg.MaxMsgsPerSubject)}) return nil } func newFakeBus() *fakeBus { return &fakeBus{states: map[string]map[string][]byte{}, refused: map[string]bool{}, own: map[string]map[string][]byte{}, revs: map[string]uint64{}, refuseTo: map[string]bool{}} } func (b *fakeBus) put(state, key string, v any) { raw, err := json.Marshal(v) if err != nil { panic(err) } if b.states[state] == nil { b.states[state] = map[string][]byte{} } b.states[state][key] = raw } func (b *fakeBus) Keys(state string) ([]string, error) { b.mu.Lock() defer b.mu.Unlock() if b.refused[state] { return nil, errors.New("Permissions Violation for Publish to $JS.API.STREAM.INFO.KV_" + state) } var out []string for k := range b.states[state] { out = append(out, k) } slices.Sort(out) return out, nil } func (b *fakeBus) Get(state, key string) ([]byte, bool, error) { b.mu.Lock() defer b.mu.Unlock() if b.refused[state] { return nil, false, errors.New("refused") } v, ok := b.states[state][key] return v, ok, nil } func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) { b.mu.Lock() hold := b.hold b.mu.Unlock() if hold != nil && key == grants.CallUsers { <-hold } b.mu.Lock() defer b.mu.Unlock() b.calls = append(b.calls, key) args, _ := body.(map[string]any) switch key { case grants.CallUsers: part, _ := args["user"].(string) if part == "" { var names []map[string]any for _, u := range b.loaded { names = append(names, map[string]any{"user": u.User, "publish_grants": len(u.Publish)}) } return json.Marshal(map[string]any{"users": names}) } var out []grants.User for _, u := range b.loaded { if strings.Contains(u.User, part) { out = append(out, u) } } return json.Marshal(map[string]any{"users": out}) case grants.CallStreams: return json.Marshal(map[string]any{"detail": b.streams}) } return nil, fmt.Errorf("no tool %s", key) } func (b *fakeBus) ownGet(state, key string) ([]byte, uint64, bool, error) { b.mu.Lock() defer b.mu.Unlock() v, ok := b.own[state][key] return v, b.revs[state+"/"+key], ok, nil } type ownStore struct{ b *fakeBus } func (s ownStore) Get(state, key string) ([]byte, uint64, bool, error) { return s.b.ownGet(state, key) } func (s ownStore) Put(state, key string, value []byte) (uint64, error) { b := s.b b.mu.Lock() defer b.mu.Unlock() if b.refuseTo[state] { return 0, errors.New("Permissions Violation for Publish to $KV.mesh-grants_" + state) } b.writes = append(b.writes, state) if b.own[state] == nil { b.own[state] = map[string][]byte{} } b.own[state][key] = value b.revs[state+"/"+key]++ return b.revs[state+"/"+key], nil } func (b *fakeBus) Emit(event string, _ any) error { b.mu.Lock() defer b.mu.Unlock() b.events = append(b.events, event) return nil } func (b *fakeBus) count(key string) int { b.mu.Lock() defer b.mu.Unlock() n := 0 for _, c := range b.calls { if c == key { n++ } } return n } // manifestOf is a fixture manifest of one module, in the indexer's two buckets. func (b *fakeBus) manifestOf(module, raw string) { digest := delivery.ManifestDigest([]byte(raw)) b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw}) b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest, Repository: "novox/x", Commit: "c"}) } func (b *fakeBus) assign(node, module string) { b.put(grants.ReadAssignments, node+"."+module, map[string]any{"node": node, "module": module, "assigned": "2026-10-25T20:00:00Z"}) } const ( natsManifest = `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"}}` xManifest = `{"module":"mod-x","version":"1","own-secrets":{"broker":"/var/lib/x/broker"},"state":[{"name":"kept","history":3}],` + `"streams":[{"name":"things","events":["thing.added"],"max-age-days":2,"max-mib":5}],"logs":[{"name":"history"}]}` yManifest = `{"module":"mod-y","version":"1","own-secrets":{"broker":"/var/lib/y/broker"},"reads":["mod-x.kept"]}` ) // fixture is a mesh of two machines: a runs the bus and mod-x, b runs mod-y; the bus node's agent loaded a list. func fixture() *fakeBus { b := newFakeBus() b.manifestOf("nats", natsManifest) b.manifestOf("mod-x", xManifest) b.manifestOf("mod-y", yManifest) b.assign("a", "nats") b.assign("a", "mod-x") b.assign("b", "mod-y") b.put(grants.ReadAssignments, "b.gone", map[string]any{"node": "b", "module": "gone", "assigned": "2026-10-01T00:00:00Z", "ended": "2026-10-02T00:00:00Z"}) b.put(grants.ReadHave, "a.node", map[string]any{"v": 1}) b.put(grants.ReadHave, "b.node", map[string]any{"v": 1}) b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}}) b.put(grants.ReadControllerGrant, "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123", delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}}) return b } func partOn(b *fakeBus) *grants.Part { return &grants.Part{Read: b, Call: b, Store: ownStore{b}, Emit: b, Publish: b, Now: func() time.Time { return time.Date(2026, 10, 25, 22, 0, 0, 0, time.UTC) }} } // shadowOf is the list the part composes from b's records now. func shadowOf(t *testing.T, b *fakeBus) []grants.User { t.Helper() recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{})) users, err := grants.ComposeUsers(recs) if err != nil { t.Fatal(err) } return users } func names(us []grants.User) []string { var out []string for _, u := range us { out = append(out, u.User) } return out } func gapNamed(gs []grants.Gap, input string) bool { return slices.ContainsFunc(gs, func(g grants.Gap) bool { return strings.Contains(g.Input, input) }) } // TestGrants is the grants part's one table of checks (ADR 0547 clauses 5 to 8 in shadow, issue 1945): its manifest, // the shadow list composed from bus values, the compare, each tool, that nothing is published or asserted, and that a // refused read or write never stops it. func TestGrants(t *testing.T) { t.Run("manifest", func(t *testing.T) { raw, err := os.ReadFile("module.json") if err != nil { t.Fatal(err) } m, err := catalogue.ParseManifest(raw) if err != nil { t.Fatalf("mesh-sdk's manifest reader refuses the manifest: %v", err) } if m.Module != "mesh-grants" || len(m.Claims) != 0 { t.Errorf("module %q claims %v: the part claims no seat until mesh-sdk's seat list names mesh-grants", m.Module, m.Claims) } for _, row := range []struct { what string got, want []string }{ {"reads", m.Reads, grants.Reads}, {"invokes", m.Invokes, grants.Invokes}, {"emits", m.Emits, grants.Emits}, {"tools", m.Tools, grants.Tools}, } { if !slices.Equal(slices.Sorted(slices.Values(row.got)), slices.Sorted(slices.Values(row.want))) { t.Errorf("%s = %v, the program's are %v", row.what, row.got, row.want) } } var states []string for _, s := range m.State { states = append(states, s.Name) } if !slices.Equal(slices.Sorted(slices.Values(states)), grants.States) { t.Errorf("state = %v, want %v", states, grants.States) } if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || m.SecretsOwner != "mesh-grants" { t.Errorf("own-secrets %v owner %q: a credential of its own, broker", m.OwnSecrets, m.SecretsOwner) } }) t.Run("no timer, no update or delete, a publish only of memberships, a create only of declared streams", func(t *testing.T) { forbidden := []string{"time.Tick", "time.NewTicker", "time.Sleep", "time.After", "time.NewTimer", "time.AfterFunc", "PublishMsg", "CreateKeyValue", "CreateOrUpdate", "UpdateStream", "UpdateKeyValue", "DeleteStream", "PurgeStream", "DeleteKeyValue", "CreateObjectStore", "EnsureStream", "RaiseBuckets", "RaiseLogs", ".Create("} // The one publish: a membership, on its own subject, in publish.go; the one create: a declared stream the bus lacks, // in create.go (issue 1790). publishOnly := filepath.Join("cmd", "mesh-grants", "publish.go") createOnly := filepath.Join("cmd", "mesh-grants", "create.go") files := 0 for _, root := range []string{"cmd", "internal"} { err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { return err } files++ src, err := os.ReadFile(path) if path != publishOnly && (strings.Contains(string(src), ".Publish(") || strings.Contains(string(src), "mesh.assignment")) { t.Errorf("%s publishes: only publish.go publishes, and only memberships", path) } if path == publishOnly && strings.Count(string(src), ".Publish(") != 1 { t.Errorf("publish.go publishes other than one membership") } if n := strings.Count(string(src), "CreateStream"); (path == createOnly && n != 1) || (path != createOnly && n != 0) { t.Errorf("%s holds CreateStream %d times: only create.go creates, once", path, n) } for _, f := range forbidden { if strings.Contains(string(src), f) { t.Errorf("%s holds %s: the part holds no timer and creates or asserts nothing", path, f) } } return err }) if err != nil { t.Fatal(err) } } if files < 8 { t.Fatalf("read %d source files, want the program's", files) } }) t.Run("shadow list from bus values", func(t *testing.T) { for _, row := range []struct { name string bend func(*fakeBus) users []string gap string }{ {"every record on the bus", func(*fakeBus) {}, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"}, {"a manifest missing is a gap and its user is left out", func(b *fakeBus) { delete(b.states[grants.ReadModules], "mod-y") }, []string{"a.mod-x", "a.nats", "controller", "node.a", "node.b"}, "manifest of mod-y"}, {"a want's built manifest is composed, not the trunk's", func(b *fakeBus) { built := strings.Replace(xManifest, `"history":3}]`, `"history":3},{"name":"newer"}]`, 1) digest := delivery.ManifestDigest([]byte(built)) b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "mod-x", JSON: built}) fp := "src2:" + strings.Repeat("cd", 32) b.put(grants.ReadWants, "want.a.mod-x", delivery.Want{V: delivery.Version, FP: fp, Composed: "sha256:" + strings.Repeat("ef", 32)}) key, _ := delivery.BuilderBuildKey("mod-x", fp) b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest}) }, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"}, {"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadAssignments] = true }, []string{"controller", "node.a", "node.b"}, grants.ReadAssignments}, } { t.Run(row.name, func(t *testing.T) { b := fixture() row.bend(b) in := grants.ReadInputs(b, &grants.Manifests{}) recs, left := grants.Records(in) users, err := grants.ComposeUsers(recs) if err != nil { t.Fatal(err) } if got := names(users); !slices.Equal(got, row.users) { t.Errorf("users = %v, want %v", got, row.users) } if !gapNamed(append(in.Gaps, left...), row.gap) { t.Errorf("gaps %v name no %q", in.Gaps, row.gap) } if strings.HasPrefix(row.name, "a want's") { x := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "a.mod-x" })] if !slices.Contains(x.Publish, "$KV.mod-x_newer.>") { t.Errorf("a.mod-x is not granted the state its wanted build declares: %v", x.Publish) } } }) } b := fixture() users := shadowOf(t, b) y := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "b.mod-y" })] if !slices.Contains(y.Publish, "$JS.API.DIRECT.GET.KV_mod-x_kept.>") { t.Errorf("b.mod-y reads mod-x.kept and is not granted its direct get: %v", y.Publish) } }) t.Run("a predefined-key fallback is read with no list; every refused manifest listed", func(t *testing.T) { // mod-p's value falls back to a predefined key, as claude-code's managed_settings does, and no list is on the bus // (issue 1945); mod-z's manifest carries a key mesh-sdk's reader refuses, and mod-z is assigned nowhere. const pManifest = `{"module":"mod-p","version":"1","own-secrets":{"broker":"/var/lib/p/broker"},` + `"config":{"size":{"type":"number","description":"The size text shows in, in points.","predefined":"font-size","default":12,"runtime":true}}}` const zManifest = `{"module":"mod-z","version":"1","no-such-key":true}` b := fixture() b.manifestOf("mod-p", pManifest) b.manifestOf("mod-z", zManifest) b.assign("b", "mod-p") in := grants.ReadInputs(b, &grants.Manifests{}) if len(in.Refused) != 1 || in.Refused[0].Input != "manifest of mod-z" || !gapNamed(in.Gaps, "manifest of mod-z") { t.Errorf("refused = %v: only mod-z's, and named in the gaps", in.Refused) } recs, _ := grants.Records(in) users, err := grants.ComposeUsers(recs) if err != nil { t.Fatal(err) } if !slices.Contains(names(users), "b.mod-p") { t.Errorf("b.mod-p is not composed, so its module counts as placed nowhere: %v", names(users)) } // The pass, the tool listing held memberships, and the log each say the refusal. part := partOn(b) res := part.Pass() if len(res.Refused) != 1 || !strings.HasPrefix(res.Refused[0], "manifest of mod-z: ") { t.Errorf("the pass says refused %v", res.Refused) } held, err := part.Held() if err != nil { t.Fatal(err) } if got, _ := held.(map[string]any)["refused"].([]string); len(got) != 1 { t.Errorf("grants_membership lists refused %v", got) } var logged []string var lmu sync.Mutex f := &grants.Follower{Part: partOn(b), Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(format string, args ...any) { lmu.Lock() logged = append(logged, fmt.Sprintf(format, args...)) lmu.Unlock() }} ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { f.Run(ctx); close(done) }() waitFor(t, func() bool { lmu.Lock() defer lmu.Unlock() return strings.Contains(strings.Join(logged, "\n"), "refused manifest of mod-z: ") }) cancel() <-done }) t.Run("credentials", func(t *testing.T) { shadow := []grants.User{{User: "a.held"}, {User: "a.new"}, {User: "a.bare"}} loaded := []grants.User{{User: "a.held"}, {User: "a.bare"}, {User: "enrol.x"}, {User: "person.op"}} hashes := map[string]grants.HeldHash{"a.held": {Hash: "$2a$10$h1", Generation: 2}, "a.new": {Hash: "$2a$10$h2", Generation: 1}, "person.op": {Hash: "$2a$10$h3", Adopted: true}} c := grants.CredentialsOf(shadow, loaded, hashes) for _, row := range []struct { user, would string held, adopted bool }{ {"a.bare", grants.WouldLeaveOut, false, false}, {"a.held", grants.WouldSame, true, false}, {"a.new", grants.WouldAdd, true, false}, {"enrol.x", grants.WouldLoseLogin, false, false}, {"person.op", grants.WouldNotComposed, true, true}, } { i := slices.IndexFunc(c.Users, func(u grants.Credential) bool { return u.User == row.user }) if i < 0 || c.Users[i].Would != row.would || c.Users[i].HashHeld != row.held || c.Users[i].Adopted != row.adopted { t.Errorf("%s: %+v, want %s held %v", row.user, c.Users, row.would, row.held) } } if c.HashesHeld != 3 || !slices.Equal(c.LeftOut, []string{"a.bare"}) || !slices.Equal(c.LosesLogin, []string{"enrol.x"}) || !slices.Equal(c.Adds, []string{"a.new"}) { t.Errorf("credentials = %+v", c) } // Composed from the vault's state: a hash changes the list's state (written on the event), and no hash is ever // in what the part writes or says. b := fixture() b.loaded = shadowOf(t, b) p := partOn(b) before := p.Compose() b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$secrethashbytes", "generation": 1}) after := p.Compose() cr := after.UserList.Credentials if !after.Written || after.Revision != before.Revision+1 || cr.HashesHeld != 1 || slices.Contains(cr.LeftOut, "a.mod-x") || !slices.Contains(cr.LeftOut, "b.mod-y") { t.Errorf("a hash heard = %+v", after.UserList.Credentials) } err := p.TakeOver("person.operator", "hashes") if err == nil || !strings.Contains(err.Error(), "no hash for") || strings.Contains(err.Error(), "a.mod-x,") { t.Errorf("take-over = %v, want the users with no hash named", err) } for state, keys := range b.own { for k, v := range keys { if strings.Contains(string(v), "secrethashbytes") { t.Errorf("the hash is written in %s %s", state, k) } } } }) t.Run("compare", func(t *testing.T) { b := fixture() shadow := shadowOf(t, b) in := grants.ReadInputs(b, &grants.Manifests{}) changed := slices.Clone(shadow) x := slices.IndexFunc(changed, func(u grants.User) bool { return u.User == "a.mod-x" }) changed[x].Publish = append(slices.Clone(changed[x].Publish[1:]), "extra.subject") for _, row := range []struct { name string loaded []grants.User added, dropped, gone []string differ []string equal bool }{ {"equal", shadow, nil, nil, nil, nil, true}, {"one user the bus lacks", shadow[1:], []string{shadow[0].User}, nil, nil, nil, false}, {"one loaded user still placed", append(slices.Clone(shadow), grants.User{User: "a.mod-x.worker"}), nil, []string{"a.mod-x.worker"}, nil, nil, false}, {"one loaded user placement no longer names", append(slices.Clone(shadow), grants.User{User: "b.gone"}), nil, []string{"b.gone"}, []string{"b.gone"}, nil, true}, {"grants differ", changed, nil, nil, nil, []string{"a.mod-x"}, false}, } { t.Run(row.name, func(t *testing.T) { c := grants.Compare(shadow, row.loaded, grants.Placed(in)) var differ []string for _, d := range c.Differ { differ = append(differ, d.User) } if !slices.Equal(c.Added, nonNil(row.added)) || !slices.Equal(c.Dropped, nonNil(row.dropped)) || !slices.Equal(c.Gone, row.gone) || !slices.Equal(differ, row.differ) || c.Equal() != row.equal { t.Errorf("compare = %+v", c) } if row.name == "grants differ" && (len(c.Differ[0].PublishAdded) != 1 || c.Differ[0].PublishDropped[0] != "extra.subject") { t.Errorf("the grant difference is not named: %+v", c.Differ[0]) } }) } }) t.Run("tools", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) b.streams = []map[string]any{{"name": "KV_mod-x_kept", "subjects": []string{"$KV.mod-x_kept.>"}, "retention": "limits", "max_per_subject": 3}} p := partOn(b) got, _ := p.UserList() if !strings.Contains(fmt.Sprint(got), "nothing composed yet") { t.Errorf("user-list before any composition = %v", got) } first := p.Compose() if !first.Written || first.Revision != 1 || !first.UserList.Compare.Equal() || first.UserList.Users != 6 { t.Fatalf("first composition = %+v", first) } if len(first.Published) != 3 || len(b.publishes) != 3 { t.Errorf("the first composition published %v, want the three assignments' memberships", first.Published) } // The second pass says each membership unchanged (published at the first); the third writes nothing at all. if again := p.Compose(); again.Written || again.Revision != 1 || len(again.Published) != 0 || again.AssertionsWritten != 0 { t.Errorf("a composition with no input changed wrote or published: %+v", again) } if third := p.Compose(); third.Written || third.MembershipsWritten != 0 || len(third.Published) != 0 || len(b.publishes) != 3 { t.Errorf("a membership equal to the one last published was published again, or a state written: %+v", third) } // mod-x placed on b too: b.mod-x is added and its first membership published; a.mod-x's would lose the module's // plain subject (mod-x now runs on two machines), a loss, so it stays a shadow difference. b.assign("b", "mod-x") moved := p.Compose() if !moved.Written || moved.Revision != 2 || !slices.Equal(moved.Published, []string{"b.mod-x"}) || len(moved.NotPublished) != 1 || !strings.HasPrefix(moved.NotPublished[0], "a.mod-x: "+grants.ShadowDifference) || moved.UserList.Compare.Equal() || !slices.Equal(moved.UserList.Compare.Added, []string{"b.mod-x"}) { t.Errorf("a composition after placement moved = %+v", moved) } got, _ = p.UserList() ul, _ := got.(map[string]any) if st, ok := ul["written"].(grants.UserListState); !ok || st.Loaded == nil || st.Loaded.Digest != "sha256:aa" || st.BusNode != "a" || ul["revision"] != uint64(2) || !gapNamed(st.Gaps, "people") { t.Errorf("user-list = %+v", got) } a, err := p.AssertOne("mod-x") am, _ := a.(map[string]any) as, _ := am["assertion"].(grants.Assertion) if err != nil || as.Asserted || as.Says != grants.ShadowAsserts || len(as.Items) != 3 || am["asked"] == nil { t.Fatalf("assert = %+v, %v", a, err) } for _, it := range as.Items { if it.Stream == "KV_mod-x_kept" && len(it.Differences) != 0 { t.Errorf("a bucket the bus holds as composed differs: %v", it.Differences) } if it.Stream == "THINGS" && !slices.Contains(it.Differences, "the bus holds no stream THINGS") { t.Errorf("a stream the bus lacks is not named: %+v", it) } } ms, _ := p.MembershipOf("a", "mod-x") if mo, _ := ms.(map[string]any); !strings.Contains(fmt.Sprint(mo["published"]), "last_pass") || mo["composed"] == nil { t.Errorf("membership = %v", ms) } gr, _ := p.Grants("mod-x") if g, _ := gr.(map[string]any); g["writes"] != grants.WritesNoGrant || g["count"] != 1 { t.Errorf("grants = %v", gr) } b.loaded = append(b.loaded, grants.User{User: "b.gone"}) for _, row := range []struct { name, act, caller string says, never []string }{ {"take-over without the operator's credential", "take-over", "shanks.node-tools", []string{"operator's alone"}, nil}, {"hand-back without the operator's credential", "hand-back", "unproven", []string{"operator's alone"}, nil}, {"take-over while a user differs names it", "take-over", "person.operator", []string{"users differ", "b.mod-x", grants.NotComposable}, []string{"b.gone"}}, {"hand-back in shadow", "hand-back", "person.operator", []string{"nothing to hand back"}, nil}, } { t.Run(row.name, func(t *testing.T) { var err error if row.act == "take-over" { err = p.TakeOver(row.caller, "a row") } else { err = p.HandBack(row.caller, "a row") } if err == nil { t.Fatalf("%s by %s was not refused", row.act, row.caller) } for _, s := range row.says { if !strings.Contains(err.Error(), s) { t.Errorf("refusal %q does not say %q", err, s) } } for _, s := range row.never { if strings.Contains(err.Error(), s) { t.Errorf("refusal %q names %q", err, s) } } raw, _, _, _ := b.ownGet(grants.StateTakeover, grants.KeySwitch) var s grants.Switch _ = json.Unmarshal(raw, &s) last := s.Acts[len(s.Acts)-1] if s.State != grants.Shadow || last.Act != row.act || last.By != row.caller || last.Refused == "" { t.Errorf("the act is not recorded: %+v", s) } }) } t.Run("take-over with only a user placement no longer names is refused for want of records alone", func(t *testing.T) { b.states[grants.ReadAssignments] = nil b.assign("a", "nats") b.assign("a", "mod-x") b.assign("b", "mod-y") b.loaded = append(shadowOf(t, b), grants.User{User: "b.gone"}) err := p.TakeOver("person.operator", "lists agree") if err == nil || strings.Contains(err.Error(), "differ") || !strings.Contains(err.Error(), grants.NotComposable) { t.Errorf("take-over = %v, want refused for the enrolment users and persons alone", err) } }) t.Run("only memberships published, nothing asserted", func(t *testing.T) { for _, s := range b.publishes { if !strings.HasPrefix(s, "mesh.assignment.") { t.Errorf("published on %s", s) } } for _, w := range b.writes { if !slices.Contains(grants.States, w) { t.Errorf("wrote %s, which is not the part's own state", w) } } for _, c := range b.calls { if c != grants.CallUsers && c != grants.CallStreams { t.Errorf("called %s: the part calls only the bus module's read tools", c) } } for _, e := range b.events { if !slices.Contains(grants.Emits, e) { t.Errorf("said %s", e) } } }) }) t.Run("memberships: a first or an additive one published, a loss never", func(t *testing.T) { const key = "a.mod-x" composed := func(b *fakeBus) map[string]any { recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{})) raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership) var m map[string]any _ = json.Unmarshal(raw, &m) return m } for _, row := range []struct { name string last func(map[string]any) map[string]any published bool names string }{ {"a first membership is published", nil, true, ""}, {"an additive change is published, the private network carried", func(m map[string]any) map[string]any { delete(m, "state") m["mesh"] = []any{"10.0.0.1"} return m }, true, ""}, {"a change that drops verified-sender is not published, and named", func(m map[string]any) map[string]any { m["seat-traffic"] = map[string]any{"kinds": []any{map[string]any{"seat": "channel", "kind": "telegram", "capabilities": []any{"verified-sender"}}}} return m }, false, "verified-sender"}, {"a change that drops any other field is not published", func(m map[string]any) map[string]any { m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.mod-x.tool.gone"}) return m }, false, "tool.gone"}, // A verb the seat catalogue retired on purpose (mark-ready, mesh-sdk d506f21, ADR 0421) is dropped and named; a // verb of a seat the catalogue does not define is still a loss (issue 1945). {"a change that drops only a retired seat verb is published, the verb named", func(m map[string]any) map[string]any { m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"}} return m }, true, "mesh-delivery mark-ready"}, {"a retired verb and a module seat's verb dropped: not published, the module seat's named", func(m map[string]any) map[string]any { m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"}, map[string]any{"seat": "telegram-sender", "verb": "send", "subject": "mesh.seat.telegram-sender.tool.send"}} return m }, false, "telegram-sender"}, } { t.Run(row.name, func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) if row.last != nil { raw, _ := json.Marshal(row.last(composed(b))) b.stream = map[string][]byte{"mesh.assignment." + key: raw} b.seqs = map[string]uint64{"mesh.assignment." + key: 4} } res := partOn(b).Compose() if got := slices.Contains(res.Published, key); got != row.published { t.Fatalf("published %v, not published %v", res.Published, res.NotPublished) } if row.published { if row.names == "" && row.last != nil && !strings.Contains(string(b.stream["mesh.assignment."+key]), "10.0.0.1") { t.Errorf("the private network of the last membership is not carried: %s", b.stream["mesh.assignment."+key]) } if raw, _, _, _ := b.ownGet(grants.StateMemberships, key); !strings.Contains(string(raw), row.names) { t.Errorf("the pass does not name %q: %s", row.names, raw) } return } said := strings.Join(res.NotPublished, "\n") if !strings.Contains(said, grants.ShadowDifference) || !strings.Contains(said, row.names) { t.Errorf("the loss is not named: %s", said) } ms, _ := partOn(b).MembershipOf("a", "mod-x") if !strings.Contains(fmt.Sprint(ms), row.names) { t.Errorf("grants_membership does not name what would be lost: %v", ms) } }) } t.Run("a refused publish: no exit, said, published at the next event", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) b.refusePub = true p := partOn(b) refused := p.Compose() if len(refused.Published) != 0 || len(refused.NotPublished) != 3 || !strings.Contains(refused.NotPublished[0], "Permissions Violation") { t.Errorf("a refused publish = %+v: said, and nothing published", refused) } b.refusePub = false if next := p.Compose(); !slices.Equal(next.Published, []string{"a.mod-x", "a.nats", "b.mod-y"}) { t.Errorf("the next event published %v", next.Published) } }) t.Run("a tool answers while a pass waits on the bus", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) b.hold = make(chan struct{}) p := partOn(b) done := make(chan struct{}) go func() { p.Compose(); close(done) }() answered := make(chan struct{}) go func() { _, _ = p.UserList() _, _ = p.MembershipOf("a", "mod-x") _, _ = p.Grants("") _ = p.Last() close(answered) }() select { case <-answered: case <-time.After(10 * time.Second): t.Error("a tool waited on the pass's round trip") } close(b.hold) <-done }) }) t.Run("assertions: what the bus lacks is created once, what it holds is never touched", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) // The bus holds mod-x's bucket with another history, and neither its log nor its stream. b.streams = []map[string]any{{"name": "KV_mod-x_kept", "subjects": []string{"$KV.mod-x_kept.>"}, "retention": "limits", "max_per_subject": 2}} p := partOn(b) p.Create = b assertion := func() grants.Assertion { raw, _, _, _ := b.ownGet(grants.StateAssertions, "mod-x") var a grants.Assertion _ = json.Unmarshal(raw, &a) return a } first := p.Compose() if !slices.Equal(b.creates, []string{"LOG_mod-x_history", "THINGS"}) || len(first.Asserted) != 1 || !strings.HasPrefix(first.Asserted[0], "mod-x at ") || !slices.Contains(b.events, grants.EventAsserted) { t.Fatalf("the first pass created %v, said %v, events %v", b.creates, first.Asserted, b.events) } a := assertion() if !a.Asserted || a.Says != grants.CreatesMissing { t.Errorf("the assertion kept = %+v", a) } for _, it := range a.Items { switch it.Stream { case "KV_mod-x_kept": if it.Created != "" || !slices.Contains(it.Differences, "history 2 held, 3 composed") { t.Errorf("a bucket the bus holds was touched, or its difference not said: %+v", it) } default: if it.Created != grants.ItemCreated { t.Errorf("%s: %q, want created", it.Stream, it.Created) } } } if again := p.Compose(); len(again.Asserted) != 0 || len(b.creates) != 2 { t.Errorf("a pass with everything held created again: %v, %v", again.Asserted, b.creates) } // THINGS deleted since: not made again at the same manifest, said; a hand assert (the tool, asking the process) // makes it again. b.mu.Lock() b.streams = slices.DeleteFunc(b.streams, func(s map[string]any) bool { return s["name"] == "THINGS" }) b.mu.Unlock() gone := p.Compose() if len(b.creates) != 2 || !strings.Contains(strings.Join(gone.NotAsserted, " "), "THINGS: created at this manifest and gone since") { t.Errorf("a stream gone since was made again by itself, or not said: %v, %v", b.creates, gone.NotAsserted) } tool := partOn(b) if got, err := tool.AssertOne("mod-x"); err != nil || fmt.Sprint(got) == "" || len(b.creates) != 2 { t.Fatalf("the tool = %v, %v, creates %v: it asks, and creates nothing itself", got, err, b.creates) } if hand := p.Compose(); len(hand.Asserted) != 1 || b.creates[len(b.creates)-1] != "THINGS" { t.Errorf("the hand assert did not create THINGS again: %v, %v", hand.Asserted, b.creates) } if after := p.Compose(); len(after.Asserted) != 0 { t.Errorf("the hand ask was acted on twice: %v", after.Asserted) } for _, c := range b.creates { if c == "KV_mod-x_kept" { t.Error("the bucket the bus holds was asked to be created") } } }) t.Run("assertions: a refused create is said, never stops it, and is tried at the next event", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) b.refuseCreate = true p := partOn(b) p.Create = b refused := p.Compose() if len(refused.Asserted) != 0 || !strings.Contains(strings.Join(refused.NotAsserted, " "), "Permissions Violation") || slices.Contains(b.events, grants.EventAsserted) { t.Fatalf("a refused create = %+v", refused) } b.refuseCreate = false if next := p.Compose(); len(next.Asserted) != 1 || !strings.Contains(next.Asserted[0], "KV_mod-x_kept") { t.Errorf("the next event created %v", next.Asserted) } // What the bus holds unread: nothing is created. c := fixture() c.loaded = shadowOf(t, c) q := partOn(c) q.Create = c q.Call = refusingStreams{c} if r := q.Compose(); len(c.creates) != 0 || len(r.Asserted) != 0 { t.Errorf("created %v with what the bus holds unread", c.creates) } }) t.Run("memberships held back: every one is said and listed", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{})) b.stream, b.seqs = map[string][]byte{}, map[string]uint64{} for i, key := range []string{"a.mod-x", "b.mod-y"} { raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership) var m map[string]any _ = json.Unmarshal(raw, &m) m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.lost.tool.gone"}) raw, _ = json.Marshal(m) subject := "mesh.assignment." + key b.stream[subject], b.seqs[subject] = raw, uint64(i+1) } var logged []string var lmu sync.Mutex w := &fakeWatcher{opened: make(chan string, 16)} f := &grants.Follower{Part: partOn(b), Watch: w, Logf: func(format string, args ...any) { lmu.Lock() logged = append(logged, fmt.Sprintf(format, args...)) lmu.Unlock() }} ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { f.Run(ctx); close(done) }() waitFor(t, func() bool { lmu.Lock() defer lmu.Unlock() all := strings.Join(logged, "\n") return strings.Contains(all, "held a.mod-x: ") && strings.Contains(all, "held b.mod-y: ") }) cancel() <-done held, err := partOn(b).MembershipOf("", "") h, _ := held.(map[string]any) if err != nil || h["count"] != 2 || !strings.Contains(fmt.Sprint(h["held"]), "tool.gone") { t.Errorf("grants_membership with neither node nor module = %v, %v", held, err) } if _, err := partOn(b).MembershipOf("a", ""); err == nil { t.Error("a node without a module was answered") } }) t.Run("a refused read or write never stops it", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) b.refuseTo[grants.StateMemberships] = true res := partOn(b).Compose() if !res.Written || len(res.Errors) == 0 || !strings.Contains(strings.Join(res.Errors, " "), "memberships") { t.Errorf("a refused write of memberships = %+v: the user list is written all the same, the refusal said", res) } w := &fakeWatcher{refuse: map[string]int{grants.ReadModules: 1}, opened: make(chan string, 16)} var logged []string var lmu sync.Mutex f := &grants.Follower{Part: partOn(b), Watch: w, Logf: func(format string, args ...any) { lmu.Lock() logged = append(logged, fmt.Sprintf(format, args...)) lmu.Unlock() }} ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { f.Run(ctx); close(done) }() waitOpened(t, w, grants.ReadAssignments) if r := f.Refused(); r[grants.ReadModules] == "" { waitFor(t, func() bool { return f.Refused()[grants.ReadModules] != "" }) } w.change(grants.ReadAssignments, "a.mod-x") waitOpened(t, w, grants.ReadModules) waitFor(t, func() bool { return len(f.Refused()) == 0 }) select { case <-done: t.Fatal("the follower stopped") default: } cancel() <-done lmu.Lock() defer lmu.Unlock() if !strings.Contains(strings.Join(logged, "\n"), "cannot be watched yet") { t.Errorf("the refusal was not said: %v", logged) } }) } func nonNil(s []string) []string { if s == nil { return []string{} } return s } // fakeWatcher refuses a state's first watches as told, and hands changes to the watches it opened. // refusingStreams is the bus with nats_streams refused: what the bus holds cannot be read. type refusingStreams struct{ *fakeBus } func (r refusingStreams) Call(key string, body any) (json.RawMessage, error) { if key == grants.CallStreams { return nil, errors.New("refused") } return r.fakeBus.Call(key, body) } type fakeWatcher struct { mu sync.Mutex refuse map[string]int changes map[string]func(string, []byte) opened chan string } func (w *fakeWatcher) Watch(state string, changed func(string, []byte), _ func(error)) error { w.mu.Lock() defer w.mu.Unlock() if w.refuse[state] > 0 { w.refuse[state]-- return errors.New("Permissions Violation for Publish to $JS.API.STREAM.INFO.KV_" + state) } if w.changes == nil { w.changes = map[string]func(string, []byte){} } w.changes[state] = changed w.opened <- state return nil } func (w *fakeWatcher) change(state, key string) { w.mu.Lock() h := w.changes[state] w.mu.Unlock() h(key, []byte(`{}`)) } func waitOpened(t *testing.T, w *fakeWatcher, state string) { t.Helper() deadline := time.After(10 * time.Second) for { select { case s := <-w.opened: if s == state { return } case <-deadline: t.Fatalf("%s was never watched", state) } } } func waitFor(t *testing.T, ok func() bool) { t.Helper() for end := time.Now().Add(10 * time.Second); time.Now().Before(end); time.Sleep(10 * time.Millisecond) { if ok() { return } } t.Fatal("never came") }