The tool runner's bus account carried the heard grants, consumer and bind word of every module it runs, and every agent session on that machine reaches the bus as that account, so any session could pull a granted module's consumer on MAIL. Per ADR 0376 §1 the controller now composes a restricted stream's consumer, grants and bind word only for a module that declares runs-as; registration refuses a consumes of a restricted stream without runs-as (§2), and a grant that composes nothing is said in doctor (D-heard) and in the controller's log on every raise and send.
308 lines
13 KiB
Go
308 lines
13 KiB
Go
package broker
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mailStream is mesh-mail's own stream as the bus holds it (novox/hq ADR 0363).
|
|
var mailStream = ModuleStream{Module: "mesh-mail", Name: "MAIL",
|
|
Events: []string{"mail.*.message.added", "mail.*.message.changed", "mail.*.message.removed"},
|
|
MaxAgeDays: 7, MaxMiB: 1024, Restricted: true}
|
|
|
|
// mailHeard is what a reader granted the account `mesh` hears of it.
|
|
var mailHeard = Heard{Stream: "MAIL", Emitter: "mesh-mail", Restricted: true, Filters: []string{
|
|
"mesh.stream.mesh-mail.event.mail.mesh.message.added", "mesh.stream.mesh-mail.event.mail.mesh.message.changed"}}
|
|
|
|
// newsStream is a module's stream without a grant: heard by whoever asks (ADR 0363 §5), the tool runner included.
|
|
var newsStream = ModuleStream{Module: "news", Name: "NEWS", Events: []string{"story.**"}, MaxAgeDays: 7, MaxMiB: 16}
|
|
|
|
var newsHeard = Heard{Stream: "NEWS", Emitter: "news", Filters: []string{"mesh.stream.news.event.story.>"}}
|
|
|
|
// A module's stream is its own: subjects EVENTS does not claim, its age and size, kept by limits (ADR 0363 §1, §2).
|
|
func TestAModuleStreamTakesSubjectsEventsDoesNot(t *testing.T) {
|
|
s := mailStream.Stream()
|
|
if s.Name != "MAIL" || s.Retention != RetentionLimits || s.MaxAge != 7*24*60*60 || s.MaxBytes != 1024<<20 || s.DiscardNew {
|
|
t.Fatalf("composed as %+v", s)
|
|
}
|
|
want := []string{"mesh.stream.mesh-mail.event.mail.*.message.added", "mesh.stream.mesh-mail.event.mail.*.message.changed",
|
|
"mesh.stream.mesh-mail.event.mail.*.message.removed"}
|
|
if !reflect.DeepEqual(s.Subjects, want) {
|
|
t.Fatalf("subjects %v", s.Subjects)
|
|
}
|
|
for _, own := range MeshStreams() {
|
|
for _, a := range own.Subjects {
|
|
for _, b := range s.Subjects {
|
|
if SubjectsOverlap(a, b) {
|
|
t.Fatalf("%s claims %s, which overlaps %s of %s: the bus would refuse the stream", s.Name, b, a, own.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if WirePattern("mail.**") != "mail.>" {
|
|
t.Fatal("`**` is spelled `>` on the wire")
|
|
}
|
|
}
|
|
|
|
// The provider publishes its stream's events on their subjects and nowhere on EVENTS for them; a consumer
|
|
// granted a part hears it through its own consumer on the stream, and one not granted has nothing there
|
|
// (ADR 0363 §2, §5).
|
|
func TestTheProviderPublishesItsStreamAndOnlyAGrantedConsumerHearsIt(t *testing.T) {
|
|
provider, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "mesh-mail",
|
|
Streams: []ModuleStream{mailStream}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !MayPublish(provider, "mesh.stream.mesh-mail.event.mail.work.message.added") {
|
|
t.Fatal("the provider may not publish its own stream's event")
|
|
}
|
|
if MayPublish(provider, "mesh.mod.mesh-mail.event.mail.work.message.added") {
|
|
t.Fatal("the provider may publish its stream's event on EVENTS")
|
|
}
|
|
if MayPublish(provider, "mesh.stream.other.event.mail.work.message.added") {
|
|
t.Fatal("the provider may publish on another module's stream")
|
|
}
|
|
|
|
reader := Principal{Kind: KindModule, Node: "ace", Module: "reader", Hears: []Heard{mailHeard}}
|
|
perms, err := PermissionsFor(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range []string{"$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_reader", "$JS.API.CONSUMER.INFO.MAIL.ace_reader",
|
|
"$JS.ACK.MAIL.ace_reader.1"} {
|
|
if !MayPublish(perms, s) {
|
|
t.Errorf("the granted reader may not publish %s", s)
|
|
}
|
|
}
|
|
if !MaySubscribe(perms, "mesh.stream.mesh-mail.event.mail.mesh.message.added") ||
|
|
MaySubscribe(perms, "mesh.stream.mesh-mail.event.mail.work.message.added") {
|
|
t.Fatal("the reader's subscription is not what it was granted")
|
|
}
|
|
if MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_other") {
|
|
t.Fatal("the reader may pull another's consumer")
|
|
}
|
|
|
|
stranger, err := PermissionsFor(Principal{Kind: KindModule, Node: "ace", Module: "stranger",
|
|
Consumes: []string{"billing.paid"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if MayPublish(stranger, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_stranger") ||
|
|
MaySubscribe(stranger, "mesh.stream.mesh-mail.event.mail.mesh.message.added") {
|
|
t.Fatal("a module not granted reaches the stream")
|
|
}
|
|
}
|
|
|
|
// A granted consumer of its own account has a durable consumer on the stream, with its granted subjects as
|
|
// filters, and an EVENTS consumer that hears what it gives up on delivered again (ADR 0363 §5, §7); a module of its
|
|
// own account is no module the runtime carries, so the runtime reads neither (ADR 0376 §1).
|
|
func TestAGrantedConsumerHasAConsumerOnTheStream(t *testing.T) {
|
|
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
|
|
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}},
|
|
"ace": {{Module: RuntimeModule}, {Module: "reader", RunsAs: "reader", Hears: []Heard{mailHeard}, HearsAStream: true},
|
|
{Module: "stranger", HearsAStream: true}},
|
|
}}
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var onMail, onEvents []Consumer
|
|
for _, c := range ConsumersOf(users) {
|
|
switch c.Consumer.Stream {
|
|
case "MAIL":
|
|
onMail = append(onMail, c.Consumer)
|
|
case EventsStream:
|
|
onEvents = append(onEvents, c.Consumer)
|
|
}
|
|
}
|
|
if len(onMail) != 1 || onMail[0].Name != "ace_reader" || !slices.Equal(onMail[0].Filters, mailHeard.Filters) {
|
|
t.Fatalf("on MAIL: %+v", onMail)
|
|
}
|
|
if len(onEvents) != 1 || onEvents[0].Name != "ace_reader" || !slices.Equal(onEvents[0].Filters, []string{AgainFilter("ace_reader")}) {
|
|
t.Fatalf("on EVENTS: %+v", onEvents)
|
|
}
|
|
for _, u := range users {
|
|
if u.Node != "ace" || (u.Kind != KindNodeTools && u.Module != "reader") {
|
|
continue
|
|
}
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reads := MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_reader") && MayPublish(perms, "$JS.ACK.MAIL.ace_reader.1")
|
|
if u.Kind == KindNodeTools && reads {
|
|
t.Fatal("the runtime may read the consumer on MAIL of a module of its own account")
|
|
}
|
|
if u.Kind == KindModule && !reads {
|
|
t.Fatal("the reader's own account may not read its consumer on MAIL")
|
|
}
|
|
if MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_stranger") {
|
|
t.Fatal("a consumer no module is granted may be read")
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The tool runner never hears a restricted stream** (ADR 0376 §1, §2): a module it carries, granted part of
|
|
// MAIL, gets no consumer on MAIL, the tool runner's account no grant to ask about, pull or acknowledge one there,
|
|
// and its membership no word to bind one; a module of its own account granted the same part gets all three, on
|
|
// its own account. The grant that composes nothing is said. A stream without a grant is heard through the tool
|
|
// runner as before.
|
|
func TestTheToolRunnerNeverHearsARestrictedStream(t *testing.T) {
|
|
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
|
|
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}},
|
|
"ace": {{Module: RuntimeModule},
|
|
{Module: "carried", Hears: []Heard{mailHeard, newsHeard}, HearsAStream: true},
|
|
{Module: "mailer", RunsAs: "mailer", Hears: []Heard{mailHeard}, HearsAStream: true}},
|
|
}}
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var onMail, onNews []string
|
|
for _, c := range ConsumersOf(users) {
|
|
switch c.Consumer.Stream {
|
|
case "MAIL":
|
|
onMail = append(onMail, c.Consumer.Name)
|
|
case "NEWS":
|
|
onNews = append(onNews, c.Consumer.Name)
|
|
}
|
|
}
|
|
if !slices.Equal(onMail, []string{"ace_mailer"}) {
|
|
t.Fatalf("consumers on MAIL: %v, want only the mailer's", onMail)
|
|
}
|
|
if !slices.Equal(onNews, []string{"ace_carried"}) {
|
|
t.Fatalf("consumers on NEWS: %v, want the carried module's as before", onNews)
|
|
}
|
|
for _, u := range users {
|
|
if u.Node != "ace" {
|
|
continue
|
|
}
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch {
|
|
case u.Kind == KindNodeTools:
|
|
for _, s := range perms.Publish {
|
|
if strings.Contains(s, ".MAIL.") {
|
|
t.Errorf("the tool runner's account may publish %s", s)
|
|
}
|
|
}
|
|
for _, s := range perms.Subscribe {
|
|
if strings.HasPrefix(s, "mesh.stream.mesh-mail.") {
|
|
t.Errorf("the tool runner's account may subscribe %s", s)
|
|
}
|
|
}
|
|
if !MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.NEWS.ace_carried") {
|
|
t.Error("the tool runner may no longer read the carried module's consumer on a stream without a grant")
|
|
}
|
|
case u.Kind == KindModule && u.Module == "carried":
|
|
if MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_carried") {
|
|
t.Error("a module without an account of its own may pull a consumer on MAIL")
|
|
}
|
|
case u.Kind == KindModule && u.Module == "mailer":
|
|
for _, s := range []string{"$JS.API.CONSUMER.INFO.MAIL.ace_mailer", "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_mailer",
|
|
"$JS.ACK.MAIL.ace_mailer.1"} {
|
|
if !MayPublish(perms, s) {
|
|
t.Errorf("the mailer's own account may not publish %s", s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
where := Placements{Nodes: map[string][]string{}}
|
|
carried := MembershipFor("ace", records.Assigned["ace"][1], where)
|
|
if !reflect.DeepEqual(carried.Hears, []HeardIssued{{Stream: "NEWS", Consumer: "ace_carried"}}) {
|
|
t.Fatalf("the carried module is told to bind %+v", carried.Hears)
|
|
}
|
|
mailer := MembershipFor("ace", records.Assigned["ace"][2], where)
|
|
if !reflect.DeepEqual(mailer.Hears, []HeardIssued{{Stream: "MAIL", Consumer: "ace_mailer"}}) {
|
|
t.Fatalf("the mailer is told to bind %+v", mailer.Hears)
|
|
}
|
|
withheld := WithheldOf(records)
|
|
if len(withheld) != 1 || withheld[0] != (Withheld{Node: "ace", Module: "carried", Stream: "MAIL", Emitter: "mesh-mail"}) {
|
|
t.Fatalf("withheld %+v", withheld)
|
|
}
|
|
if said := withheld[0].Said(); !strings.Contains(said, "carried on ace") || !strings.Contains(said, "MAIL") ||
|
|
!strings.Contains(said, "runs-as") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
}
|
|
|
|
// The membership says where a module's stream events go and which consumers on module streams to bind
|
|
// (ADR 0363 §2, §8).
|
|
func TestTheMembershipNamesTheStreamAndWhatIsHeard(t *testing.T) {
|
|
where := Placements{Nodes: map[string][]string{}}
|
|
provider := MembershipFor("novox", Declared{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, where)
|
|
if len(provider.Streams) != 1 || provider.Streams[0].Stream != "MAIL" ||
|
|
provider.Streams[0].Subject != "mesh.stream.mesh-mail.event.{event}" ||
|
|
!slices.Equal(provider.Streams[0].Events, mailStream.Events) {
|
|
t.Fatalf("the provider's membership: %+v", provider.Streams)
|
|
}
|
|
reader := MembershipFor("ace", Declared{Module: "reader", RunsAs: "reader", Hears: []Heard{mailHeard}}, where)
|
|
if !reflect.DeepEqual(reader.Hears, []HeardIssued{{Stream: "MAIL", Consumer: "ace_reader"}}) {
|
|
t.Fatalf("the reader's membership: %+v", reader.Hears)
|
|
}
|
|
if none := MembershipFor("ace", Declared{Module: "stranger", HearsAStream: true}, where); none.Hears != nil {
|
|
t.Fatalf("a module not granted is told to bind %+v", none.Hears)
|
|
}
|
|
}
|
|
|
|
type fakePruner struct {
|
|
on map[string][]string
|
|
removed []string
|
|
}
|
|
|
|
func (f *fakePruner) ConsumerNames(stream string) ([]string, error) { return f.on[stream], nil }
|
|
func (f *fakePruner) RemoveConsumer(stream, name string) error {
|
|
f.removed = append(f.removed, stream+"/"+name)
|
|
return nil
|
|
}
|
|
|
|
// A consumer that lost its grant has its consumer on the stream removed by the next assertion (ADR 0363 §6).
|
|
func TestAConsumerThatLostItsGrantIsRemoved(t *testing.T) {
|
|
f := &fakePruner{on: map[string][]string{"MAIL": {"ace_reader", "ace_former"}}}
|
|
wanted := []Consumer{{Name: "ace_reader", Stream: "MAIL"}, {Name: "ace_former", Stream: EventsStream}}
|
|
removed, err := PruneHeard(f, []ModuleStream{mailStream}, wanted)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Equal(removed, []string{"MAIL/ace_former"}) || !slices.Equal(f.removed, removed) {
|
|
t.Fatalf("removed %v", removed)
|
|
}
|
|
}
|
|
|
|
type refusingRaiser struct {
|
|
fakePruner
|
|
refuse bool
|
|
}
|
|
|
|
func (r *refusingRaiser) EnsureStream(Stream) error { return nil }
|
|
func (r *refusingRaiser) EnsureConsumer(Consumer) error {
|
|
if r.refuse {
|
|
return errors.New("consumer update refused")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A narrowing the bus refuses on a restricted stream removes the consumer, so its wider filters deliver nothing
|
|
// more; on an unrestricted stream the refusal is only said (review of PR 338, ADR 0363 §6).
|
|
func TestARefusedNarrowingOnARestrictedStreamRemovesTheConsumer(t *testing.T) {
|
|
c := Consumer{Name: "ace_reader", Stream: "MAIL", Filters: mailHeard.Filters[:1]}
|
|
r := &refusingRaiser{refuse: true}
|
|
err := EnsureHeard(r, c, true)
|
|
if err == nil || !strings.Contains(err.Error(), "REMOVED") || !slices.Equal(r.removed, []string{"MAIL/ace_reader"}) {
|
|
t.Fatalf("restricted: %v, removed %v", err, r.removed)
|
|
}
|
|
r = &refusingRaiser{refuse: true}
|
|
if err := EnsureHeard(r, c, false); err == nil || len(r.removed) != 0 {
|
|
t.Fatalf("unrestricted: %v, removed %v", err, r.removed)
|
|
}
|
|
r = &refusingRaiser{}
|
|
if err := EnsureHeard(r, c, true); err != nil || len(r.removed) != 0 {
|
|
t.Fatalf("accepted: %v, removed %v", err, r.removed)
|
|
}
|
|
}
|