Grant the health holders what ADR 0564 gives them and nothing more: named reads of the lease and the controller's conditions and hand acts, the watchman's own dead-letter consumer, and the bus tools' removal account (issue 1952 S5)
This commit is contained in:
@@ -33,8 +33,16 @@ var createdAdvisory = regexp.MustCompile(`"\$JS\.EVENT\.ADVISORY\.STREAM\.CREATE
|
||||
// grants part's list is the one the bus loads; a hand-back loads the controller's, which lacks these reads, and the nodes
|
||||
// keep applying without composing their own until the next take-over. Each principal's hearing of its buckets' creation
|
||||
// (issue 1790) is the other row beyond, matched by createdAdvisory.
|
||||
//
|
||||
// And the watchman's dead letters (novox/hq ADR 0564 clause 5, issue 1952 S5): its own consumer on DEAD_LETTER_NOTICES and
|
||||
// a read of DEAD_LETTERS, the controller's job until the lease write.
|
||||
func beyondTheControllers(user string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
if strings.HasSuffix(user, "."+WatchmanModule) {
|
||||
for _, s := range WatchmanDeadLetterGrants() {
|
||||
out[s] = true
|
||||
}
|
||||
}
|
||||
node, ok := strings.CutPrefix(user, "node.")
|
||||
if !ok {
|
||||
return out
|
||||
@@ -548,6 +556,8 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
t.Errorf("the grants part creates consumers as %v", u.CreatesConsumers)
|
||||
}
|
||||
}
|
||||
// 1952 S5 (novox/hq ADR 0564 clauses 2, 5, 8; Limits 2, 4, 8): the health holders' credentials.
|
||||
healthChecks(t)
|
||||
q := ConsumerConfig(QueueConsumer(Principal{Kind: KindProcess, Node: "a", Module: "m", Process: "p"},
|
||||
QueueOwner{Process: "p", Stream: "M_WORK", MaxDeliver: 3}))
|
||||
if q.Durable != "a_m_p" || q.FilterSubject != "" || q.MaxDeliver != 3 || q.DeliverPolicy != jetstream.DeliverAllPolicy {
|
||||
@@ -555,3 +565,89 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// healthChecks are the health holders' credential cases (ADR 0564 clauses 2, 5, 8; Limits 2, 4, 8; issue 1952 S5).
|
||||
func healthChecks(t *testing.T) {
|
||||
t.Helper()
|
||||
announced := map[string]announce.States{ControllerSeat: ControllerAnnouncement("novox"),
|
||||
"mesh-healers": {Module: "mesh-healers", States: []announce.State{{Name: "attempts", Readable: true}, {Name: "would", Readable: true}}}}
|
||||
for _, c := range []struct {
|
||||
reader, read string
|
||||
may bool
|
||||
}{
|
||||
{"mesh-conditions", ControllerLease, true}, {"mesh-healers", ControllerLease, true}, {WatchmanModule, ControllerLease, true},
|
||||
{"mesh-self-check", ControllerLease, false}, {"messenger", ControllerLease, false},
|
||||
{"mesh-conditions", ControllerConditions, true}, {"mesh-healers", ControllerConditions, false}, {"messenger", ControllerConditions, false},
|
||||
{"mesh-healers", ControllerHandActs, true}, {"mesh-conditions", ControllerHandActs, false},
|
||||
{"mesh-conditions", "mesh-healers.attempts", true}, {"mesh-conditions", HealersWould, false}, {"mesh-delivery-view", HealersWould, false},
|
||||
} {
|
||||
granted, _ := ReadsAnnouncedBy(c.reader, []string{c.read}, announced)
|
||||
if (len(granted) == 1) != c.may {
|
||||
t.Errorf("%s reads %s: %v, want %v", c.reader, c.read, granted, c.may)
|
||||
}
|
||||
}
|
||||
bucket := func(m, n string) Bucket { return Bucket{Module: m, Name: n} }
|
||||
users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": {
|
||||
{Module: "mesh-conditions", State: []Bucket{bucket("mesh-conditions", "conditions"), bucket("mesh-conditions", "takeover")},
|
||||
Reads: []string{"mesh-self-check.findings", "mesh-healers.attempts", ControllerConditions}},
|
||||
{Module: "mesh-self-check", State: []Bucket{bucket("mesh-self-check", "findings")}},
|
||||
{Module: "mesh-healers", State: []Bucket{bucket("mesh-healers", "findings"), bucket("mesh-healers", "attempts"), bucket("mesh-healers", "would")}},
|
||||
{Module: WatchmanModule, State: []Bucket{bucket(WatchmanModule, "findings")}},
|
||||
{Module: "nats", SnapshotsTheBus: true, Processes: []ProcessPrincipal{{Process: BusToolsProcess}, {Process: "other"}}},
|
||||
{Module: "shop", Processes: []ProcessPrincipal{{Process: BusToolsProcess}}},
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
may := map[string][]string{
|
||||
"a.mesh-conditions": {"$KV.mesh-conditions_conditions.machine.g14.silent"},
|
||||
WatchmanModule: {"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES." + WatchmanModule, "$JS.API.CONSUMER.MSG.NEXT.DEAD_LETTER_NOTICES." + WatchmanModule,
|
||||
"$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller", "$JS.API.STREAM.MSG.GET.DEAD_LETTERS"},
|
||||
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller",
|
||||
"$KV.mesh-host_acts.g14.a1", "$JS.API.STREAM.INFO.KV_mesh-host_acts"},
|
||||
}
|
||||
mayNot := map[string][]string{
|
||||
// The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2).
|
||||
"a.mesh-conditions": {"$KV.mesh-self-check_findings.x", "$KV.mesh-healers_attempts.x", "$KV.mesh-controller_conditions.x"},
|
||||
"a.mesh-self-check": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch"},
|
||||
"a.mesh-healers": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch"},
|
||||
// The watchman never deletes or changes the controller's consumer (clause 5).
|
||||
WatchmanModule: {"$KV.mesh-conditions_conditions.x", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller",
|
||||
"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES." + WatchmanModule},
|
||||
// The bus module's tools remove the one consumer and the acts' keys, nothing else (Limits 4); its other accounts
|
||||
// remove nothing, and a process of that name in another module is no tools account.
|
||||
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m",
|
||||
"$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts"},
|
||||
"a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
||||
"a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
||||
"a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, u := range users {
|
||||
name := u.Username()
|
||||
if u.Module == WatchmanModule && u.Kind == KindModule {
|
||||
name = WatchmanModule
|
||||
}
|
||||
perms, err := PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
for _, subject := range may[name] {
|
||||
seen[name] = true
|
||||
if !MayPublish(perms, subject) {
|
||||
t.Errorf("%s may not publish %s", name, subject)
|
||||
}
|
||||
}
|
||||
for _, subject := range mayNot[name] {
|
||||
seen[name] = true
|
||||
if MayPublish(perms, subject) {
|
||||
t.Errorf("%s may publish %s", name, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
for name := range mayNot {
|
||||
if !seen[name] {
|
||||
t.Errorf("no user %s was composed", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user