Grant a seat's traffic by caller and by kind, so an ask's asker and a channel's kind are facts the bus enforces (hq ADR 0259)
This commit is contained in:
@@ -126,7 +126,9 @@ func ConsumerFor(p Principal) (Consumer, bool) {
|
||||
// A module that reacts to anything — a module's events or a role's (novox/hq ADR 0121). Watching
|
||||
// a role was missing here, so the one module that does it got no consumer at all: it started,
|
||||
// connected, and its graph stayed empty with nothing anywhere reporting why.
|
||||
if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0) {
|
||||
// And one that hears its own answers on a seat it uses (novox/hq ADR 0259 §3): an asker's warrants.
|
||||
hearsItsOwn := len(SeatTrafficOf(p.Module, nil, p.Uses, nil).Subscribe) > 0
|
||||
if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0 && !hearsItsOwn) {
|
||||
return Consumer{}, false
|
||||
}
|
||||
perms, err := PermissionsFor(p)
|
||||
|
||||
@@ -50,6 +50,12 @@ type Membership struct {
|
||||
// and refuses, with the reason, what is not on it — the bus enforces only the union over every
|
||||
// module on the machine.
|
||||
State []StateIssued `json:"state,omitempty"`
|
||||
// SeatTraffic is what this module's code may submit, say, hear, take, ask, answer and read on seats
|
||||
// that name their caller or their kind (novox/hq ADR 0259 §3). The runtime carrying the module
|
||||
// publishes, takes and answers for it only what is listed here: the bus enforces only the union
|
||||
// over every module on the machine, so one module's code reaching another's name or kind through
|
||||
// the runtime is the runtime's to refuse.
|
||||
SeatTraffic *SeatTraffic `json:"seat-traffic,omitempty"`
|
||||
}
|
||||
|
||||
// Served is one address a tool is answered on.
|
||||
@@ -109,6 +115,10 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
|
||||
}
|
||||
}
|
||||
m.State = stateIssuedFor(d, node)
|
||||
if t := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches); len(t.Publish)+len(t.Subscribe)+
|
||||
len(t.Answers)+len(t.Workers)+len(t.Records) > 0 {
|
||||
m.SeatTraffic = &t
|
||||
}
|
||||
if len(d.Invokes) > 0 {
|
||||
m.Reaches = map[string][]string{}
|
||||
for _, t := range d.Invokes {
|
||||
|
||||
+38
-1
@@ -63,6 +63,18 @@ type Seat struct {
|
||||
Emits []string
|
||||
Serves []string
|
||||
Versions []string // protocol versions served beside the current one; empty for v1 only
|
||||
|
||||
// Kinded says the seat is a kinded bench (novox/hq ADR 0234 §2, ADR 0259 §3): each holder claims one
|
||||
// kind, and its verbs' subjects carry it. Kind is the kind this principal's claim names, for a seat it
|
||||
// holds.
|
||||
Kinded bool
|
||||
Kind string
|
||||
// ByCaller are the accepts and emits whose last token names the calling module (ADR 0259 §3).
|
||||
ByCaller []string
|
||||
// Proofs are the seat's proof verbs: core request and reply, never on a stream (ADR 0259 §3).
|
||||
Proofs []string
|
||||
// Records are the holder's buckets, by their full name, each user reads under its own name.
|
||||
Records []string
|
||||
}
|
||||
|
||||
// A Principal is one user of the bus. Its permissions are derived from what it declares and
|
||||
@@ -530,6 +542,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// 2b. Events of a role it watches, under the seat's own namespace. Subscribe only: watching a
|
||||
// role is hearing what it announced, not taking part in it.
|
||||
for _, w := range p.Watches {
|
||||
if w.Kinded {
|
||||
continue // composed by SeatTrafficOf below
|
||||
}
|
||||
for _, e := range w.Emits {
|
||||
sub = append(sub, seatSubject(w, "event", e))
|
||||
}
|
||||
@@ -548,6 +563,13 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
|
||||
// 3. Seats it holds: full participation.
|
||||
for _, s := range p.Holds {
|
||||
if s.isNewTraffic() {
|
||||
// Composed by SeatTrafficOf below, worker and all; only its tools are served here.
|
||||
for _, t := range s.Serves {
|
||||
sub = append(sub, seatToolSubject(s, t, p.Node))
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Taking work from the role's queue: the worker consumer every holder shares (asked
|
||||
// about, pulled from, acknowledged), on the seat's own stream (novox/hq ADR 0190). A
|
||||
// holder pulls — asks the consumer for its next message, answered on its own inbox —
|
||||
@@ -590,7 +612,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// seat's inbound subject and watch other modules' traffic, nor publish its outbound
|
||||
// events and lie about outcomes (design 29 §2).
|
||||
for _, s := range p.Uses {
|
||||
for _, a := range s.Accepts {
|
||||
for _, a := range plainVerbs(s, s.Accepts) {
|
||||
pub = append(pub, seatSubject(s, "accept", a))
|
||||
}
|
||||
for _, t := range s.Serves {
|
||||
@@ -603,6 +625,12 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State,
|
||||
PerMachine: p.PerMachine, Reads: p.Reads, KeyedReads: p.KeyedReads})...)
|
||||
|
||||
// 6. Its traffic on seats that name their caller or their kind, ask proofs or keep records
|
||||
// (novox/hq ADR 0259 §3).
|
||||
tp, ts := SeatTrafficOf(p.Module, p.Holds, p.Uses, p.Watches).grants()
|
||||
pub = append(pub, tp...)
|
||||
sub = append(sub, ts...)
|
||||
|
||||
case KindNodeTools:
|
||||
// **One process serves what every module on the machine would have served for itself**
|
||||
// (novox/hq ADR 0175). Each carried module's whole tool namespace — the same grant that
|
||||
@@ -680,6 +708,15 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State),
|
||||
PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...)
|
||||
}
|
||||
// **And the seat traffic of the modules it carries** (novox/hq ADR 0259 §3): a bundle reaches the
|
||||
// bus only through its runtime, so the runtime is granted the union. That one module's code does
|
||||
// not publish under another's name or kind through it is the runtime's to keep, from the seat
|
||||
// traffic each module's membership lists.
|
||||
for _, d := range p.Carries {
|
||||
tp, ts := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches).grants()
|
||||
pub = append(pub, tp...)
|
||||
sub = append(sub, ts...)
|
||||
}
|
||||
sub = unique(sub)
|
||||
pub = unique(pub)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
package broker
|
||||
|
||||
import "sort"
|
||||
|
||||
// What a module may say and take on the seats it holds, uses and watches, beyond tools (novox/hq ADR
|
||||
// 0259 §3, to-be 46 §10).
|
||||
//
|
||||
// Three rules are added to the ones a seat always had, each a subject whose last token names who may
|
||||
// publish it, granted to that publisher alone — the way a node seat's event about a machine carries the
|
||||
// machine (ADR 0219):
|
||||
//
|
||||
// - **A verb named by its caller** (`by-caller`). A user of the seat submits that accept, and hears that
|
||||
// event, under its own module's name and no other: `accept.ask.<module>`, `event.decided.<module>`. The
|
||||
// holder takes every caller's accept and says the event to any caller. So an ask's asker is a fact the
|
||||
// server enforces, and a warrant reaches only the asker it is for.
|
||||
// - **A kinded bench** (ADR 0234 §2). A holder claims one kind and takes its own kind's accepts, says its
|
||||
// own kind's events and asks its own kind's proofs, and nothing of another kind; a user submits to any
|
||||
// kind. Each kind has its own worker on the seat's queue, so a holder that is away keeps its work and
|
||||
// holds up no other kind.
|
||||
// - **A proof** (`proofs`): core request and reply on `mesh.seat.<seat>.proof.<verb>.<kind>`. No stream's
|
||||
// subjects cover it, so what travels there — a code typed by the operator — is never persisted. A kinded
|
||||
// holder asks with its own kind; the modules that watch the seat answer.
|
||||
//
|
||||
// And one read: **a holder's records**, a bucket the seat names, read by each user under its own name only
|
||||
// (`$KV.<bucket>.<module>.>`), so an asker reads the state of its own asks and no other asker's.
|
||||
|
||||
// Worker is one durable consumer a holder pulls a seat's work from.
|
||||
type Worker struct {
|
||||
Stream string
|
||||
Consumer string
|
||||
Filter string
|
||||
}
|
||||
|
||||
// SeatTraffic is one module's seat traffic beyond tools. Publish and Subscribe are subject patterns, in the
|
||||
// server's wildcards; the runtime that carries the module checks a bundle's request against them, since
|
||||
// the runtime's own principal holds the union of every module it carries.
|
||||
type SeatTraffic struct {
|
||||
// Publish is what it submits (accepts of seats it uses), says (events of seats it holds) and asks
|
||||
// (proofs of seats it holds a kind of).
|
||||
Publish []string `json:"publish,omitempty"`
|
||||
// Subscribe is what it hears: events of seats it uses that are named by caller, events of seats it
|
||||
// watches, and accepts of seats it holds.
|
||||
Subscribe []string `json:"subscribe,omitempty"`
|
||||
// Answers is the proof subjects it answers, as a watcher of a kinded seat.
|
||||
Answers []string `json:"answers,omitempty"`
|
||||
// Workers are the work queues it takes from, as a holder.
|
||||
Workers []Worker `json:"workers,omitempty"`
|
||||
// Records is the direct-get subjects of the records it reads under its own name.
|
||||
Records []string `json:"records,omitempty"`
|
||||
}
|
||||
|
||||
// KindedBenches are the seats that may be kinded (ADR 0234 §2): making another is a decision, recorded.
|
||||
var KindedBenches = map[string]bool{"channel": true, "intake": true}
|
||||
|
||||
// WorkerName is the worker a seat's holders pull from: one for the seat, or one per kind on a kinded bench.
|
||||
func WorkerName(seat, kind string) string {
|
||||
if kind == "" {
|
||||
return "SEAT_" + upperSnake(seat) + "_worker"
|
||||
}
|
||||
return "SEAT_" + upperSnake(seat) + "_" + upperSnake(kind) + "_worker"
|
||||
}
|
||||
|
||||
func namesVerb(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SeatTrafficOf derives one module's seat traffic from the seats it holds, uses and watches. Only seats
|
||||
// carrying one of the rules above are read: every other seat is composed as it always was.
|
||||
func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic {
|
||||
var t SeatTraffic
|
||||
for _, s := range holds {
|
||||
if !s.isNewTraffic() {
|
||||
continue
|
||||
}
|
||||
kind := ""
|
||||
if s.Kinded {
|
||||
kind = s.Kind
|
||||
if kind == "" || !safeSubject.MatchString(kind) {
|
||||
// A kinded claim without a usable kind is refused at registration; here it is granted
|
||||
// nothing, which is the same answer at the last place it could be asked.
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(s.Accepts) > 0 {
|
||||
stream := seatStreamName(s.Name)
|
||||
filter := "mesh.seat." + s.Name + ".accept.>"
|
||||
if kind != "" {
|
||||
filter = "mesh.seat." + s.Name + ".accept.*." + kind
|
||||
}
|
||||
t.Workers = append(t.Workers, Worker{Stream: stream, Consumer: WorkerName(s.Name, kind), Filter: filter})
|
||||
}
|
||||
for _, a := range s.Accepts {
|
||||
switch {
|
||||
case kind != "":
|
||||
t.Subscribe = append(t.Subscribe, seatSubject(s, "accept", a+"."+kind))
|
||||
case namesVerb(s.ByCaller, a):
|
||||
t.Subscribe = append(t.Subscribe, seatSubject(s, "accept", a+".*"))
|
||||
}
|
||||
}
|
||||
for _, e := range s.Emits {
|
||||
switch {
|
||||
case kind != "":
|
||||
t.Publish = append(t.Publish, seatSubject(s, "event", e+"."+kind))
|
||||
case namesVerb(s.ByCaller, e):
|
||||
t.Publish = append(t.Publish, seatSubject(s, "event", e+".*"))
|
||||
}
|
||||
}
|
||||
if kind != "" {
|
||||
for _, v := range s.Proofs {
|
||||
t.Publish = append(t.Publish, seatSubject(s, "proof", v+"."+kind))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range uses {
|
||||
if !s.isNewTraffic() {
|
||||
continue
|
||||
}
|
||||
for _, a := range s.Accepts {
|
||||
switch {
|
||||
case namesVerb(s.ByCaller, a):
|
||||
t.Publish = append(t.Publish, seatSubject(s, "accept", a+"."+module))
|
||||
case s.Kinded:
|
||||
t.Publish = append(t.Publish, seatSubject(s, "accept", a+".*"))
|
||||
}
|
||||
}
|
||||
for _, e := range s.Emits {
|
||||
if namesVerb(s.ByCaller, e) {
|
||||
t.Subscribe = append(t.Subscribe, seatSubject(s, "event", e+"."+module))
|
||||
}
|
||||
}
|
||||
for _, b := range s.Records {
|
||||
if !safeSubject.MatchString(b) {
|
||||
continue
|
||||
}
|
||||
t.Records = append(t.Records, "$JS.API.DIRECT.GET.KV_"+b+".$KV."+b+"."+module+".>")
|
||||
}
|
||||
}
|
||||
for _, w := range watches {
|
||||
if w.Kinded {
|
||||
for _, e := range w.Emits {
|
||||
t.Subscribe = append(t.Subscribe, seatSubject(w, "event", e+".*"))
|
||||
}
|
||||
for _, v := range w.Proofs {
|
||||
t.Answers = append(t.Answers, seatSubject(w, "proof", v+".*"))
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Publish = unique(t.Publish)
|
||||
t.Subscribe = unique(t.Subscribe)
|
||||
t.Answers = unique(t.Answers)
|
||||
t.Records = unique(t.Records)
|
||||
sort.Slice(t.Workers, func(i, j int) bool { return t.Workers[i].Consumer < t.Workers[j].Consumer })
|
||||
return t
|
||||
}
|
||||
|
||||
// grants is the bus permissions seat traffic needs: the subjects themselves, and the JetStream API a
|
||||
// worker is pulled and acknowledged through and a record is read through.
|
||||
func (t SeatTraffic) grants() (pub, sub []string) {
|
||||
pub = append(pub, t.Publish...)
|
||||
sub = append(sub, t.Subscribe...)
|
||||
sub = append(sub, t.Answers...)
|
||||
for _, w := range t.Workers {
|
||||
pub = append(pub,
|
||||
"$JS.API.CONSUMER.INFO."+w.Stream+"."+w.Consumer,
|
||||
"$JS.API.CONSUMER.MSG.NEXT."+w.Stream+"."+w.Consumer,
|
||||
"$JS.ACK."+w.Stream+"."+w.Consumer+".>")
|
||||
}
|
||||
pub = append(pub, t.Records...)
|
||||
return pub, sub
|
||||
}
|
||||
|
||||
// isNewTraffic says whether a seat carries any of the rules above, so a seat that carries none is
|
||||
// composed exactly as before them.
|
||||
func (s Seat) isNewTraffic() bool {
|
||||
return s.Kinded || len(s.ByCaller) > 0 || len(s.Proofs) > 0 || len(s.Records) > 0
|
||||
}
|
||||
|
||||
// plainVerbs is a seat's accepts or emits with those the rules above compose taken out: a verb named by
|
||||
// its caller and every verb of a kinded bench are composed by SeatTrafficOf and nowhere else.
|
||||
func plainVerbs(s Seat, verbs []string) []string {
|
||||
if s.Kinded {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, v := range verbs {
|
||||
if !namesVerb(s.ByCaller, v) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SeatTrafficObjects is the work queues and workers the seat traffic of every composed user implies
|
||||
// (novox/hq ADR 0259 §3): a queue for each seat a holder takes work from, and each holder's worker on it —
|
||||
// one per kind on a kinded bench, filtered to that kind, so the kinds never take each other's work. Only
|
||||
// seats carrying the rules above; the mesh's own seats' queues are RaiseSeats'.
|
||||
func SeatTrafficObjects(users []Principal) ([]Stream, []Consumer) {
|
||||
streams := map[string]Stream{}
|
||||
consumers := map[string]Consumer{}
|
||||
add := func(module string, holds []Seat) {
|
||||
for _, w := range SeatTrafficOf(module, holds, nil, nil).Workers {
|
||||
seat := ""
|
||||
for _, s := range holds {
|
||||
if seatStreamName(s.Name) == w.Stream {
|
||||
seat = s.Name
|
||||
}
|
||||
}
|
||||
streams[w.Stream] = Stream{
|
||||
Name: w.Stream,
|
||||
Subjects: []string{"mesh.seat." + seat + ".accept.>"},
|
||||
Retention: RetentionWorkQueue,
|
||||
MaxAge: 7 * 24 * 60 * 60,
|
||||
Why: "work submitted to the " + seat + " seat; its holders take it, each kind its own, " +
|
||||
"and it queues while nobody does",
|
||||
}
|
||||
consumers[w.Consumer] = Consumer{
|
||||
Name: w.Consumer,
|
||||
Stream: w.Stream,
|
||||
Filters: []string{w.Filter},
|
||||
AckWaitSeconds: 60,
|
||||
MaxDeliver: 5,
|
||||
Why: module + " holds " + seat + "; it pulls one ask at a time and acknowledges once it has " +
|
||||
"recorded it, so a crash redelivers rather than loses",
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range users {
|
||||
switch p.Kind {
|
||||
case KindModule:
|
||||
add(p.Module, p.Holds)
|
||||
case KindNodeTools:
|
||||
for _, d := range p.Carries {
|
||||
add(d.Module, d.Holds)
|
||||
}
|
||||
}
|
||||
}
|
||||
var ss []Stream
|
||||
for _, s := range streams {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name })
|
||||
var cs []Consumer
|
||||
for _, c := range consumers {
|
||||
cs = append(cs, c)
|
||||
}
|
||||
sort.Slice(cs, func(i, j int) bool { return cs[i].Name < cs[j].Name })
|
||||
return ss, cs
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The seats of novox/hq ADR 0259 §3, as the messenger declares them.
|
||||
func operatorChannel() Seat {
|
||||
return Seat{Name: "operator-channel", Scope: "mesh", Accepts: []string{"ask", "cancel"},
|
||||
Emits: []string{"decided"}, Serves: []string{"open", "history", "notify"},
|
||||
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"messenger_asks"}}
|
||||
}
|
||||
|
||||
func channelSeat(kind string) Seat {
|
||||
return Seat{Name: "channel", Scope: "mesh", Accepts: []string{"show", "edit", "send"}, Kinded: true, Kind: kind}
|
||||
}
|
||||
|
||||
func intakeSeat(kind string) Seat {
|
||||
return Seat{Name: "intake", Scope: "mesh", Emits: []string{"choice", "link"}, Proofs: []string{"code"},
|
||||
Kinded: true, Kind: kind}
|
||||
}
|
||||
|
||||
func allowed(patterns []string, subject string) bool {
|
||||
for _, p := range patterns {
|
||||
if subjectMatches(p, subject) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func perms(t *testing.T, p Principal) Permissions {
|
||||
t.Helper()
|
||||
got, err := PermissionsFor(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
func TestAnAskerAsksAndHearsUnderItsOwnNameOnly(t *testing.T) {
|
||||
asker := Principal{Kind: KindModule, Node: "anchor", Module: "mesh-delivery", Uses: []Seat{operatorChannel()}}
|
||||
got := perms(t, asker)
|
||||
for _, s := range []string{
|
||||
"mesh.seat.operator-channel.accept.ask.mesh-delivery",
|
||||
"mesh.seat.operator-channel.accept.cancel.mesh-delivery",
|
||||
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-delivery.a1",
|
||||
} {
|
||||
if !allowed(got.Publish, s) {
|
||||
t.Errorf("an asker may not publish %s", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{
|
||||
"mesh.seat.operator-channel.accept.ask.mesh-controller",
|
||||
"mesh.seat.operator-channel.accept.ask.*",
|
||||
"mesh.seat.operator-channel.event.decided.mesh-delivery",
|
||||
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-controller.a1",
|
||||
"$KV.messenger_asks.mesh-delivery.a1",
|
||||
} {
|
||||
if allowed(got.Publish, s) {
|
||||
t.Errorf("an asker may publish %s, which is not its own to submit", s)
|
||||
}
|
||||
}
|
||||
if !allowed(got.Subscribe, "mesh.seat.operator-channel.event.decided.mesh-delivery") {
|
||||
t.Error("an asker does not hear its own warrants")
|
||||
}
|
||||
if allowed(got.Subscribe, "mesh.seat.operator-channel.event.decided.mesh-controller") {
|
||||
t.Error("an asker hears another asker's warrants")
|
||||
}
|
||||
// And its own consumer carries its warrants, so a restart catches up.
|
||||
c, ok := ConsumerFor(asker)
|
||||
if !ok || !allowed(c.Filters, "mesh.seat.operator-channel.event.decided.mesh-delivery") {
|
||||
t.Errorf("the asker's consumer does not carry its warrants: %v", c.Filters)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyTheHolderPublishesAWarrant(t *testing.T) {
|
||||
users, err := Users(Records{
|
||||
Nodes: []string{"anchor"},
|
||||
Assigned: map[string][]Declared{"anchor": {
|
||||
{Module: "messenger", Holds: []Seat{operatorChannel()}, Uses: []Seat{channelSeat("")},
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice", "link"}, Kinded: true, Proofs: []string{"code"}}}},
|
||||
{Module: "mesh-delivery", Uses: []Seat{operatorChannel()}},
|
||||
{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, u := range users {
|
||||
got := perms(t, u)
|
||||
says := allowed(got.Publish, "mesh.seat.operator-channel.event.decided.mesh-delivery")
|
||||
if says != (u.Module == "messenger") {
|
||||
t.Errorf("%s %s publish a warrant", u.Username(), map[bool]string{true: "may", false: "may not"}[says])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheHolderTakesEveryCallersAskThroughItsWorker(t *testing.T) {
|
||||
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "messenger", Holds: []Seat{operatorChannel()}})
|
||||
if !allowed(got.Subscribe, "mesh.seat.operator-channel.accept.ask.mesh-delivery") {
|
||||
t.Error("the router does not take an ask")
|
||||
}
|
||||
for _, s := range []string{
|
||||
"$JS.API.CONSUMER.MSG.NEXT.SEAT_OPERATOR_CHANNEL.SEAT_OPERATOR_CHANNEL_worker",
|
||||
"$JS.ACK.SEAT_OPERATOR_CHANNEL.SEAT_OPERATOR_CHANNEL_worker.x",
|
||||
"mesh.seat.operator-channel.event.decided.mesh-controller",
|
||||
} {
|
||||
if !allowed(got.Publish, s) {
|
||||
t.Errorf("the router may not publish %s", s)
|
||||
}
|
||||
}
|
||||
if allowed(got.Publish, "mesh.seat.operator-channel.accept.ask.messenger") {
|
||||
t.Error("the holder may ask its own seat without using it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAKindedHolderReachesItsOwnKindAndNoOther(t *testing.T) {
|
||||
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "telegram",
|
||||
Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}})
|
||||
for _, s := range []string{
|
||||
"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.event.link.telegram",
|
||||
"mesh.seat.intake.proof.code.telegram",
|
||||
"$JS.API.CONSUMER.MSG.NEXT.SEAT_CHANNEL.SEAT_CHANNEL_TELEGRAM_worker",
|
||||
} {
|
||||
if !allowed(got.Publish, s) {
|
||||
t.Errorf("telegram may not publish %s", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{
|
||||
"mesh.seat.intake.event.choice.desktop", "mesh.seat.intake.proof.code.desktop",
|
||||
"mesh.seat.channel.accept.show.telegram",
|
||||
"$JS.API.CONSUMER.MSG.NEXT.SEAT_CHANNEL.SEAT_CHANNEL_DESKTOP_worker",
|
||||
"mesh.seat.operator-channel.event.decided.mesh-delivery",
|
||||
} {
|
||||
if allowed(got.Publish, s) {
|
||||
t.Errorf("telegram may publish %s", s)
|
||||
}
|
||||
}
|
||||
if !allowed(got.Subscribe, "mesh.seat.channel.accept.show.telegram") ||
|
||||
allowed(got.Subscribe, "mesh.seat.channel.accept.show.desktop") {
|
||||
t.Error("telegram does not take exactly its own kind's work")
|
||||
}
|
||||
if allowed(got.Subscribe, "mesh.seat.intake.proof.code.telegram") {
|
||||
t.Error("a channel answers its own proofs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheWatcherAnswersProofsAndHearsEveryKind(t *testing.T) {
|
||||
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "messenger",
|
||||
Uses: []Seat{channelSeat("")},
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice"}, Kinded: true, Proofs: []string{"code"}}}})
|
||||
for _, s := range []string{"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.proof.code.desktop"} {
|
||||
if !allowed(got.Subscribe, s) {
|
||||
t.Errorf("the router does not hear %s", s)
|
||||
}
|
||||
}
|
||||
if !allowed(got.Publish, "mesh.seat.channel.accept.show.telegram") {
|
||||
t.Error("the router cannot send a channel its work")
|
||||
}
|
||||
if allowed(got.Publish, "mesh.seat.intake.event.choice.telegram") || allowed(got.Publish, "mesh.seat.intake.proof.code.telegram") {
|
||||
t.Error("the router may say a channel's answer or proof")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoStreamKeepsAProof(t *testing.T) {
|
||||
users, _ := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
|
||||
{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}},
|
||||
{Module: "messenger", Holds: []Seat{operatorChannel()}},
|
||||
}}})
|
||||
streams, _ := SeatTrafficObjects(users)
|
||||
streams = append(streams, MeshStreams()...)
|
||||
for _, s := range streams {
|
||||
for _, subject := range s.Subjects {
|
||||
if subjectMatches(subject, "mesh.seat.intake.proof.code.telegram") {
|
||||
t.Errorf("%s keeps a proof (%s)", s.Name, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEachKindHasAWorkerOfItsOwn(t *testing.T) {
|
||||
users, _ := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
|
||||
{Module: "telegram", Holds: []Seat{channelSeat("telegram")}},
|
||||
{Module: "desk-channel", Holds: []Seat{channelSeat("desktop")}},
|
||||
{Module: "messenger", Holds: []Seat{operatorChannel()}},
|
||||
}}})
|
||||
streams, workers := SeatTrafficObjects(users)
|
||||
names := map[string]string{}
|
||||
for _, w := range workers {
|
||||
names[w.Name] = strings.Join(w.Filters, ",")
|
||||
}
|
||||
want := map[string]string{
|
||||
"SEAT_CHANNEL_TELEGRAM_worker": "mesh.seat.channel.accept.*.telegram",
|
||||
"SEAT_CHANNEL_DESKTOP_worker": "mesh.seat.channel.accept.*.desktop",
|
||||
"SEAT_OPERATOR_CHANNEL_worker": "mesh.seat.operator-channel.accept.>",
|
||||
}
|
||||
for n, f := range want {
|
||||
if names[n] != f {
|
||||
t.Errorf("worker %s filters %q, want %q", n, names[n], f)
|
||||
}
|
||||
}
|
||||
if len(streams) != 2 {
|
||||
t.Errorf("want the queues of channel and operator-channel, got %v", streams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRuntimeIsGrantedTheUnionAndTheMembershipEachModulesShare(t *testing.T) {
|
||||
telegram := Declared{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}}
|
||||
desk := Declared{Module: "desk-channel", Holds: []Seat{channelSeat("desktop"), intakeSeat("desktop")}}
|
||||
got := perms(t, Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, Carries: []Declared{telegram, desk}})
|
||||
for _, s := range []string{"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.event.choice.desktop"} {
|
||||
if !allowed(got.Publish, s) {
|
||||
t.Errorf("the runtime may not publish %s for a module it carries", s)
|
||||
}
|
||||
}
|
||||
m := MembershipFor("anchor", telegram, Placements{})
|
||||
if m.SeatTraffic == nil || !allowed(m.SeatTraffic.Publish, "mesh.seat.intake.event.choice.telegram") ||
|
||||
allowed(m.SeatTraffic.Publish, "mesh.seat.intake.event.choice.desktop") {
|
||||
t.Errorf("telegram's membership does not list exactly its own kind: %+v", m.SeatTraffic)
|
||||
}
|
||||
if plain := MembershipFor("anchor", Declared{Module: "plain"}, Placements{}); plain.SeatTraffic != nil {
|
||||
t.Error("a module with no such seat is given seat traffic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestASeatWithoutTheNewRulesIsComposedAsBefore(t *testing.T) {
|
||||
old := Seat{Name: "node-build-agent", Scope: "node", Accepts: []string{"build"}, Emits: []string{"built"}}
|
||||
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "builder", Holds: []Seat{old}})
|
||||
for _, s := range []string{"mesh.seat.node-build-agent.event.built",
|
||||
"$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_BUILD_AGENT.SEAT_NODE_BUILD_AGENT_worker"} {
|
||||
if !allowed(got.Publish, s) {
|
||||
t.Errorf("an old seat's holder lost %s", s)
|
||||
}
|
||||
}
|
||||
if !allowed(got.Subscribe, "mesh.seat.node-build-agent.accept.build") {
|
||||
t.Error("an old seat's holder lost its accept")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user