ADR 0590 §3 ends the choice of a condition source by the keeper's take-over key: the controller is unassigned and the keeper is the one source. The part still read the take-over key, fell back to the controller's condition events when it was absent, and read and watched the controller's lease, which nothing writes any more. It now hears the keeper's events alone and reads neither. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2628 lines
124 KiB
Go
2628 lines
124 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
|
"git.novox.be/novox/mesh-sdk/go/seal"
|
|
|
|
"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
|
|
// Each membership's subject: the last membership said there, every notice, and whether it is refused.
|
|
stream map[string][]byte
|
|
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
|
|
createdAge map[string]time.Duration
|
|
refuseCreate bool
|
|
// refuseUsers refuses the bus module's nats_users: the loaded list cannot be read.
|
|
refuseUsers bool
|
|
// rootFree answers a machine's engine's root-free verb: nil is no answerer at all (no responders).
|
|
rootFree func(machines []string) (any, error)
|
|
// consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and
|
|
// consumerCreates each consumer the part asked to create, `<stream>.<name>`.
|
|
consumers map[string][]map[string]any
|
|
consumerCreates []string
|
|
// maxValue, when set, refuses an own value larger than it as the bus does (JetStream 10054).
|
|
maxValue int
|
|
}
|
|
|
|
// MatchConsumer is the bus's update of a held durable consumer to the configuration given.
|
|
func (b *fakeBus) MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.consumerCreates = append(b.consumerCreates, "match "+stream+"."+cfg.Durable)
|
|
for _, c := range b.consumers[stream] {
|
|
if c["name"] == cfg.Durable {
|
|
c["max_deliver"] = cfg.MaxDeliver
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("consumer not found")
|
|
}
|
|
|
|
// CreateConsumer is the bus's create of a durable consumer: held when the bus holds one of that name, else held from now.
|
|
func (b *fakeBus) CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.consumerCreates = append(b.consumerCreates, stream+"."+cfg.Durable)
|
|
for _, c := range b.consumers[stream] {
|
|
if c["name"] == cfg.Durable {
|
|
return grants.ErrHeld
|
|
}
|
|
}
|
|
filters := append([]string{}, cfg.FilterSubjects...)
|
|
if cfg.FilterSubject != "" {
|
|
filters = []string{cfg.FilterSubject}
|
|
}
|
|
if b.consumers == nil {
|
|
b.consumers = map[string][]map[string]any{}
|
|
}
|
|
b.consumers[stream] = append(b.consumers[stream], map[string]any{"name": cfg.Durable, "filters": filters, "max_deliver": cfg.MaxDeliver})
|
|
return nil
|
|
}
|
|
|
|
func (b *fakeBus) PublishMembership(node, module string, body []byte) 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.stream == nil {
|
|
b.stream = map[string][]byte{}
|
|
}
|
|
b.stream[subject] = body
|
|
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.createdAge == nil {
|
|
b.createdAge = map[string]time.Duration{}
|
|
}
|
|
b.createdAge[cfg.Name] = cfg.MaxAge
|
|
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()
|
|
args, _ := body.(map[string]any)
|
|
// A node seat's verb is asked of one machine: `<verb>@<machine>`; the call is kept and judged by the verb alone.
|
|
machine := ""
|
|
if base, m, ok := strings.Cut(key, "@"); ok {
|
|
key, machine = base, m
|
|
}
|
|
b.calls = append(b.calls, key)
|
|
switch key {
|
|
case grants.CallUsers:
|
|
if b.refuseUsers {
|
|
return nil, errors.New("nats: no responders available for request")
|
|
}
|
|
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:
|
|
if stream, _ := args["stream"].(string); stream != "" {
|
|
return json.Marshal(map[string]any{"detail": []map[string]any{{"name": stream, "consumer_detail": b.consumers[stream]}}})
|
|
}
|
|
return json.Marshal(map[string]any{"detail": b.streams})
|
|
case grants.CallRootFree:
|
|
if b.rootFree == nil {
|
|
return nil, errors.New("nats: no responders available for request")
|
|
}
|
|
out, err := b.rootFree([]string{machine})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(out)
|
|
}
|
|
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)
|
|
}
|
|
if b.maxValue > 0 && len(value) > b.maxValue {
|
|
return 0, errors.New("nats: API error: code=400 err_code=10054 description=message size exceeds maximum allowed")
|
|
}
|
|
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 (s ownStore) Keys(state string) ([]string, error) {
|
|
b := s.b
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
var out []string
|
|
for k := range b.own[state] {
|
|
out = append(out, k)
|
|
}
|
|
slices.Sort(out)
|
|
return out, nil
|
|
}
|
|
|
|
func (s ownStore) Delete(state, key string) error {
|
|
b := s.b
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.writes = append(b.writes, state+" delete")
|
|
delete(b.own[state], key)
|
|
return 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"})
|
|
}
|
|
|
|
// refuse marks the indexer's refusal of module's manifest in its value (novox/hq ADR 0578 §1).
|
|
func (b *fakeBus) refuse(module, raw, problem string) {
|
|
b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
|
Manifest: delivery.ManifestDigest([]byte(raw)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
|
|
Problems: []string{problem}})
|
|
}
|
|
|
|
// assign places module on node in the seat's placement, the one place the part reads it (ADR 0549 §5; issue 774).
|
|
func (b *fakeBus) assign(node, module string) {
|
|
var pl delivery.Placement
|
|
if raw, ok := b.states[grants.ReadPlacement]["placement."+node]; ok {
|
|
pl, _ = delivery.DecodePlacement(raw)
|
|
}
|
|
pl.Node = node
|
|
for _, m := range pl.Modules {
|
|
if m.Module == module {
|
|
return
|
|
}
|
|
}
|
|
pl.Modules = append(pl.Modules, delivery.PlacedModule{Module: module})
|
|
raw, err := delivery.EncodePlacement(pl)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if b.states[grants.ReadPlacement] == nil {
|
|
b.states[grants.ReadPlacement] = map[string][]byte{}
|
|
}
|
|
b.states[grants.ReadPlacement]["placement."+node] = raw
|
|
}
|
|
|
|
// unassign takes module off node in the seat's placement.
|
|
func (b *fakeBus) unassign(node, module string) {
|
|
pl, err := delivery.DecodePlacement(b.states[grants.ReadPlacement]["placement."+node])
|
|
if err != nil {
|
|
return
|
|
}
|
|
pl.Modules = slices.DeleteFunc(pl.Modules, func(m delivery.PlacedModule) bool { return m.Module == module })
|
|
raw, _ := delivery.EncodePlacement(pl)
|
|
b.states[grants.ReadPlacement]["placement."+node] = raw
|
|
}
|
|
|
|
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","max-age-days":30}]}`
|
|
yManifest = `{"module":"mod-y","version":"1","own-secrets":{"broker":"/var/lib/y/broker"},"reads":["mod-x.kept"]}`
|
|
// vaultManifest is the vault as the composition needs it for a rotation (ADR 0583 §2, §3): its claim serving the verbs.
|
|
vaultManifest = `{"module":"mesh-vault","version":"1","own-secrets":{"broker":"/var/lib/v/broker"},` +
|
|
`"provides":[{"name":"secret","scope":"mesh"}],` +
|
|
`"claims":[{"name":"mesh-vault","serves":["rotate-node","node-account-confirm","abandon"]}],` +
|
|
`"tools":["rotate-node","node-account-confirm","abandon"],"state":[{"name":"node-logins"}]}`
|
|
// enrolmentManifest is the holder of mesh-enrolment as the composition needs it (ADR 0587 §1): the claim, its six verbs
|
|
// and its bucket.
|
|
enrolmentManifest = `{"module":"mesh-enrolment","version":"1","own-secrets":{"broker":"/var/lib/e/broker"},` +
|
|
`"claims":[{"name":"mesh-enrolment","serves":["issue","redeem","confirm","revoke","tokens","again"]}],` +
|
|
`"tools":["issue","redeem","confirm","revoke","tokens","again"],"state":[{"name":"enrolments","key-ttl":true}]}`
|
|
)
|
|
|
|
// brokerSeat is one verb of the bus's seat as a membership serves it: nats claims the seat in fixture and no membership
|
|
// composed there serves it, so a membership or list that drops it leaves the seat with no holder (issue 1945).
|
|
func brokerSeat() (seat, verb string) {
|
|
s, _ := catalogue.SeatNamed(catalogue.BrokerSeat)
|
|
verb = "v"
|
|
if len(s.Serves) > 0 {
|
|
verb = s.Serves[0].Name
|
|
}
|
|
return s.Name, verb
|
|
}
|
|
|
|
// 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.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}})
|
|
return b
|
|
}
|
|
|
|
func partOn(b *fakeBus) *grants.Part {
|
|
return &grants.Part{Read: b, Call: b, Store: ownStore{b}, Emit: 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.Decode(raw)
|
|
if err != nil {
|
|
t.Fatalf("mesh-sdk cannot decode the manifest: %v", err)
|
|
}
|
|
// The part claims no seat: the seat mesh-grants served the lease write alone, which went with the controller
|
|
// (novox/hq ADR 0590 §3, issue 1126).
|
|
if m.Module != "mesh-grants" || len(m.Claims) != 0 {
|
|
t.Errorf("module %q claims %v: the part claims no seat", m.Module, m.Claims)
|
|
}
|
|
for _, row := range []struct {
|
|
what string
|
|
got, want []string
|
|
}{
|
|
{"reads", m.Reads, grants.Reads},
|
|
{"invokes", m.Invokes, grants.Invokes},
|
|
{"consumes", m.Consumes, grants.Consumes},
|
|
{"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 conditions, 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 condition, on the seat operator-channel's subject, in asking.go; a membership is written to
|
|
// the part's state alone, never said on its subject (issue 1126). The one create: a declared stream the bus lacks,
|
|
// in create.go (issue 1790).
|
|
createOnly := filepath.Join("cmd", "mesh-grants", "create.go")
|
|
asksOnly := filepath.Join("cmd", "mesh-grants", "asking.go")
|
|
sealedOnly := filepath.Join("cmd", "mesh-grants", "sealed.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 != asksOnly && (strings.Contains(string(src), ".Publish(") || strings.Contains(string(src), "mesh.assignment")) {
|
|
t.Errorf("%s publishes: only asking.go publishes, a condition", path)
|
|
}
|
|
if path == asksOnly && (strings.Count(string(src), ".Publish(") != 1 || strings.Contains(string(src), "mesh.assignment")) {
|
|
t.Errorf("asking.go publishes other than a condition, through js.Publish once")
|
|
}
|
|
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)
|
|
}
|
|
// The one object bucket: the part's own, of the user list sealed to the bus node, in sealed.go (ADR 0547 clause 7).
|
|
if path == sealedOnly {
|
|
if strings.Count(string(src), "CreateObjectStore(") != 1 || !strings.Contains(string(src), "delivery.GrantsUserListObjects") {
|
|
t.Errorf("sealed.go creates other than the part's one object bucket")
|
|
}
|
|
src = []byte(strings.ReplaceAll(string(src), "CreateObjectStore", ""))
|
|
}
|
|
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"}, "people"},
|
|
{"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; the list's records carry both (issue 774)", func(b *fakeBus) {
|
|
built := strings.Replace(xManifest, `{"name":"kept","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"}, "people"},
|
|
{"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadPlacement] = true },
|
|
[]string{"controller", "node.a", "node.b"}, grants.ReadPlacement},
|
|
} {
|
|
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.>") || slices.Contains(x.Publish, "$KV.mod-x_kept.>") {
|
|
t.Errorf("a.mod-x is not granted the state its wanted build declares alone: %v", x.Publish)
|
|
}
|
|
// The list composes what runs and what the trunk names: a subject only the trunk's manifest names is in
|
|
// it before the want moves to the trunk's build (issue 774).
|
|
listRecs, _ := grants.ListRecords(in)
|
|
listed, err := grants.ComposeUsers(listRecs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := names(listed); !slices.Equal(got, row.users) {
|
|
t.Errorf("the list's users = %v, want %v", got, row.users)
|
|
}
|
|
lx := listed[slices.IndexFunc(listed, func(u grants.User) bool { return u.User == "a.mod-x" })]
|
|
if !slices.Contains(lx.Publish, "$KV.mod-x_newer.>") || !slices.Contains(lx.Publish, "$KV.mod-x_kept.>") {
|
|
t.Errorf("the list does not grant a.mod-x both its running and its trunk manifest's state: %v", lx.Publish)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
// A module whose only runtime-loaded part is a helper bundle (route-proxy's route-provider) says `loads` in
|
|
// the repository manifest's build.artifacts; the indexer's manifest has no derived Bundles (issue 1945).
|
|
for _, row := range []struct {
|
|
name, build string
|
|
carried bool
|
|
}{
|
|
{"a bundle that says loads is carried by node-tools", `,"build":{"artifacts":[{"name":"helper","kind":"bundle","language":"go","system":"arch","from":"cmd/h","binary":"h","loads":["h"]}]}`, true},
|
|
{"a module with no loading bundle is left to its own account", ``, false},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
b := fixture()
|
|
b.manifestOf("mod-z", `{"module":"mod-z","version":"1","own-secrets":{"broker":"/var/lib/z/broker"}`+row.build+`}`)
|
|
b.assign("a", "mod-z")
|
|
b.manifestOf("node-tools", `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/nt/broker"}}`)
|
|
b.assign("a", "node-tools")
|
|
users := shadowOf(t, b)
|
|
tools := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "a.node-tools" })]
|
|
if got := slices.Contains(tools.Subscribe, "mesh.mod.mod-z.tool.>"); got != row.carried {
|
|
t.Errorf("a.node-tools subscribes to mod-z's tools = %v, want %v: %v", got, row.carried, tools.Subscribe)
|
|
}
|
|
})
|
|
}
|
|
// ADR 0567 §3, §9: the seat mesh-placement's candidates are read by every node agent (mesh-sdk NodeComposeReads)
|
|
// and by a module whose manifest reads them (the view), written by their holder, the assigner, and read by no other.
|
|
cands := fixture()
|
|
cands.manifestOf("mesh-delivery-assigner", `{"module":"mesh-delivery-assigner","version":"1","own-secrets":{"broker":"/var/lib/as/broker"},"state":[{"name":"candidates","history":5}]}`)
|
|
cands.manifestOf("mesh-delivery-view", `{"module":"mesh-delivery-view","version":"1","own-secrets":{"broker":"/var/lib/v/broker"},"reads":["mesh-delivery-assigner.candidates"]}`)
|
|
cands.manifestOf("mesh-delivery-indexer", `{"module":"mesh-delivery-indexer","version":"1","own-secrets":{"broker":"/var/lib/i/broker"}}`)
|
|
for _, m := range []string{"mesh-delivery-indexer", "mesh-delivery-assigner", "mesh-delivery-view"} {
|
|
cands.assign("a", m)
|
|
}
|
|
var readers []string
|
|
for _, u := range shadowOf(t, cands) {
|
|
if slices.Contains(u.Publish, "$JS.API.DIRECT.GET.KV_"+delivery.PlacementCandidatesBucket+".>") {
|
|
readers = append(readers, u.User)
|
|
}
|
|
}
|
|
if want := []string{"a.mesh-delivery-assigner", "a.mesh-delivery-view", "node.a", "node.b"}; !slices.Equal(readers, want) {
|
|
t.Errorf("the placement candidates are read by %v, want the node agents, the view and their holder %v", readers, want)
|
|
}
|
|
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("withheld: a hearing of a restricted stream in the tool runner is written on change only, and deleted once heard (issue 1955 L7)", func(t *testing.T) {
|
|
const mail = `{"module":"mesh-mail","version":"1","streams":[{"name":"mail","events":["mail.*.message.added"],"max-age-days":7,` +
|
|
`"grant":{"config":"grants","events":"mail.{key}.**"}}],"resources":[{"id":"state","type":"directory","mode":"0700","place":"."},` +
|
|
`{"id":"grants","trusted":true,"type":"file","path":"${dir:state}/grants.json","mode":"0600","merge":"json","takes-settings":["grants"],"content":"{\"grants\": {}}"}]}`
|
|
b := fixture()
|
|
b.manifestOf("mesh-mail", mail)
|
|
b.manifestOf("reader", `{"module":"reader","version":"1","consumes":["mesh-mail.mail.box.message.added"]}`)
|
|
b.assign("a", "mesh-mail")
|
|
b.assign("a", "reader")
|
|
b.put(grants.ReadResolved, "a.mesh-mail.grants", map[string]any{"node": "a", "module": "mesh-mail", "key": "grants", "value": map[string]any{"box": []string{"reader"}}})
|
|
p := partOn(b)
|
|
res := p.Compose()
|
|
const key = "reader.mail.a"
|
|
var h grants.WithheldHearing
|
|
if res.Withheld == nil || !slices.Equal(res.Withheld.Written, []string{key}) || json.Unmarshal(b.own[grants.StateWithheld][key], &h) != nil ||
|
|
h.Module != "reader" || h.Node != "a" || h.Stream != "MAIL" || h.Emitter != "mesh-mail" || !strings.Contains(h.Said, "runs-as") {
|
|
t.Fatalf("withheld = %+v, stored %+v (errors %v), want %s written naming the remedy", res.Withheld, h, res.Errors, key)
|
|
}
|
|
if again := p.Compose(); again.Withheld == nil || len(again.Withheld.Written) != 0 || again.Withheld.Withheld != 1 {
|
|
t.Errorf("a second pass wrote %+v, want nothing (on change only)", again.Withheld)
|
|
}
|
|
b.manifestOf("reader", `{"module":"reader","version":"1","runs-as":"reader","resources":[{"id":"account","type":"user","name":"reader","shell":"/usr/bin/nologin","home":"/var/lib/reader"}],`+
|
|
`"own-secrets":{"broker":"/var/lib/r/broker"},"secrets-owner":"reader","consumes":["mesh-mail.mail.box.message.added"]}`)
|
|
if heard := p.Compose(); heard.Withheld == nil || !slices.Equal(heard.Withheld.Deleted, []string{key}) || b.own[grants.StateWithheld][key] != nil {
|
|
t.Errorf("once reader runs as its own account the pass did %+v (errors %v), want %s deleted", heard.Withheld, heard.Errors, key)
|
|
}
|
|
})
|
|
|
|
t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
|
|
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
|
|
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
|
|
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
|
|
const telegram = `{"module":"telegram","version":"1","runs-as":"telegram","resources":[{"id":"account","type":"user","name":"telegram","shell":"/usr/bin/nologin","home":"/var/lib/telegram"}],"own-secrets":{"broker":"/var/lib/t/broker"},"secrets-owner":"telegram",` +
|
|
`"claims":[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]}`
|
|
answer := func(free ...string) func([]string) (any, error) {
|
|
return func(machines []string) (any, error) {
|
|
var vs []map[string]any
|
|
for _, m := range machines {
|
|
ok := slices.Contains(free, m)
|
|
why := "judged free"
|
|
if !ok {
|
|
why = "its node-engine judged nothing"
|
|
}
|
|
vs = append(vs, map[string]any{"machine": m, "free": ok, "why": why})
|
|
}
|
|
return map[string]any{"machines": vs}, nil
|
|
}
|
|
}
|
|
for _, row := range []struct {
|
|
name string
|
|
routerOn []string
|
|
telegramOn string
|
|
ask func([]string) (any, error)
|
|
keeps bool
|
|
note string
|
|
}{
|
|
{"router and telegram on one free machine", []string{"a"}, "a", answer("a"), true, ""},
|
|
{"router and telegram on two free machines", []string{"a"}, "b", answer("a", "b"), true, ""},
|
|
{"the holder's machine is not free", []string{"a"}, "b", answer("a"), false, "b: not free, its node-engine judged nothing"},
|
|
{"the router's machine is not free", []string{"a"}, "b", answer("b"), false, "a: not free"},
|
|
{"an error is not free", []string{"a"}, "a", func([]string) (any, error) { return nil, errors.New("boom") }, false, "boom"},
|
|
{"no answerer is not free", []string{"a"}, "a", nil, false, "no responders"},
|
|
{"a machine left out of the answer is not free", []string{"a"}, "a",
|
|
func([]string) (any, error) { return map[string]any{"machines": []any{}}, nil }, false, "named no verdict"},
|
|
{"the router on two machines frees nothing", []string{"a", "b"}, "a", answer("a", "b"), false, ""},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
b := fixture()
|
|
b.manifestOf("messenger", router)
|
|
b.manifestOf("telegram", telegram)
|
|
for _, node := range row.routerOn {
|
|
b.assign(node, "messenger")
|
|
}
|
|
b.assign(row.telegramOn, "telegram")
|
|
b.rootFree = row.ask
|
|
res := partOn(b).Compose()
|
|
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
|
|
grants.WithRootFree(&recs, b)
|
|
kept := false
|
|
for _, k := range buscompose.PlacementsOf(recs, recs.Interchangeable).Kinds {
|
|
kept = kept || slices.Contains(k.Capabilities, "verified-sender")
|
|
}
|
|
if kept != row.keeps {
|
|
t.Errorf("verified-sender kept = %v, want %v (said %v)", kept, row.keeps, res.RootNotFree)
|
|
}
|
|
if !strings.Contains(strings.Join(res.RootNotFree, " | "), row.note) || (row.note == "" && row.keeps && len(res.RootNotFree) != 0) {
|
|
t.Errorf("the pass says %v, want it to name %q", res.RootNotFree, row.note)
|
|
}
|
|
})
|
|
}
|
|
// A root-not-free condition raised or cleared wakes a pass; another kind does not (never a timer).
|
|
wb := fixture()
|
|
var handle func(string, []byte)
|
|
wf := &grants.Follower{Part: partOn(wb), Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(string, ...any) {},
|
|
Hear: func(h func(string, []byte)) error { handle = h; return nil }}
|
|
wctx, wcancel := context.WithCancel(context.Background())
|
|
wdone := make(chan struct{})
|
|
go func() { wf.Run(wctx); close(wdone) }()
|
|
waitFor(t, func() bool { return wb.count(grants.CallUsers) > 0 && handle != nil })
|
|
passes := func() int { return wb.count(grants.CallUsers) }
|
|
before := passes()
|
|
handle(grants.EventKeeperRaised, []byte(`{"kind":"disk-full","key":"machine.a.disk-full"}`))
|
|
time.Sleep(100 * time.Millisecond)
|
|
if passes() != before {
|
|
t.Errorf("a condition of another kind woke a pass")
|
|
}
|
|
// 1126/B7b (ADR 0590 §3): only the keeper's events are heard, with no take-over key read; the controller's never.
|
|
handle("mesh.seat.mesh-controller.event.condition-raised", []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
|
time.Sleep(100 * time.Millisecond)
|
|
if passes() != before {
|
|
t.Errorf("the controller's event woke a pass")
|
|
}
|
|
handle("mesh.seat.mesh-conditions.event.condition-raised", []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
|
waitFor(t, func() bool { return passes() > before })
|
|
before = passes()
|
|
handle(grants.EventKeeperCleared, []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
|
waitFor(t, func() bool { return passes() > before })
|
|
if slices.Contains(grants.Followed, "mesh-conditions.takeover") || slices.Contains(grants.Reads, "mesh-conditions.takeover") {
|
|
t.Errorf("the part still reads the keeper's take-over key: %v", grants.Reads)
|
|
}
|
|
wcancel()
|
|
<-wdone
|
|
if !grants.IsRootNotFree([]byte(`{"kind":"root-not-free"}`)) || !grants.IsRootNotFree([]byte(`{"key":"machine.novox.root-not-free"}`)) ||
|
|
grants.IsRootNotFree([]byte(`{"kind":"disk-full","key":"machine.novox.disk-full"}`)) {
|
|
t.Errorf("only a root-not-free condition wakes a pass")
|
|
}
|
|
})
|
|
|
|
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 the indexer's judge refused (ADR 0578 §3), 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.refuse("mod-z", zManifest, "it carries no-such-key")
|
|
b.assign("b", "mod-p")
|
|
// mesh-wireguard is provided by the mesh, not read from a repository: no manifest in the indexer, none refused,
|
|
// no gap, and its manifest is the one mesh-sdk provides (issue 1945).
|
|
b.assign("b", "mesh-wireguard")
|
|
in := grants.ReadInputs(b, &grants.Manifests{})
|
|
// An indexer row of the same name never wins: the provided manifest is used and the row is said as a gap.
|
|
bw := fixture()
|
|
bw.manifestOf("mesh-wireguard", `{"module":"mesh-wireguard","version":"9","own-secrets":{"broker":"/x"}}`)
|
|
bw.assign("b", "mesh-wireguard")
|
|
if iw := grants.ReadInputs(bw, &grants.Manifests{}); !strings.HasPrefix(iw.Digests["mesh-wireguard"], "provided:") || !gapNamed(iw.Gaps, "manifest of mesh-wireguard") || len(iw.Refused) != 0 {
|
|
t.Errorf("an indexer manifest of mesh-wireguard won or went unsaid: %v %v", iw.Digests["mesh-wireguard"], iw.Gaps)
|
|
}
|
|
if _, ok := in.Manifests["mesh-wireguard"]; !ok || gapNamed(in.Gaps, "manifest of mesh-wireguard") || !strings.HasPrefix(in.Digests["mesh-wireguard"], "provided:") {
|
|
t.Errorf("mesh-wireguard's manifest is not taken from mesh-sdk: gaps %v", in.Gaps)
|
|
}
|
|
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))
|
|
}
|
|
// Issue 1126: a key a module's served value fills from its settings (mailu's smtp domain) is granted to every other
|
|
// node's agent, that key alone, so a node composing a consumer elsewhere settles what it serves.
|
|
bs := fixture()
|
|
bs.manifestOf("mod-s", `{"module":"mod-s","version":"1","own-secrets":{"broker":"/var/lib/s/broker"},`+
|
|
`"config":{"domain":{"type":"string","description":"The mail domain.","required":true}},`+
|
|
`"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}`)
|
|
bs.assign("b", "mod-s")
|
|
served := "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.b.mod-s.domain"
|
|
for _, u := range shadowOf(t, bs) {
|
|
if (u.User == "node.a") != slices.Contains(u.Publish, served) {
|
|
t.Errorf("%s is granted b's mod-s domain: %v (node a alone)", u.User, slices.Contains(u.Publish, served))
|
|
}
|
|
}
|
|
// The provider not yet moved: it runs a build whose manifest serves no setting, and the trunk's adds one, which a
|
|
// node composing a consumer reads (frombus reads the trunk's manifest of another node's module): granted too.
|
|
bt := fixture()
|
|
const oldS = `{"module":"mod-s","version":"1","own-secrets":{"broker":"/var/lib/s/broker"},` +
|
|
`"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587}}}`
|
|
bt.manifestOf("mod-s", oldS)
|
|
oldDigest := delivery.ManifestDigest([]byte(oldS))
|
|
bt.manifestOf("mod-s", `{"module":"mod-s","version":"1","own-secrets":{"broker":"/var/lib/s/broker"},`+
|
|
`"config":{"domain":{"type":"string","description":"The mail domain.","required":true}},`+
|
|
`"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}`)
|
|
bt.assign("b", "mod-s")
|
|
runsFP := "src2:" + strings.Repeat("cd", 32)
|
|
bt.put(grants.ReadWants, "want.b.mod-s", delivery.Want{V: delivery.Version, FP: runsFP, Composed: "sha256:" + strings.Repeat("ef", 32)})
|
|
runsKey, _ := delivery.BuilderBuildKey("mod-s", runsFP)
|
|
bt.put(grants.ReadBuilds, runsKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": oldDigest})
|
|
for _, u := range shadowOf(t, bt) {
|
|
if u.User == "node.a" && !slices.Contains(u.Publish, served) {
|
|
t.Errorf("node a is not granted the domain the trunk's mod-s serves, while b runs one that serves none")
|
|
}
|
|
}
|
|
// 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)
|
|
}
|
|
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")
|
|
// Only added rights (ADR 0562): the loaded list lacks a publish of a.mod-x the shadow grants.
|
|
fewer := slices.Clone(shadow)
|
|
fewer[x].Publish = slices.Clone(fewer[x].Publish[1:])
|
|
// A deny the shadow adds where the loaded list says its own (empty): a publish right lost. And the same deny where
|
|
// the loaded list says none, as nats_users answers today, on a user whose grants differ: it cannot be compared.
|
|
denied := slices.Clone(shadow)
|
|
denied[x].PublishDeny = []string{"mesh.seat.node-launcher.tool.secret"}
|
|
readable := slices.Clone(fewer)
|
|
for i := range readable {
|
|
readable[i].PublishDeny = []string{}
|
|
}
|
|
// The live comparison the operator's yes was refused on, 2026-10-27 21:19 UTC (issue 1945): 41 users differ, every
|
|
// difference an added right. Read whole; a row's mutate makes one of the cases that must still refuse.
|
|
var live struct {
|
|
Compare grants.Comparison `json:"compare"`
|
|
Credentials grants.Credentials `json:"credentials"`
|
|
}
|
|
if raw, err := os.ReadFile(filepath.Join("testdata", "takeover-compare-2026-10-27.json")); err != nil || json.Unmarshal(raw, &live) != nil {
|
|
t.Fatalf("the captured comparison cannot be read: %v", err)
|
|
}
|
|
if len(live.Compare.Differ) != 41 || len(live.Credentials.LosesLogin)+len(live.Credentials.LeftOut) != 0 {
|
|
t.Fatalf("the captured comparison is not the one of 21:19: %d differ", len(live.Compare.Differ))
|
|
}
|
|
captured := func(mutate func(*grants.Comparison)) *grants.Comparison {
|
|
raw, _ := json.Marshal(live.Compare)
|
|
var c grants.Comparison
|
|
_ = json.Unmarshal(raw, &c)
|
|
if mutate != nil {
|
|
mutate(&c)
|
|
}
|
|
return &c
|
|
}
|
|
nodeAce := func(c *grants.Comparison) *grants.UserDiff {
|
|
return &c.Differ[slices.IndexFunc(c.Differ, func(d grants.UserDiff) bool { return d.User == "node.ace" })]
|
|
}
|
|
for _, row := range []struct {
|
|
name string
|
|
loaded []grants.User
|
|
added, dropped, gone []string
|
|
differ []string
|
|
equal bool
|
|
// live is a captured comparison used in place of Compare; removing the users take-over is refused for
|
|
// (ADR 0562 §2), and adds the users whose added rights the ask names.
|
|
live *grants.Comparison
|
|
removing []string
|
|
adds int
|
|
}{
|
|
{"equal", shadow, nil, nil, nil, nil, true, nil, nil, 0},
|
|
{"one user the bus lacks", shadow[1:], []string{shadow[0].User}, nil, nil, nil, false, nil, []string{shadow[0].User}, 0},
|
|
{"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, nil, []string{"a.mod-x.worker"}, 0},
|
|
{"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, nil, nil, 0},
|
|
{"grants differ", changed, nil, nil, nil, []string{"a.mod-x"}, false, nil, []string{"a.mod-x"}, 1},
|
|
{"only a right added", fewer, nil, nil, nil, []string{"a.mod-x"}, false, nil, nil, 1},
|
|
{"a deny added refuses", readable, nil, nil, nil, []string{"a.mod-x"}, false, nil, []string{"a.mod-x"}, 1},
|
|
{"a deny the loaded list cannot say refuses a user whose grants differ", fewer, nil, nil, nil, []string{"a.mod-x"}, false, nil, []string{"a.mod-x"}, 1},
|
|
{"captured 21:19: only added rights", nil, nil, nil, nil, nil, false, captured(nil), nil, 41},
|
|
{"captured, a right removed", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) {
|
|
nodeAce(c).SubscribeDropped = []string{"mesh.mod.mesh-vault.event.secret-changed"}
|
|
}), []string{"node.ace"}, 41},
|
|
{"captured, responses lost", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) {
|
|
nodeAce(c).Responses = "shadow false, loaded true"
|
|
}), []string{"node.ace"}, 41},
|
|
{"captured, a user dropped", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) {
|
|
c.Dropped = []string{"novox.mesh-vault.worker"}
|
|
}), []string{"novox.mesh-vault.worker"}, 41},
|
|
{"captured, a user added", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) {
|
|
c.Added = []string{"shanks.new-module"}
|
|
}), []string{"shanks.new-module"}, 41},
|
|
{"captured, a login lost", nil, nil, nil, nil, nil, false, captured(nil), []string{"novox.telegram"}, 41},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
var c grants.Comparison
|
|
if row.live != nil {
|
|
c = *row.live
|
|
} else {
|
|
from := shadow
|
|
if strings.Contains(row.name, "deny") {
|
|
from = denied
|
|
}
|
|
c = grants.Compare(from, 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)
|
|
}
|
|
}
|
|
var cr grants.Credentials
|
|
if row.live != nil {
|
|
cr = live.Credentials
|
|
}
|
|
if strings.Contains(row.name, "login lost") {
|
|
cr.LosesLogin = []string{"novox.telegram"}
|
|
}
|
|
// Removing, and the users with no hash in the vault, are what a list in force is held for.
|
|
var removing []string
|
|
for _, r := range c.Removing() {
|
|
removing = append(removing, strings.Fields(r)[0])
|
|
}
|
|
removing = append(removing, cr.LosesLogin...)
|
|
if !slices.Equal(removing, row.removing) || len(c.AddedRights()) != row.adds {
|
|
t.Errorf("removing %v, want %v; added rights of %d users, want %d", c.Removing(), row.removing, len(c.AddedRights()), row.adds)
|
|
}
|
|
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])
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// 1126 (ADR 0549 §1, §3): once the catalogue places the nodes, a seat held once for the mesh is held where placement
|
|
// says, not on every machine running a module that claims it; what placement adds is composed beside the assignments.
|
|
t.Run("a mesh seat is held where placement says", func(t *testing.T) {
|
|
store := catalogue.Manifest{Module: "store", Version: "1",
|
|
DefinesSeats: []catalogue.SeatDeclaration{{Name: "store-seat", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{{Name: "databases"}}}},
|
|
Claims: []catalogue.Claim{{Name: "store-seat", Scope: catalogue.ScopeMesh}}}
|
|
in := grants.Inputs{Nodes: []string{"a", "b"}, Assigned: map[string][]string{"a": {"store"}, "b": {"store"}},
|
|
Manifests: map[string]catalogue.Manifest{"store": store}}
|
|
holds := func(recs buscompose.Records, node string) int {
|
|
n := 0
|
|
for _, d := range recs.Assigned[node] {
|
|
n += len(d.Holds)
|
|
}
|
|
return n
|
|
}
|
|
if recs, _ := grants.Records(in); holds(recs, "a") != 1 || holds(recs, "b") != 1 {
|
|
t.Fatalf("before placement the seat is held on a %d and b %d times, want both", holds(recs, "a"), holds(recs, "b"))
|
|
}
|
|
in.Placed = map[string]delivery.Placement{
|
|
"a": {Node: "a", Modules: []delivery.PlacedModule{{Module: "store"}}, Holds: []delivery.Holding{{Claim: "store-seat", Scope: "mesh", Module: "store"}}},
|
|
"b": {Node: "b", Modules: []delivery.PlacedModule{{Module: "store"}}}}
|
|
if recs, _ := grants.Records(in); holds(recs, "a") != 1 || holds(recs, "b") != 0 {
|
|
t.Errorf("placed, the seat is held on a %d and b %d times, want a alone", holds(recs, "a"), holds(recs, "b"))
|
|
}
|
|
b := fixture()
|
|
pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "mod-y"}, {Module: "mod-x"}}})
|
|
b.states[grants.ReadPlacement] = map[string][]byte{"placement.b": pl}
|
|
read := grants.ReadInputs(b, &grants.Manifests{})
|
|
if !slices.Contains(read.Assigned["b"], "mod-x") || read.Placed["b"].Node != "b" || slices.ContainsFunc(read.Gaps, func(g grants.Gap) bool { return g.Input == "seat holdings" }) {
|
|
t.Errorf("read: assigned %v, placed %v, gaps %v", read.Assigned, read.Placed, read.Gaps)
|
|
}
|
|
// Research 072 row 37: a node key and a key per module, the legacy mirror left out, read the same.
|
|
perModule, _ := delivery.PlacementValues(delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "mod-y"}, {Module: "mod-x"}}})
|
|
delete(perModule, "placement.b")
|
|
nb := fixture()
|
|
nb.states[grants.ReadPlacement] = perModule
|
|
if got := grants.ReadInputs(nb, &grants.Manifests{}); !slices.Equal(got.Placed["b"].Assigned(), []string{"mod-y", "mod-x"}) ||
|
|
slices.ContainsFunc(got.Gaps, func(g grants.Gap) bool { return strings.HasPrefix(g.Input, grants.ReadPlacement) }) {
|
|
t.Errorf("read of the keys per module: placed %v, gaps %v", got.Placed, got.Gaps)
|
|
}
|
|
b.refused[grants.ReadPlacement] = true
|
|
// The placement alone places (issue 774, 774/S29): not read, nothing is placed, and it is a failed read.
|
|
if read := grants.ReadInputs(b, &grants.Manifests{}); !slices.ContainsFunc(read.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement && g.Failed }) ||
|
|
len(read.Assigned) != 0 {
|
|
t.Errorf("placement not on the bus is no failed read, or something is placed: %v, %v", read.Gaps, read.Assigned)
|
|
}
|
|
// Issue 1945, the user list of 2026-10-27 09:21: a placement's holds are the holders on record, not every holder.
|
|
// A mesh seat no placement holds keeps the module claiming it as its holder, wherever placement puts it.
|
|
in.Manifests["other"] = catalogue.Manifest{Module: "other", Version: "1", Claims: []catalogue.Claim{{Name: "other-seat", Scope: catalogue.ScopeMesh}},
|
|
DefinesSeats: []catalogue.SeatDeclaration{{Name: "other-seat", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{{Name: "v"}}}}}
|
|
in.Assigned["b"] = append(in.Assigned["b"], "other")
|
|
in.Placed["b"] = delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "other"}}}
|
|
if recs, _ := grants.Records(in); holds(recs, "a") != 1 || holds(recs, "b") != 1 {
|
|
t.Errorf("a seat no placement holds is held on b %d times, want once (and store-seat on a alone: a %d)", holds(recs, "b"), holds(recs, "a"))
|
|
}
|
|
in.Placed["b"] = delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "other"}},
|
|
Holds: []delivery.Holding{{Claim: "other-seat", Scope: "mesh", Module: "someone-else"}}}
|
|
if recs, _ := grants.Records(in); holds(recs, "b") != 0 {
|
|
t.Errorf("a seat placement holds by another module is still held by other on b")
|
|
}
|
|
// The live shapes: the seed's novox placement (mesh-catalog 0c61bad, its twelve holdings, without
|
|
// anthropic-licence-manager), the licence manager's manifest (mesh-catalog c05aa5e6), and the subjects the bus
|
|
// grants novox.node-tools for its seat (nats_users, 2026-10-27 19:40Z).
|
|
file := func(name string, v any) {
|
|
raw, err := os.ReadFile(filepath.Join("testdata", name))
|
|
if err == nil {
|
|
err = json.Unmarshal(raw, v)
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var seed delivery.Placement
|
|
var live struct{ Users []grants.User }
|
|
file("placement-novox-0c61bad.json", &seed)
|
|
file("node-tools-novox-licence-subjects.json", &live)
|
|
licence, _ := os.ReadFile(filepath.Join("testdata", "claude-licence-manager.json"))
|
|
n := newFakeBus()
|
|
// 1952/S5 (ADR 0564 Limits 4): the bus module's tools bundle declares its own account and what it may remove.
|
|
n.manifestOf("nats", strings.Replace(strings.TrimSuffix(natsManifest, "}"), `"broker":"/var/lib/nats/broker"`, `"broker":"/var/lib/nats/broker","broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"`, 1)+
|
|
`,"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go",`+
|
|
`"system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts"]}}]}}`)
|
|
n.manifestOf("claude-licence-manager", string(licence))
|
|
n.assign("novox", "nats")
|
|
n.assign("novox", "claude-licence-manager")
|
|
n.manifestOf("node-tools", `{"module":"node-tools","version":"1"}`) // the machine's tool runner, which carries the seat
|
|
n.assign("novox", "node-tools")
|
|
// The live placement names mesh-host; with its manifest's state the runner once carried it and the whole list was
|
|
// refused as a second writer of the engine's health (issue 774, 2026-10-30): the shadow below now composes.
|
|
n.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","consumes":["restic.backed-up"],"state":[{"name":"health"}]}`)
|
|
n.put(grants.ReadHave, "novox.node", map[string]any{"v": 1})
|
|
pl, _ = delivery.EncodePlacement(seed)
|
|
n.states[grants.ReadPlacement] = map[string][]byte{"placement.novox": pl}
|
|
recs, _ := grants.Records(grants.ReadInputs(n, &grants.Manifests{}))
|
|
var tools []buscompose.ProcessPrincipal
|
|
for _, d := range recs.Assigned["novox"] {
|
|
if d.Module == "nats" {
|
|
tools = d.Processes
|
|
}
|
|
}
|
|
if len(tools) != 1 || tools[0].Process != "nats-tools" || !slices.Equal(tools[0].Removes, []string{"DEAD_LETTER_NOTICES.controller", "mesh-host_acts"}) {
|
|
t.Errorf("the bus module's principals = %+v, want nats-tools carrying both removals", tools)
|
|
}
|
|
if m := grants.Memberships(recs)["novox.claude-licence-manager"]; len(m.Membership.Seats) == 0 {
|
|
t.Errorf("the licence manager's membership under the seeded placement carries no seats: %+v", m.Membership)
|
|
}
|
|
var runtime grants.User
|
|
for _, u := range shadowOf(t, n) {
|
|
if u.User == "novox.node-tools" {
|
|
runtime = u
|
|
}
|
|
}
|
|
if slices.ContainsFunc(runtime.Publish, func(s string) bool { return strings.HasPrefix(s, "$KV.mesh-host_") }) {
|
|
t.Errorf("novox.node-tools is granted a write of the node-engine's state: %v", runtime.Publish)
|
|
}
|
|
for _, s := range live.Users[0].Subscribe {
|
|
if strings.Contains(s, ".call.") && !slices.Contains(runtime.Subscribe, s) {
|
|
t.Errorf("under the seeded placement novox.node-tools is not granted %s, which the bus grants it today", s)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("compare excuses a retired verb and a multiply claimed mesh seat's added subscriptions on its machines, nothing else", func(t *testing.T) {
|
|
const retired, store, storeInfo = "mesh.seat.mesh-delivery.tool.mark-ready", "mesh.seat.mesh-store.tool.databases", "$SRV.INFO.mesh-store.>"
|
|
two := map[string][]string{"mesh-store": {"a", "n"}}
|
|
for _, row := range []struct {
|
|
name string
|
|
user string
|
|
subShadow, subBus, pubShadow []string
|
|
multi map[string][]string
|
|
differs bool
|
|
excused int
|
|
}{
|
|
{"a retired verb the shadow dropped", "a.node-tools", []string{"x"}, []string{"x", retired}, nil, nil, false, 1},
|
|
{"a verb of a live seat the shadow lost", "a.node-tools", []string{"x"}, []string{"x", store}, nil, two, true, 0},
|
|
{"a seat claimed on two machines, added to a user on one", "a.node-tools", []string{"x", store, storeInfo}, []string{"x"}, nil, two, false, 2},
|
|
{"the same added to a user on a machine that does not claim it", "q.node-tools", []string{"x", store}, []string{"x"}, nil, two, true, 0},
|
|
{"a publish of the seat added is not excused", "a.node-tools", []string{"x"}, []string{"x"}, []string{store}, two, true, 0},
|
|
{"the same added for a seat claimed once", "a.node-tools", []string{"x", store}, []string{"x"}, nil, nil, true, 0},
|
|
} {
|
|
c := grants.Compare([]grants.User{{User: row.user, Subscribe: row.subShadow, Publish: row.pubShadow}}, []grants.User{{User: row.user, Subscribe: row.subBus}}, nil).
|
|
Excusing(grants.ExcusedBy(row.multi))
|
|
if c.Equal() == row.differs || len(c.Excused) != row.excused {
|
|
t.Errorf("%s: equal %v, excused %v", row.name, c.Equal(), c.Excused)
|
|
}
|
|
}
|
|
// The guard of the user list and the memberships (issue 1945, the lead's decision in review round 1 of its fix): a
|
|
// seat the list in force holds left with no holder while a placed module still claims it, nothing else.
|
|
sm, _ := catalogue.SeatNamed(catalogue.ServiceManagerSeat)
|
|
nodeSeat := func(node string) string {
|
|
return "mesh.seat." + sm.Name + ".tool." + sm.Serves[0].Name + "." + node
|
|
}
|
|
h := grants.HoldingsOf(grants.Inputs{Assigned: map[string][]string{"novox": {"postgres", "systemd"}, "ace": {"postgres", "systemd"}},
|
|
Manifests: map[string]catalogue.Manifest{
|
|
"postgres": {Module: "postgres", Claims: []catalogue.Claim{{Name: "mesh-store", Scope: catalogue.ScopeMesh}}},
|
|
"systemd": {Module: "systemd", Claims: []catalogue.Claim{{Name: sm.Name, Scope: catalogue.ScopeNode}}}}})
|
|
u := func(user string, sub ...string) grants.User { return grants.User{User: user, Subscribe: sub} }
|
|
for _, row := range []struct {
|
|
name string
|
|
inForce, now []grants.User
|
|
lost []string
|
|
}{
|
|
{"09:21: the seat's holder lost, its claimant still placed", []grants.User{u("novox.node-tools", "x", store)}, []grants.User{u("novox.node-tools", "x")}, []string{"mesh-store"}},
|
|
{"a dropped tool", []grants.User{u("novox.node-tools", "mesh.mod.postgres.tool.gone", store)}, []grants.User{u("novox.node-tools", store)}, nil},
|
|
{"a seat moved from novox to ace", []grants.User{u("novox.node-tools", store)}, []grants.User{u("novox.node-tools"), u("ace.node-tools", store)}, nil},
|
|
{"a seat no placed module claims any more", []grants.User{u("novox.node-tools", "mesh.seat.unclaimed.tool.v")}, []grants.User{u("novox.node-tools")}, nil},
|
|
{"a retired verb dropped", []grants.User{u("novox.node-tools", retired)}, []grants.User{u("novox.node-tools")}, nil},
|
|
{"a node seat lost on novox, held on ace", []grants.User{u("novox.node-tools", nodeSeat("novox")), u("ace.node-tools", nodeSeat("ace"))},
|
|
[]grants.User{u("novox.node-tools"), u("ace.node-tools", nodeSeat("ace"))}, []string{sm.Name + "@novox"}},
|
|
} {
|
|
if lost := h.LastHolders(h.ByUsers(row.inForce), h.ByUsers(row.now)); !slices.Equal(lost, row.lost) {
|
|
t.Errorf("%s: lost %v, want %v", row.name, lost, row.lost)
|
|
}
|
|
}
|
|
})
|
|
|
|
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)
|
|
// The bus node says its key in its have of the bus module; the process puts the sealed list (ADR 0547 clause 7).
|
|
busKey, err := seal.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
withKey := func(w delivery.UserListWritten) {
|
|
w.Key = busKey.PublicKey
|
|
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &w})
|
|
}
|
|
loadedBefore := delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}
|
|
withKey(loadedBefore)
|
|
objs := &fakeObjects{}
|
|
p.Objects = objs
|
|
open := func(g delivery.GrantsUserList) string {
|
|
t.Helper()
|
|
list, err := delivery.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
|
|
out, err := seal.Open(bx, busKey.PrivateKey)
|
|
return []byte(out), err
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("the sealed list does not open with the bus node's key: %v", err)
|
|
}
|
|
return string(list)
|
|
}
|
|
|
|
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)
|
|
}
|
|
// The bus node follows the part's list alone (issue 1126): a first list that would leave out every loaded user
|
|
// while the vault holds no hash is held, nothing sealed, the key saying follow grants; a tool's part (no object
|
|
// bucket) writes none.
|
|
if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowGrants || !g.LeaseHanded || g.Digest != "" ||
|
|
len(objs.objects) != 0 || first.Sealed == nil || !strings.Contains(first.Sealed.Why, "would be left out") {
|
|
t.Errorf("the first sealed list = %+v, %d objects, %+v", g, len(objs.objects), first.Sealed)
|
|
}
|
|
if _, _, had := partOn(fixture()).Sealed(); had {
|
|
t.Error("a part read before any pass holds a sealed list")
|
|
}
|
|
if tp := partOn(fixture()); tp.Compose().Sealed == nil {
|
|
t.Error("a tool's pass does not say the sealed list")
|
|
} else if _, _, had := tp.Sealed(); had {
|
|
t.Error("a tool's part wrote the sealed list")
|
|
}
|
|
// Written to the state alone, never said on their subject, with no lease key (1126/A1).
|
|
if len(first.Published) != 3 || first.MembershipsWritten != 3 || len(b.publishes) != 0 {
|
|
t.Errorf("the first composition published %v (%d on the subject), want the three assignments' memberships in the state alone", first.Published, len(b.publishes))
|
|
}
|
|
// 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) != 0 {
|
|
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 loses the module's plain
|
|
// subject (mod-x now runs on two machines), which leaves no seat without its holder, so it is published too (issue
|
|
// 1945, the lead's decision in review round 1 of its fix).
|
|
b.assign("b", "mod-x")
|
|
moved := p.Compose()
|
|
if !moved.Written || moved.Revision != 2 || !slices.Equal(moved.Published, []string{"a.mod-x", "b.mod-x"}) || len(moved.NotPublished) != 0 || 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"]), "membership:") || mo["last_pass"] == nil || mo["composed"] == nil {
|
|
t.Errorf("membership = %v", ms)
|
|
}
|
|
gr, _ := p.Grants("mod-x")
|
|
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller"] != nil || g["controller_count"] != nil {
|
|
t.Errorf("grants = %v", gr)
|
|
}
|
|
|
|
var said []asks.Condition
|
|
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
|
// settled is the placement of the lists agreeing, every hash held, and the loaded list as edit leaves it.
|
|
settled := func(edit func(modX *grants.User)) func() {
|
|
return func() {
|
|
b.states[grants.ReadPlacement] = 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"})
|
|
x := slices.IndexFunc(b.loaded, func(u grants.User) bool { return u.User == "a.mod-x" })
|
|
b.loaded[x].Publish = slices.Clone(b.loaded[x].Publish)
|
|
edit(&b.loaded[x])
|
|
for _, u := range shadowOf(t, b) {
|
|
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1})
|
|
}
|
|
}
|
|
}
|
|
lacks := func(modX *grants.User) { modX.Publish = modX.Publish[1:] } // the shadow adds a right (ADR 0562)
|
|
// The bus node follows the part's list alone since the controller's end (novox/hq ADR 0590, issue 1126): with the
|
|
// lists apart only by an added right and every hash held, the list is sealed whole, the key saying follow grants.
|
|
settled(lacks)()
|
|
p.Compose()
|
|
g, _, _ := p.Sealed()
|
|
if g.Follow != delivery.FollowGrants || !g.LeaseHanded {
|
|
t.Errorf("the key says follow %s, lease handed %v; want grants with no lease key (1126/A1/AC2)", g.Follow, g.LeaseHanded)
|
|
}
|
|
if users := strings.Count(open(g), "{ user: "); users != g.Users || g.Users == 0 || len(g.LeftOut) != 0 {
|
|
t.Errorf("the sealed list holds %d users, the key says %d, left out %v", users, g.Users, g.LeftOut)
|
|
}
|
|
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: g.Revision, Wanted: g.Digest, Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants})
|
|
// Steady (issue 1126, 2026-10-28 00:16: the shadow list moved every 3 s, 99270 to 99357, with nothing
|
|
// differing): the bus node saying again that it loaded the same list, at a later time and the key's
|
|
// later revision, moves neither the shadow list nor the sealed key.
|
|
_, sealedRev, _ := p.Sealed()
|
|
p.Compose()
|
|
for i := 0; i < 3; i++ {
|
|
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: sealedRev + uint64(i+1), Wanted: g.Digest,
|
|
Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants, At: time.Date(2026, 10, 28, 0, 16, i, 0, time.UTC)})
|
|
if res := p.Compose(); res.Written {
|
|
t.Errorf("pass %d: the shadow list is written again for the bus node saying the same list again (revision %d)", i, res.Revision)
|
|
}
|
|
if _, rev, _ := p.Sealed(); rev != sealedRev {
|
|
t.Errorf("pass %d: the sealed key is written again with nothing changed: revision %d, was %d", i, rev, sealedRev)
|
|
}
|
|
}
|
|
withKey(loadedBefore)
|
|
// After take-over a list composed past a failed read, or one dropping a user of the list in force for
|
|
// want of a hash, is not sealed: the list in force stays, and the pass says why.
|
|
b.refused[grants.ReadAccounts] = true
|
|
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a read of this pass failed") {
|
|
t.Errorf("a pass past a failed read = %+v", res.Sealed)
|
|
}
|
|
// While the hold lasts the warning stands, said once however many passes find it; it names why.
|
|
p.Compose()
|
|
nHeld := 0
|
|
for _, c := range said {
|
|
if c.Key == grants.HeldCondition && c.State == asks.StateRaised && c.Severity == asks.SeverityWarning && strings.Contains(c.Summary, "a read of this pass failed") {
|
|
nHeld++
|
|
}
|
|
}
|
|
if nHeld != 1 {
|
|
t.Errorf("the held list raised the warning %d times, want once: %+v", nHeld, said)
|
|
}
|
|
b.refused[grants.ReadAccounts] = false
|
|
if now, _, _ := p.Sealed(); now.Digest != g.Digest || now.Object != g.Object {
|
|
t.Errorf("the list in force moved past a failed read: %s, was %s", now.Digest, g.Digest)
|
|
}
|
|
b.mu.Lock()
|
|
hash := b.states[grants.ReadAccounts]["a.mod-x"]
|
|
delete(b.states[grants.ReadAccounts], "a.mod-x")
|
|
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 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: %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")
|
|
}
|
|
if last := said[len(said)-1]; last.Key != grants.HeldCondition || last.State != asks.StateCleared {
|
|
t.Errorf("the held warning is not cleared once nothing is held: %+v", last)
|
|
}
|
|
// A part started while the warning stood (the said state is in memory only) clears it on its first pass.
|
|
var fresh []asks.Condition
|
|
fp := partOn(b)
|
|
fp.Objects = p.Objects
|
|
fp.Say = func(c asks.Condition) error {
|
|
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows
|
|
fresh = append(fresh, c)
|
|
}
|
|
return nil
|
|
}
|
|
fp.Compose()
|
|
if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != asks.StateCleared {
|
|
t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh)
|
|
}
|
|
// Issue 1945, 2026-10-27 09:21: a list that would leave a seat the list in force holds with no holder,
|
|
// while a placed module still claims it, is held, as such a membership is: not written, the warning
|
|
// raised naming the seat. A list that only adds is written; one whose loaded grants cannot be read is held.
|
|
inForce := slices.Clone(b.loaded)
|
|
seat, verb := brokerSeat()
|
|
seatGrant := "mesh.seat." + seat + ".call." + verb + ".*"
|
|
for i := range b.loaded {
|
|
if b.loaded[i].User == "a.nats" {
|
|
b.loaded[i].Subscribe = append(slices.Clone(b.loaded[i].Subscribe), seatGrant)
|
|
}
|
|
}
|
|
b.assign("a", "mod-y")
|
|
b.put(grants.ReadAccounts, "a.mod-y", map[string]any{"user": "a.mod-y", "hash": "$2a$10$x", "generation": 1})
|
|
said = nil
|
|
res := p.Compose()
|
|
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, "publishing would lose the last holder of "+seat) {
|
|
t.Errorf("a list losing a seat grant = %+v", res.Sealed)
|
|
}
|
|
if now, _, _ := p.Sealed(); now.Digest != g.Digest {
|
|
t.Errorf("a list losing a seat grant was written: %s, was %s", now.Digest, g.Digest)
|
|
}
|
|
if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "the last holder of "+seat) {
|
|
t.Errorf("the held list did not raise the warning naming the lost grant: %+v", said)
|
|
}
|
|
b.loaded = inForce
|
|
if p.Compose(); func() bool { now, _, _ := p.Sealed(); return now.Digest == g.Digest }() {
|
|
t.Errorf("a list that only adds a.mod-y was not written")
|
|
}
|
|
written, _, _ := p.Sealed()
|
|
b.mu.Lock()
|
|
b.refuseUsers = true
|
|
b.mu.Unlock()
|
|
b.mu.Lock()
|
|
b.unassign("a", "mod-y")
|
|
b.mu.Unlock()
|
|
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "the grants of the list in force cannot be read") {
|
|
t.Errorf("a pass whose loaded list cannot be read = %+v", res.Sealed)
|
|
}
|
|
if now, _, _ := p.Sealed(); now.Digest != written.Digest {
|
|
t.Errorf("a list was written while the loaded list could not be read")
|
|
}
|
|
b.mu.Lock()
|
|
b.refuseUsers = false
|
|
b.mu.Unlock()
|
|
|
|
t.Run("nothing published, nothing asserted", func(t *testing.T) {
|
|
if len(b.publishes) != 0 {
|
|
t.Errorf("published on %v", b.publishes)
|
|
}
|
|
for _, w := range b.writes {
|
|
if !slices.Contains(grants.States, strings.TrimSuffix(w, " delete")) {
|
|
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: published unless a seat would lose its last holder or a holder a capability it asks for", func(t *testing.T) {
|
|
const key = "a.mod-x"
|
|
brokerName, brokerVerb := brokerSeat()
|
|
// The telegram module's claim of its channel, as mesh-catalog's telegram manifest has it (c05aa5e6), on mod-x.
|
|
const telegramClaim = `[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]`
|
|
verified := func(m map[string]any) map[string]any {
|
|
m["seat-traffic"] = map[string]any{"kinds": []any{map[string]any{"seat": "channel", "kind": "telegram", "module": "mod-x", "node": "a",
|
|
"capabilities": []any{"deliver", "verified-sender"}}}}
|
|
return m
|
|
}
|
|
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
|
|
// claims, when set, are mod-x's claims in its manifest for the row.
|
|
claims 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, "", ""},
|
|
// The lead's decision in review round 1 of the fix of issue 1945: only a seat left with no holder, or a
|
|
// capability its holder still asks for (verified-sender, dropped by a root-free word that failed), holds a
|
|
// membership back; a dropped tool or field is published.
|
|
{"a change that drops a tool is 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"})
|
|
m["mesh"] = []any{"10.0.0.1"}
|
|
return m
|
|
}, true, "", ""},
|
|
{"a change that drops verified-sender its holder still asks for is not published, and named", verified, false, "channel telegram verified-sender of a/mod-x", telegramClaim},
|
|
{"a change that drops verified-sender its holder no longer asks for is published", verified, true, "published:published", `[{"name":"channel","scope":"mesh","kind":"telegram"}]`},
|
|
{"a change that takes the last holder from a seat a placed module claims is not published, the seat named", func(m map[string]any) map[string]any {
|
|
m["seats"] = []any{map[string]any{"seat": brokerName, "verb": brokerVerb, "subject": "mesh.seat." + brokerName + ".tool." + brokerVerb}}
|
|
return m
|
|
}, false, "the last holder of " + brokerName, ""},
|
|
// 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 seat no placed module claims dropped is published", 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
|
|
}, true, "mesh-delivery mark-ready", ""},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
b := fixture()
|
|
if row.claims != "" {
|
|
b.manifestOf("mod-x", strings.Replace(xManifest, `"own-secrets"`, `"claims":`+row.claims+`,"own-secrets"`, 1))
|
|
}
|
|
b.loaded = shadowOf(t, b)
|
|
// The last membership is the part's own state's, never the assignments stream's: the stream goes at the lease
|
|
// write (research 072 row 33), and a part reading its last there would publish nothing after it.
|
|
if row.last != nil {
|
|
raw, _ := json.Marshal(row.last(composed(b)))
|
|
b.own[grants.StateMemberships] = map[string][]byte{key: raw}
|
|
}
|
|
res := partOn(b).Pass()
|
|
if got := slices.Contains(res.Published, key); got != row.published {
|
|
t.Fatalf("published %v, not published %v", res.Published, res.NotPublished)
|
|
}
|
|
// The state holds the bare membership the assignments stream carries, through mesh-sdk's codec, and
|
|
// only one published (issue 774); what became of it is the kept pass's, which grants_membership reads.
|
|
state, _, _, _ := b.ownGet(grants.StateMemberships, key)
|
|
ms, _ := partOn(b).MembershipOf("a", "mod-x")
|
|
pass := fmt.Sprint(ms.(map[string]any)["last_pass"])
|
|
if row.published {
|
|
if row.names == "" && row.last != nil && !strings.Contains(string(state), "10.0.0.1") {
|
|
t.Errorf("the private network of the last membership is not carried: %s", state)
|
|
}
|
|
// Written to the state alone, never said on its subject (1126/A1).
|
|
got, err := membership.DecodeGrantsMembership(state)
|
|
if err != nil || got.Node != "a" || got.Module != "mod-x" || bytes.Contains(state, []byte(`"membership"`)) || len(b.stream) != 0 {
|
|
t.Errorf("the state is not the bare membership published, or its subject said (%v, %d said): %s", err, len(b.stream), state)
|
|
}
|
|
if !strings.Contains(pass, row.names) {
|
|
t.Errorf("the pass does not name %q: %s", row.names, pass)
|
|
}
|
|
return
|
|
}
|
|
if was, _ := json.Marshal(row.last(composed(b))); !bytes.Equal(state, was) {
|
|
t.Errorf("a membership held back reached the state: %s", state)
|
|
}
|
|
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)
|
|
}
|
|
if !strings.Contains(pass, row.names) {
|
|
t.Errorf("grants_membership does not name what would be lost: %v", ms)
|
|
}
|
|
})
|
|
}
|
|
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)
|
|
}
|
|
if b.createdAge["LOG_mod-x_history"] != 30*24*time.Hour { // a log's max-age-days is its stream's MaxAge (mesh-sdk 2e5cc4f)
|
|
t.Errorf("the log was created with max age %v, want 30 days", b.createdAge["LOG_mod-x_history"])
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
})
|
|
|
|
// 774 (ADR 0563 §2, §3): the node-engine's manifest (mesh-host) is placed on every node though no node is assigned it,
|
|
// so its declared state acts is created as a placed module's, and composed for the user list's create right.
|
|
t.Run("assertions: the node-engine's declared state is created though no node is assigned it", func(t *testing.T) {
|
|
b := fixture()
|
|
b.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","state":[{"name":"acts"}]}`)
|
|
b.loaded = shadowOf(t, b)
|
|
p := partOn(b)
|
|
p.Create = b
|
|
p.Compose()
|
|
if !slices.Contains(b.creates, "KV_"+buscompose.EngineActsBucket) {
|
|
t.Fatalf("the node-engine's acts were not created: %v", b.creates)
|
|
}
|
|
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
|
|
if recs.Engine == nil || len(recs.Engine.State) != 1 || recs.Engine.State[0].Bucket() != buscompose.EngineActsBucket {
|
|
t.Fatalf("the records carry the node-engine as %+v", recs.Engine)
|
|
}
|
|
for node, mods := range recs.Assigned {
|
|
for _, d := range mods {
|
|
if d.Module == buscompose.EngineModule {
|
|
t.Errorf("the node-engine is a module of %s, which would give it a module's account", node)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
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.own[grants.StateMemberships] = map[string][]byte{}
|
|
for _, 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)
|
|
seat, verb := brokerSeat()
|
|
m["seats"] = []any{map[string]any{"seat": seat, "verb": verb, "subject": "mesh.seat." + seat + ".tool." + verb}}
|
|
raw, _ = json.Marshal(m)
|
|
b.own[grants.StateMemberships][key] = raw
|
|
}
|
|
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 seat, _ := brokerSeat(); err != nil || h["count"] != 2 || !strings.Contains(fmt.Sprint(h["held"]), "the last holder of "+seat) {
|
|
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.ReadPlacement)
|
|
if r := f.Refused(); r[grants.ReadModules] == "" {
|
|
waitFor(t, func() bool { return f.Refused()[grants.ReadModules] != "" })
|
|
}
|
|
w.change(grants.ReadPlacement, "placement.a")
|
|
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)
|
|
}
|
|
})
|
|
|
|
// 774/L3a (novox/hq ADR 0547 §5, ADR 0561 §2): the part writes grants.<module>.<fp> in shadow, once each and only for a
|
|
// module whose accounts the vault holds a hash for, says grant-written for each, deletes one only when a module has no
|
|
// account with a hash and every read was made, and the tool says every difference from the controller's.
|
|
t.Run("grants: written once each, only for hashed accounts, deleted only when read whole, and once the loaded list carries them", func(t *testing.T) {
|
|
b := fixture()
|
|
b.loaded = shadowOf(t, b)
|
|
p := partOn(b)
|
|
fp := "src2:" + strings.Repeat("ab", 32)
|
|
key := func(module string) string { k, _ := delivery.PartGrantKey(module, fp); return k }
|
|
hash := func(user string) {
|
|
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1})
|
|
}
|
|
held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) }
|
|
hash("a.mod-x")
|
|
hash("a.nats")
|
|
res := p.Compose()
|
|
if res.Grants == nil || !slices.Equal(held(), []string{key("mod-x"), key("nats")}) || len(res.Grants.Written) != 2 {
|
|
t.Fatalf("grants of the hashed modules: held %v, %+v", held(), res.Grants)
|
|
}
|
|
if g, err := delivery.DecodeControllerGrant(b.own[grants.StateGrants][key("mod-x")]); err != nil || !slices.Equal(g.Accounts, []string{"a.mod-x"}) {
|
|
t.Errorf("the grant of mod-x is %+v, %v", g, err)
|
|
}
|
|
said := func() int {
|
|
n := 0
|
|
for _, e := range b.events {
|
|
if e == grants.EventGrantWritten {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
if said() != 2 {
|
|
t.Errorf("grant-written said %d times for two grants: %v", said(), b.events)
|
|
}
|
|
if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 {
|
|
t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said())
|
|
}
|
|
// The tool answers what the part wrote, and reads nothing of the controller's.
|
|
got, err := p.Grants("")
|
|
gm, _ := got.(map[string]any)
|
|
if _, ok := gm["controller"]; err != nil || gm["count"] != 2 || ok {
|
|
t.Errorf("grants = %+v, %v", got, err)
|
|
}
|
|
// A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted.
|
|
hash("b.mod-y")
|
|
if res := p.Compose(); !slices.Equal(res.Grants.Written, []string{key("mod-y")}) {
|
|
t.Errorf("mod-y was granted %v when its account got a hash", res.Grants.Written)
|
|
}
|
|
b.refused[grants.ReadAccounts] = true
|
|
if res := p.Compose(); res.Grants == nil || len(res.Grants.Deleted)+len(res.Grants.Written) != 0 || len(res.Grants.Kept) != 1 || len(held()) != 3 {
|
|
t.Errorf("a pass that could not read the hashes changed grants: %+v, held %v", res.Grants, held())
|
|
}
|
|
delete(b.refused, grants.ReadAccounts)
|
|
delete(b.states[grants.ReadAccounts], "b.mod-y")
|
|
if res := p.Compose(); !slices.Equal(res.Grants.Deleted, []string{key("mod-y")}) || len(held()) != 2 {
|
|
t.Errorf("a module with no hashed account kept its grant: %+v, held %v", res.Grants, held())
|
|
}
|
|
|
|
// Issue 774: after take-over a grant is written at once when the list the bus loaded carries its accounts' permissions,
|
|
// held list or not, and waits only when it adds one. (1) mod-x moves to a new fingerprint with the same permissions
|
|
// while the list is held (users of the loaded list have no hash): written.
|
|
busKey, err := seal.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loads := func(source, digest string) {
|
|
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
|
UserList: &delivery.UserListWritten{Source: source, Revision: 8, Wanted: digest, Digest: digest, Users: 6, Key: busKey.PublicKey,
|
|
Follows: delivery.FollowGrants}})
|
|
}
|
|
loads("a.nats.src2", "sha256:aa")
|
|
p.Objects = &fakeObjects{}
|
|
moves := func(module, manifest, fp string) string {
|
|
digest := delivery.ManifestDigest([]byte(manifest))
|
|
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: manifest})
|
|
b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
|
k, _ := delivery.PartGrantKey(module, fp)
|
|
return k
|
|
}
|
|
written := func(k string) bool { _, ok := b.own[grants.StateGrants][k]; return ok }
|
|
keyX2 := moves("mod-x", xManifest, "src2:"+strings.Repeat("cd", 32))
|
|
res = p.Compose()
|
|
if !written(keyX2) || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "held") || len(res.Grants.Waits) != 0 {
|
|
t.Fatalf("a new fingerprint with the same permissions under a held list: sealed %+v, grants %+v", res.Sealed, res.Grants)
|
|
}
|
|
// Every user hashed: the list is sealed, and the part keeps its permissions. The bus loads it, and its tools go quiet.
|
|
for _, u := range shadowOf(t, b) {
|
|
hash(u.User)
|
|
}
|
|
p.Compose()
|
|
sealed, _, _ := p.Sealed()
|
|
if sealed.Digest == "" {
|
|
t.Fatal("no list sealed once every user has a hash")
|
|
}
|
|
loads(delivery.GrantsUserListSource, sealed.Digest)
|
|
b.refuseUsers = true
|
|
// (2) mod-y's trunk adds a state, so its account adds permissions: it waits, judged from the part's own record of the
|
|
// loaded list; mod-x at another fingerprint with the same permissions is written.
|
|
fp3 := "src2:" + strings.Repeat("ef", 32)
|
|
keyY3 := moves("mod-y", strings.Replace(yManifest, `"reads"`, `"state":[{"name":"fresh"}],"reads"`, 1), fp3)
|
|
keyX3 := moves("mod-x", xManifest, fp3)
|
|
res = p.Compose()
|
|
if !written(keyX3) || written(keyY3) || len(res.Grants.Waits) != 1 ||
|
|
!strings.HasPrefix(res.Grants.Waits[0], keyY3+": waits for the bus to load a list carrying what it adds: b.mod-y") {
|
|
t.Fatalf("an added permission against the loaded list: %+v", res.Grants)
|
|
}
|
|
// A loaded list the part keeps no permissions of: every grant naming an account waits, said.
|
|
loads(delivery.GrantsUserListSource, "sha256:bb")
|
|
keyX4 := moves("mod-x", xManifest, "src2:"+strings.Repeat("12", 32))
|
|
if res := p.Compose(); written(keyX4) || len(res.Grants.Waits) != 2 || !strings.Contains(res.Grants.Waits[0], "unknown") {
|
|
t.Fatalf("an unknown loaded list let a grant through: %+v", res.Grants)
|
|
}
|
|
// The bus loads a list carrying mod-y's state (its tools answer it): both written, nothing waits.
|
|
listRecs, _ := grants.ListRecords(grants.ReadInputs(b, &grants.Manifests{}))
|
|
now, err := grants.ComposeUsers(listRecs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.refuseUsers, b.loaded = false, now
|
|
if res := p.Compose(); len(res.Grants.Waits) != 0 || !written(keyY3) || !written(keyX4) {
|
|
t.Errorf("once the loaded list carries them: %+v", res.Grants)
|
|
}
|
|
})
|
|
|
|
// Issue 1126 (C5): a module whose source commit has not moved since a build of the old form, with a done key of the
|
|
// new form at that commit (link2pay, snake, f1-circuits, txt-game), is granted at the indexer's fingerprint, never
|
|
// judged by its commit; and a placed module left without a grant is said, never passed over: in the pass, and as the
|
|
// condition mesh-grants.grant-missing once the part's grants are the ones read, cleared when it has one.
|
|
t.Run("grants: at the indexer's fingerprint whatever the commit, and a placed module without one is said", func(t *testing.T) {
|
|
b := fixture()
|
|
b.loaded = shadowOf(t, b)
|
|
p := partOn(b)
|
|
var said []asks.Condition
|
|
p.Say = func(c asks.Condition) error {
|
|
if c.Key == grants.GrantMissingCondition {
|
|
said = append(said, c)
|
|
}
|
|
return nil
|
|
}
|
|
// With no lease key (1126/A1).
|
|
// The private network the mesh provides is placed with no fingerprint by construction: never a missing grant.
|
|
b.assign("b", "mesh-wireguard")
|
|
fp := "src2:d4afe7a1abab5ccdbef6d8629b2245006a2aec60290f430f7b4756e2930ed60c"
|
|
b.put(grants.ReadModules, "mod-x", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(xManifest)),
|
|
Repository: "novox/Link2Pay", Commit: "5d20e8bfbc953c51e8ebe3c671fe883df9c36e15"})
|
|
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, None2: "its builder's version is not read yet",
|
|
Manifest: delivery.ManifestDigest([]byte(yManifest)), Repository: "novox/y", Commit: "c"})
|
|
for _, u := range []string{"a.mod-x", "a.nats", "b.mod-y"} {
|
|
b.put(grants.ReadAccounts, u, map[string]any{"user": u, "hash": "$2a$10$x", "generation": 1})
|
|
}
|
|
res := p.Compose()
|
|
key, _ := delivery.PartGrantKey("mod-x", fp)
|
|
if _, held := b.own[grants.StateGrants][key]; !held {
|
|
t.Errorf("mod-x has no grant at the indexer's fingerprint %s: %+v", fp, res.Grants)
|
|
}
|
|
if res.Grants == nil || len(res.Grants.Missing) != 1 || !strings.HasPrefix(res.Grants.Missing[0],
|
|
"mod-y: no fingerprint of the new form: its builder's version is not read yet") {
|
|
t.Fatalf("missing = %+v", res.Grants)
|
|
}
|
|
if len(said) != 1 || said[0].State != asks.StateRaised || !strings.Contains(said[0].Explanation, "mod-y") {
|
|
t.Fatalf("said %+v", said)
|
|
}
|
|
if p.Compose(); len(said) != 1 {
|
|
t.Errorf("the same missing grant was said again: %+v", said)
|
|
}
|
|
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(yManifest)),
|
|
Repository: "novox/y", Commit: "c"})
|
|
if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != asks.StateCleared {
|
|
t.Errorf("once mod-y has a grant: missing %v, said %+v", res.Grants.Missing, said)
|
|
}
|
|
})
|
|
|
|
// ADR 0571 §13's test for this repository (issue 774, 774/S24/AC1), replacing the guard of ADR 0549 §6 and its list
|
|
// ../assignments-readers: at phase write no module.json here depends on mesh-controller beyond the lease and the three
|
|
// kept to the write (mesh-sdk catalogue.ManifestRefusals, the judge grants_lease_check runs).
|
|
t.Run("no module of this repository depends on the controller beyond what the lease write keeps", func(t *testing.T) {
|
|
paths, _ := filepath.Glob("../*/module.json")
|
|
manifests := map[string]catalogue.Manifest{}
|
|
for _, path := range paths {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := catalogue.Decode(raw)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
manifests[m.Module] = m
|
|
}
|
|
if len(manifests) < 8 {
|
|
t.Fatalf("read %d manifests, want every module of the repository", len(manifests))
|
|
}
|
|
for _, why := range catalogue.ManifestRefusals(manifests, catalogue.PhaseWrite) {
|
|
t.Error(why)
|
|
}
|
|
})
|
|
|
|
// 774/S29/AC1 (ADR 0549 §5, ADR 0571 §10): the memberships composed from the placement alone, with no read of the
|
|
// controller, are the ones the union of placement and assignments gave on the same input: the fixture's (two nodes,
|
|
// the bus and mod-x on a, mod-y on b, as placed on the live mesh's own shape of nats beside its modules), whose
|
|
// assignments named the same modules. Neither the controller's assignments nor its condition events are read.
|
|
t.Run("placement alone places, with the memberships the union gave", func(t *testing.T) {
|
|
b := fixture()
|
|
p := partOn(b)
|
|
p.Compose()
|
|
want := map[string]string{"a.nats": "", "a.mod-x": "", "b.mod-y": ""}
|
|
for key := range want {
|
|
raw, _, found, _ := b.ownGet(grants.StateMemberships, key)
|
|
if !found {
|
|
t.Fatalf("no membership of %s from the placement", key)
|
|
}
|
|
want[key] = string(raw)
|
|
}
|
|
if got := slices.Sorted(maps.Keys(b.own[grants.StateMemberships])); !slices.Equal(got, []string{"a.mod-x", "a.nats", "b.mod-y"}) {
|
|
t.Errorf("memberships %v", got)
|
|
}
|
|
for _, r := range grants.Reads {
|
|
if strings.HasPrefix(r, "mesh-controller.") {
|
|
t.Errorf("the part reads %s", r)
|
|
}
|
|
}
|
|
for _, c := range grants.Consumes {
|
|
if strings.HasPrefix(c, "mesh-controller.") {
|
|
t.Errorf("the part consumes %s", c)
|
|
}
|
|
}
|
|
for _, c := range b.calls {
|
|
if strings.Contains(c, "mesh-controller") {
|
|
t.Errorf("the part called %s", c)
|
|
}
|
|
}
|
|
})
|
|
|
|
// 774/AC4 (novox/hq ADR 0561 §2): every account the vault mints that the records compose is written to the state
|
|
// accounts once, with the bus's address and a module's claims, read by the vault alone; a key is deleted when placement
|
|
// no longer names its user, and never in a pass whose reads failed.
|
|
t.Run("wanted accounts: written once each, read by the vault alone, retired only when placement drops them", func(t *testing.T) {
|
|
b := fixture()
|
|
b.loaded = shadowOf(t, b)
|
|
p := partOn(b)
|
|
p.Bus = grants.BusAddress{URL: "nats://a.mesh:4222", Fingerprint: "ab"}
|
|
p.Compose()
|
|
p.Compose()
|
|
wrote := 0
|
|
for _, w := range b.writes {
|
|
if w == grants.StateAccounts {
|
|
wrote++
|
|
}
|
|
}
|
|
held := slices.Sorted(maps.Keys(b.own[grants.StateAccounts]))
|
|
if !slices.Equal(held, []string{"a.mod-x", "a.nats", "b.mod-y"}) || wrote != 3 {
|
|
t.Fatalf("accounts %v written %d times in two passes, want the three modules' once each", held, wrote)
|
|
}
|
|
nats, err := buscompose.ReadWantedAccount("a.nats", b.own[grants.StateAccounts]["a.nats"])
|
|
if err != nil || nats.URL != "nats://a.mesh:4222" || nats.Fingerprint != "ab" || len(nats.Claims) != 1 ||
|
|
nats.Claims[0].Seat != catalogue.BrokerSeat || nats.At.IsZero() {
|
|
t.Errorf("a.nats is wanted as %+v (%v)", nats, err)
|
|
}
|
|
var readers []string
|
|
for _, s := range grants.Announced() {
|
|
if s.Name == grants.StateAccounts {
|
|
if s.Readable {
|
|
t.Error("the wanted accounts are announced readable by anyone")
|
|
}
|
|
readers = s.Readers
|
|
}
|
|
}
|
|
if !slices.Equal(readers, []string{"mesh-vault"}) || buscompose.ReaderMay("mesh-delivery-view", buscompose.GrantsAccounts) {
|
|
t.Errorf("the wanted accounts are read by %v", readers)
|
|
}
|
|
// A read that fails retires nothing; once read, the user placement dropped is retired.
|
|
b.unassign("b", "mod-y")
|
|
b.refused[grants.ReadResolved] = true
|
|
if res := p.Compose(); res.Accounts == nil || len(res.Accounts.Deleted) != 0 || b.own[grants.StateAccounts]["b.mod-y"] == nil {
|
|
t.Fatalf("a pass whose read failed deleted %+v", res.Accounts)
|
|
}
|
|
delete(b.refused, grants.ReadResolved)
|
|
if res := p.Compose(); res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-y"}) {
|
|
t.Fatalf("the user placement dropped is not retired: %+v", res.Accounts)
|
|
}
|
|
// The tool reads them, one and all, each with when it was written.
|
|
one, err := p.Accounts("a.nats")
|
|
all, err2 := p.Accounts("")
|
|
if err != nil || err2 != nil || one.(map[string]any)["count"] != 1 || all.(map[string]any)["count"] != 2 ||
|
|
one.(map[string]any)["accounts"].([]buscompose.WantedAccount)[0].At.IsZero() {
|
|
t.Errorf("the tool reads %v and %v (%v %v)", one, all, err, err2)
|
|
}
|
|
if _, err := p.Accounts("b.mod-y"); err == nil {
|
|
t.Error("the tool reads an account no longer wanted")
|
|
}
|
|
p.Pass()
|
|
if all, _ := p.Accounts(""); all.(map[string]any)["consumers"] == nil {
|
|
t.Errorf("the tool does not say the account consumers in shadow: %v", all)
|
|
}
|
|
// With no bus address no account is written, and the pass says why.
|
|
bare := partOn(fixture())
|
|
if res := bare.Compose(); res.Accounts != nil || !strings.Contains(strings.Join(res.Errors, " "), "no bus address") {
|
|
t.Errorf("a part with no bus address wrote %+v: %v", res.Accounts, res.Errors)
|
|
}
|
|
})
|
|
|
|
// 774/AC5 (novox/hq ADR 0561 §1): the vault's account-issued wakes a pass, so the next list carries the new user's hash
|
|
// within one pass; an event of another kind does not.
|
|
// 774/S52/AC1 (novox/hq ADR 0587 §4, §5): a key of the seat mesh-enrolment at live, with the vault's hash, composes
|
|
// enrol.<node>, which from the lease write may publish the seat's redeem in its own name and subscribe its own inbox,
|
|
// nothing else; a key at asked composes none; when the key ends the user is gone.
|
|
t.Run("an enrolment key at live composes its enrolment user, and its end takes it out", func(t *testing.T) {
|
|
b := fixture()
|
|
b.manifestOf("mesh-enrolment", enrolmentManifest)
|
|
b.assign("a", "mesh-enrolment")
|
|
issued := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC)
|
|
for node, stage := range map[string]string{"lab": delivery.EnrolmentLive, "pi": delivery.EnrolmentAsked} {
|
|
raw, err := delivery.EncodeEnrolment(delivery.Enrolment{Node: node, Address: "10.0.0.7", Why: "joins",
|
|
Caller: "a.mesh-enrolment", Stage: stage, Issued: issued, Expires: issued.Add(time.Hour),
|
|
OverlayKey: base64.StdEncoding.EncodeToString(make([]byte, 32))})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.put(grants.ReadEnrolments, delivery.EnrolmentNodeKey(node), json.RawMessage(raw))
|
|
}
|
|
b.put(grants.ReadAccounts, "enrol.lab", map[string]any{"user": "enrol.lab", "hash": "$2a$10$x", "generation": 1})
|
|
in := grants.ReadInputs(b, &grants.Manifests{})
|
|
if !slices.Equal(in.Enrolling, []string{"lab"}) || gapNamed(in.Gaps, "enrolment tokens") {
|
|
t.Fatalf("enrolling %v, gaps %v", in.Enrolling, in.Gaps)
|
|
}
|
|
users := shadowOf(t, b)
|
|
var lab *grants.User
|
|
for i := range users {
|
|
if users[i].User == "enrol.lab" {
|
|
lab = &users[i]
|
|
}
|
|
if users[i].User == "enrol.pi" {
|
|
t.Error("a key at asked composed enrol.pi")
|
|
}
|
|
}
|
|
if lab == nil || !slices.Equal(lab.Publish, []string{"mesh.seat.mesh-enrolment.call.redeem.enrol~lab"}) ||
|
|
!slices.Equal(lab.Subscribe, []string{"_INBOX.enrol.lab.>"}) || lab.AllowResponses {
|
|
t.Fatalf("enrol.lab: %+v", lab)
|
|
}
|
|
if c := grants.CredentialsOf(users, nil, in.Hashes); !slices.Contains(c.Adds, "enrol.lab") {
|
|
t.Errorf("enrol.lab's hash is not carried: %+v", c)
|
|
}
|
|
b.mu.Lock()
|
|
delete(b.states[grants.ReadEnrolments], delivery.EnrolmentNodeKey("lab"))
|
|
b.mu.Unlock()
|
|
if slices.Contains(names(shadowOf(t, b)), "enrol.lab") {
|
|
t.Error("enrol.lab stays after its key ended")
|
|
}
|
|
if !slices.Contains(grants.Followed, grants.ReadEnrolments) {
|
|
t.Error("a key's change composes nothing again")
|
|
}
|
|
})
|
|
// 774/S53 (novox/hq ADR 0583 §1, §2): while node-logins says b rotates to node.b--b, with its hash, both logins are in
|
|
// the list and b's machine stays placed under either; after rotation-done (b on node.b--b, nothing rotating) the old
|
|
// login is not (AC1). Each node's engine may read its own node-logins key alone (AC2).
|
|
t.Run("a node rotating is composed under both its logins, then the new one alone", func(t *testing.T) {
|
|
b := fixture()
|
|
b.manifestOf("mesh-vault", vaultManifest)
|
|
b.assign("a", "mesh-vault")
|
|
since := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC)
|
|
login := func(l delivery.NodeLogin) {
|
|
raw, err := delivery.EncodeNodeLogin(l)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.put(grants.ReadNodeLogins, l.Node, json.RawMessage(raw))
|
|
}
|
|
login(delivery.NodeLogin{Node: "b", Login: "node.b", Rotating: "node.b--b", Stage: delivery.RotationBegun, Since: since})
|
|
b.put(grants.ReadAccounts, "node.b--b", map[string]any{"user": "node.b--b", "hash": "$2a$10$x", "generation": 1})
|
|
users := shadowOf(t, b)
|
|
in := grants.ReadInputs(b, &grants.Manifests{})
|
|
if got := names(users); !slices.Contains(got, "node.b") || !slices.Contains(got, "node.b--b") || !slices.Contains(got, "node.a") {
|
|
t.Fatalf("the node users are %v", got)
|
|
}
|
|
if c := grants.CredentialsOf(users, nil, in.Hashes); !slices.Contains(c.Adds, "node.b--b") {
|
|
t.Errorf("node.b--b's hash is not carried: %+v", c)
|
|
}
|
|
if placed := grants.Placed(in); !placed("node.b--b") || placed("node.c--b") || !in.Rotating()["node.b--b"] {
|
|
t.Error("the second login is not judged its node's, or not rotating")
|
|
}
|
|
get := func(key string) string {
|
|
return "$JS.API.DIRECT.GET.KV_" + delivery.NodeLoginsBucket + ".$KV." + delivery.NodeLoginsBucket + "." + key
|
|
}
|
|
for _, u := range users {
|
|
if !strings.HasPrefix(u.User, "node.") {
|
|
continue
|
|
}
|
|
node, _ := delivery.LoginNode(u.User)
|
|
for _, of := range []string{"a", "b"} {
|
|
if slices.Contains(u.Publish, get(of)) != (of == node) {
|
|
t.Errorf("%s reads %s's node-logins key: %v", u.User, of, of != node)
|
|
}
|
|
}
|
|
}
|
|
login(delivery.NodeLogin{Node: "b", Login: "node.b--b", Since: since.Add(time.Hour)})
|
|
if got := names(shadowOf(t, b)); slices.Contains(got, "node.b") || !slices.Contains(got, "node.b--b") {
|
|
t.Errorf("after rotation-done the node users are %v", got)
|
|
}
|
|
})
|
|
t.Run("the vault's account-issued composes the list again", func(t *testing.T) {
|
|
b := fixture()
|
|
var handle func(string, []byte)
|
|
p := partOn(b)
|
|
f := &grants.Follower{Part: p, Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(string, ...any) {},
|
|
Hear: func(h func(string, []byte)) error { handle = h; return nil }}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() { f.Run(ctx); close(done) }()
|
|
defer func() { cancel(); <-done }()
|
|
waitFor(t, func() bool { return b.count(grants.CallUsers) > 0 && handle != nil })
|
|
held := func() bool {
|
|
for _, c := range p.Last().UserList.Credentials.Users {
|
|
if c.User == "a.mod-x" {
|
|
return c.HashHeld
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if held() {
|
|
t.Fatal("a.mod-x has a hash before the vault made one")
|
|
}
|
|
b.mu.Lock()
|
|
b.states[grants.ReadAccounts] = map[string][]byte{"a.mod-x": []byte(`{"user":"a.mod-x","hash":"$2a$10$x","generation":1}`)}
|
|
b.mu.Unlock()
|
|
handle(buscompose.AccountIssuedSubject, []byte(`{"user":"a.mod-x","kind":"module","node":"a","module":"mod-x","generation":1}`))
|
|
waitFor(t, held)
|
|
if !slices.Contains(grants.Consumes, grants.EventAccountIssued) || !grants.IsAccountIssued(grants.EventAccountIssued) ||
|
|
grants.IsAccountIssued("mesh.mod.mesh-vault.event.secret-changed") {
|
|
t.Errorf("the part hears %v", grants.Consumes)
|
|
}
|
|
})
|
|
|
|
// 774/AC6 (novox/hq ADR 0561 §6), 1126/A1/AC1: the durable consumers an account needs, with the composed
|
|
// configuration, are created where the bus lacks them with no lease key at all (the controller ended by its
|
|
// unassignment, never by the lease write); one the bus holds is never changed.
|
|
t.Run("account consumers: created where the bus lacks them, with no lease key", func(t *testing.T) {
|
|
const cManifest = `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["mod-y.done"]}`
|
|
b := fixture()
|
|
b.manifestOf("mod-c", cManifest)
|
|
b.assign("b", "mod-c")
|
|
b.loaded = shadowOf(t, b)
|
|
p := partOn(b)
|
|
p.Consumers = b
|
|
b.consumers = map[string][]map[string]any{"EVENTS": {{"name": "b_other", "filters": []string{"x"}, "max_deliver": 5}}}
|
|
res := p.Compose()
|
|
if res.Consumers == nil || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) {
|
|
t.Fatalf("with no lease key: %+v", res.Consumers)
|
|
}
|
|
var made map[string]any
|
|
for _, c := range b.consumers["EVENTS"] {
|
|
if c["name"] == "b_mod-c" {
|
|
made = c
|
|
}
|
|
}
|
|
if made == nil || made["max_deliver"] != 5 || !slices.Contains(made["filters"].([]string), "mesh.mod.mod-y.event.done") {
|
|
t.Errorf("b_mod-c was made as %v, not as the controller makes it", made)
|
|
}
|
|
res = p.Compose()
|
|
if len(res.Consumers.Created) != 0 || len(res.Consumers.Missing) != 0 || len(b.consumerCreates) != 1 {
|
|
t.Errorf("a consumer the bus holds was asked again: %+v, %v", res.Consumers, b.consumerCreates)
|
|
}
|
|
// A held consumer that differs is never changed by a pass: it is raised as a condition naming it, and brought to
|
|
// the composed configuration only by hand (grants_consumer_match), with no lease key too.
|
|
var said []asks.Condition
|
|
p.Say = func(c asks.Condition) error {
|
|
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows
|
|
said = append(said, c)
|
|
}
|
|
return nil
|
|
}
|
|
made["max_deliver"] = 9
|
|
res = p.Compose()
|
|
if len(res.Consumers.Differ) != 1 || !strings.Contains(res.Consumers.Differ[0], "EVENTS.b_mod-c: max deliver 9") ||
|
|
len(said) != 1 || said[0].Key != grants.ConsumerDiffersCondition || said[0].State != asks.StateRaised ||
|
|
!strings.Contains(said[0].Explanation, "EVENTS.b_mod-c") {
|
|
t.Fatalf("a differing consumer: %+v, said %+v", res.Consumers, said)
|
|
}
|
|
if _, err := p.MatchConsumer("EVENTS", "b_nothing"); err == nil {
|
|
t.Error("a consumer the records do not compose was asked to match")
|
|
}
|
|
if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res = p.Compose()
|
|
if !slices.Contains(b.consumerCreates, "match EVENTS.b_mod-c") || made["max_deliver"] != 5 || len(res.Consumers.Matched) != 1 {
|
|
t.Errorf("the ask to match: %+v, %v", res.Consumers, b.consumerCreates)
|
|
}
|
|
res = p.Compose()
|
|
if len(res.Consumers.Differ) != 0 || said[len(said)-1].State != asks.StateCleared {
|
|
t.Errorf("once matched: %+v, said %+v", res.Consumers, said)
|
|
}
|
|
})
|
|
|
|
// 774/AC13 (novox/hq ADR 0561 §7): after take-over a list in which more than one loaded user's hash changes, with no
|
|
// rotation by the vault naming each, is held: the list in force stays, user-list-held is raised naming those users. One
|
|
// user's change, or changes the vault's rotation names, are written.
|
|
t.Run("a list changing several loaded users' hashes is held unless the vault's rotation names each", func(t *testing.T) {
|
|
b := fixture()
|
|
b.loaded = shadowOf(t, b)
|
|
hash := func(user, h string, generation int, rotating bool) {
|
|
v := map[string]any{"user": user, "hash": h, "generation": generation, "fingerprint": fmt.Sprintf("sha256:%d", generation)}
|
|
if rotating {
|
|
v["rotating"] = map[string]any{"login": user + "--b", "generation": generation, "revision": 7}
|
|
}
|
|
b.put(grants.ReadAccounts, user, v)
|
|
}
|
|
for _, u := range shadowOf(t, b) {
|
|
hash(u.User, "$2a$10$first", 1, false)
|
|
}
|
|
busKey, err := seal.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}})
|
|
p := partOn(b)
|
|
p.Objects = &fakeObjects{}
|
|
var said []asks.Condition
|
|
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
|
p.Compose()
|
|
inForce, _, _ := p.Sealed()
|
|
if inForce.Digest == "" {
|
|
t.Fatal("no list written after take-over")
|
|
}
|
|
// One user's hash changes: written.
|
|
hash("a.mod-x", "$2a$10$second", 2, false)
|
|
p.Compose()
|
|
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest {
|
|
t.Fatal("a list changing one loaded user's hash was not written")
|
|
}
|
|
inForce, _, _ = p.Sealed()
|
|
// Two change at once, no rotation names them: held, said naming both.
|
|
hash("a.mod-x", "$2a$10$third", 3, false)
|
|
hash("a.nats", "$2a$10$third", 2, false)
|
|
said = nil
|
|
res := p.Compose()
|
|
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x, a.nats") {
|
|
t.Fatalf("a list changing two loaded users' hashes: %+v", res.Sealed)
|
|
}
|
|
if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "a.mod-x, a.nats") {
|
|
t.Errorf("the hold was said as %+v", said)
|
|
}
|
|
// A person accepts exactly the named users' changes of the held list, with why: refused for another set, with no
|
|
// why, or once the list changed since; accepted, the next pass writes it once, said as a hand act.
|
|
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x"}, "rotated by hand"); err == nil || !strings.Contains(err.Error(), "a.mod-x, a.nats") {
|
|
t.Errorf("an acceptance of another set than the held one: %v", err)
|
|
}
|
|
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, ""); err == nil {
|
|
t.Error("an acceptance with no why")
|
|
}
|
|
if _, err := p.AcceptHashes("person.jochen", []string{"a.nats", "a.mod-x"}, "the vault's store was restored"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The list changed since the acceptance (a.mod-x's hash again): the acceptance is bound to the held list, refused.
|
|
hash("a.mod-x", "$2a$10$fourth", 4, false)
|
|
res = p.Compose()
|
|
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "held") {
|
|
t.Fatalf("an acceptance of a list that changed since released it: %+v", res.Sealed)
|
|
}
|
|
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, "the vault's store was restored"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.events = nil
|
|
p.Compose()
|
|
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared ||
|
|
!slices.Contains(b.events, grants.EventHashesAccepted) {
|
|
t.Fatalf("an accepted hold was not released once and said: %+v, events %v", said, b.events)
|
|
}
|
|
inForce, _, _ = p.Sealed()
|
|
// The vault's rotation names each: written, the warning cleared.
|
|
hash("a.mod-x", "$2a$10$fifth", 5, true)
|
|
hash("a.nats", "$2a$10$fifth", 3, true)
|
|
p.Compose()
|
|
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared {
|
|
t.Errorf("changes the vault's rotation names were held: %+v", said)
|
|
}
|
|
})
|
|
|
|
// Sweep 2026-10-30 item 5: the permissions of the sealed lists were one value holding every user of two lists, which
|
|
// outgrew the bucket's largest value, so they were never kept. One key per user: no value grows with the list.
|
|
t.Run("sealed grants: one key per user, so no value grows with the list, and only the last two lists kept", func(t *testing.T) {
|
|
b := fixture()
|
|
for _, u := range shadowOf(t, b) {
|
|
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
|
|
}
|
|
largest := 0
|
|
for _, u := range shadowOf(t, b) {
|
|
raw, _ := json.Marshal(u)
|
|
largest = max(largest, len(raw))
|
|
}
|
|
b.maxValue = largest + 256 // one user's permissions fit, every user's of a list do not
|
|
busKey, err := seal.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}})
|
|
p := partOn(b)
|
|
p.Objects = &fakeObjects{}
|
|
var digests []string
|
|
for gen := 1; gen <= 3; gen++ {
|
|
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": fmt.Sprintf("$2a$10$gen%d", gen), "generation": gen, "fingerprint": fmt.Sprintf("sha256:%d", gen)})
|
|
res := p.Compose()
|
|
for _, e := range res.Errors {
|
|
if strings.Contains(e, "10054") {
|
|
t.Fatalf("a sealed-grants write exceeded the bucket's largest value: %s", e)
|
|
}
|
|
}
|
|
g, _, _ := p.Sealed()
|
|
digests = append(digests, g.Digest)
|
|
}
|
|
var idx grants.SealedLists
|
|
raw, _, found, _ := b.ownGet(grants.StateUserList, grants.KeySealedLists)
|
|
if !found || json.Unmarshal(raw, &idx) != nil || len(idx.Lists) != 2 || idx.Lists[0].Digest != digests[2] || idx.Lists[1].Digest != digests[1] {
|
|
t.Fatalf("the index of kept lists = %s, want the last two of %v", raw, digests)
|
|
}
|
|
for i, d := range digests {
|
|
_, _, kept, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(d, "a.mod-x"))
|
|
if kept != (i > 0) {
|
|
t.Errorf("list %d's a.mod-x key kept = %v", i, kept)
|
|
}
|
|
}
|
|
for _, u := range idx.Lists[0].Users {
|
|
if _, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(digests[2], u)); !ok {
|
|
t.Errorf("the index names %s, whose key is not there", u)
|
|
}
|
|
}
|
|
// The loaded list's permissions are read back from the keys when the bus module's tools cannot say them.
|
|
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
|
UserList: &delivery.UserListWritten{Source: delivery.GrantsUserListSource, Follows: delivery.FollowGrants, Revision: 8,
|
|
Digest: digests[2], Users: 6, Key: busKey.PublicKey}})
|
|
b.refuseUsers = true
|
|
if res := p.Compose(); slices.ContainsFunc(res.Grants.Waits, func(w string) bool { return strings.Contains(w, "unknown") }) {
|
|
t.Errorf("the kept permissions of the loaded list were not read: %v", res.Grants.Waits)
|
|
}
|
|
})
|
|
|
|
// Sweep 2026-10-30 item 5, a kind met twice that day (the engine's acts, then mesh-host_health): one principal the
|
|
// composer refuses as a second writer froze the whole list. Now it keeps its grants of the list in force, the rest is
|
|
// written, and the refusal is raised by name; one the list in force does not carry stays out, said too.
|
|
t.Run("a refused principal keeps its grants of the list in force; the rest of the list is written", func(t *testing.T) {
|
|
b := fixture()
|
|
const ctl = `{"module":"mesh-controller","version":"1","own-secrets":{"broker":"/var/lib/c/broker"}}`
|
|
refusedCtl := strings.Replace(ctl, `}}`, `},"state":[{"name":"declared-config"}]}`, 1)
|
|
b.manifestOf("mesh-controller", ctl)
|
|
b.assign("b", "mesh-controller")
|
|
hash := func(user, h string, gen int) {
|
|
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": h, "generation": gen, "fingerprint": fmt.Sprintf("sha256:%d", gen)})
|
|
}
|
|
for _, u := range shadowOf(t, b) {
|
|
hash(u.User, "$2a$10$first", 1)
|
|
}
|
|
hash("a.mesh-controller", "$2a$10$first", 1)
|
|
busKey, err := seal.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}})
|
|
p := partOn(b)
|
|
objs := &fakeObjects{}
|
|
p.Objects = objs
|
|
entryOf := func(g delivery.GrantsUserList, user string) string {
|
|
t.Helper()
|
|
list, err := delivery.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
|
|
out, err := seal.Open(bx, busKey.PrivateKey)
|
|
return []byte(out), err
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("the sealed list does not open: %v", err)
|
|
}
|
|
at := strings.Index(string(list), fmt.Sprintf("{ user: %q,", user))
|
|
if at < 0 {
|
|
return ""
|
|
}
|
|
rest := string(list)[at:]
|
|
return rest[:strings.Index(rest, "} }\n")+4]
|
|
}
|
|
var said []asks.Condition
|
|
p.Say = func(c asks.Condition) error {
|
|
if c.Key == grants.RefusedCondition {
|
|
said = append(said, c)
|
|
}
|
|
return nil
|
|
}
|
|
p.Compose()
|
|
inForce, _, _ := p.Sealed()
|
|
if inForce.Digest == "" {
|
|
t.Fatal("no list written after take-over")
|
|
}
|
|
entryBefore := entryOf(inForce, "b.mesh-controller")
|
|
ctlKey := grants.SealedUserKey(inForce.Digest, "b.mesh-controller")
|
|
before, _, ok, _ := b.ownGet(grants.StateUserList, ctlKey)
|
|
if !ok {
|
|
t.Fatal("the list in force keeps no grants of b.mesh-controller")
|
|
}
|
|
// b.mesh-controller now declares the controller's mirror as its own state: refused as a second writer. a.mod-x's hash
|
|
// changes in the same pass, and a.mesh-controller, refused the same way, is placed new.
|
|
b.manifestOf("mesh-controller", refusedCtl)
|
|
b.assign("a", "mesh-controller")
|
|
hash("a.mod-x", "$2a$10$second", 2)
|
|
res := p.Compose()
|
|
now, _, _ := p.Sealed()
|
|
if now.Digest == inForce.Digest || slices.ContainsFunc(res.Errors, func(e string) bool { return strings.Contains(e, "composed whole") }) {
|
|
t.Fatalf("a refused principal froze the list: sealed %+v, errors %v", res.Sealed, res.Errors)
|
|
}
|
|
after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mesh-controller"))
|
|
if !ok || string(after) != string(before) {
|
|
t.Errorf("b.mesh-controller did not keep its grants of the list in force: %s, was %s", after, before)
|
|
}
|
|
// The kept user is written as the composer writes one: the bus reads the same entry it loaded.
|
|
if got := entryOf(now, "b.mesh-controller"); entryBefore == "" || got != entryBefore {
|
|
t.Errorf("the kept entry is written otherwise than the list in force holds it:\n%s\nwas\n%s", got, entryBefore)
|
|
}
|
|
if entryOf(now, "a.mesh-controller") != "" {
|
|
t.Error("a refused principal the list in force did not carry is in the sealed list")
|
|
}
|
|
if _, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.mesh-controller")); ok {
|
|
t.Error("a refused principal the list in force did not carry is in the list")
|
|
}
|
|
if len(res.PrincipalsRefused) != 2 || len(said) == 0 || said[len(said)-1].State != asks.StateRaised ||
|
|
!strings.Contains(said[len(said)-1].Summary, "b.mesh-controller (keeps its grants of the list in force)") ||
|
|
!strings.Contains(said[len(said)-1].Summary, "a.mesh-controller (left out of the list") {
|
|
t.Fatalf("the refusals were not said by name: %v, said %+v", res.PrincipalsRefused, said)
|
|
}
|
|
// Composing again changes nothing and says nothing new; once the manifest composes, the warning is cleared.
|
|
n := len(said)
|
|
if p.Compose(); len(said) != n {
|
|
t.Errorf("an unchanged refusal was said again: %+v", said[n:])
|
|
}
|
|
b.manifestOf("mesh-controller", ctl)
|
|
if res := p.Compose(); len(res.PrincipalsRefused) != 0 || said[len(said)-1].State != asks.StateCleared {
|
|
t.Errorf("the refusal was not cleared once it composes: %v, said %+v", res.PrincipalsRefused, said)
|
|
}
|
|
})
|
|
}
|
|
|
|
// fakeObjects is the part's object bucket of sealed lists.
|
|
type fakeObjects struct{ objects map[string][]byte }
|
|
|
|
func (o *fakeObjects) PutObject(name string, data []byte) error {
|
|
if o.objects == nil {
|
|
o.objects = map[string][]byte{}
|
|
}
|
|
o.objects[name] = data
|
|
return nil
|
|
}
|
|
|
|
func (o *fakeObjects) DeleteObject(name string) error { delete(o.objects, name); return nil }
|
|
|
|
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")
|
|
}
|