235 lines
9.4 KiB
Go
235 lines
9.4 KiB
Go
package broker
|
|
|
|
import (
|
|
"slices"
|
|
"sort"
|
|
"testing"
|
|
)
|
|
|
|
// A module's process with a bus declaration gets a bus account of its own (novox/hq ADR 0436 §1, §2, §3,
|
|
// §5): named <machine>.<module>.<process id>, granted exactly its bus declaration and nothing of its
|
|
// module's other grants or the tool runner's. The fixture is mesh-mail's future manifest shape (ADR 0436
|
|
// §7, §8): process "send", stream "mail-sends" (MAIL_SENDS), state "sends", using operator-channel as the
|
|
// router of its own asks (novox/hq ADR 0259 §3, seattraffic_test.go's operatorChannel()).
|
|
|
|
// mailSenderProcess is the process principal PermissionsFor composes a bus account from, as
|
|
// inventory.processesOf would read it off mesh-mail's manifest (1179/AC1's fixture, 1178's queue owner).
|
|
func mailSenderProcess() Principal {
|
|
return Principal{Kind: KindProcess, Node: "novox", Module: "mesh-mail", Process: "send",
|
|
Emits: []string{"mail.send.sent", "mail.send.failed"},
|
|
Queues: []QueueOwner{{Process: "send", Stream: "MAIL_SENDS", MaxDeliver: 5}},
|
|
State: []string{"sends"}, Asks: true, Uses: []Seat{operatorChannel()},
|
|
Invokes: []string{"seat:operator-channel.tell"},
|
|
}
|
|
}
|
|
|
|
// 1180/AC1: the process principal publishes its emits, pulls and acknowledges its one queue consumer, reads
|
|
// and writes its own state bucket, reads the router's records of its module's own asks, publishes its
|
|
// invoked seat verb in its own name and the plain form, and subscribes only its own inbox.
|
|
func TestAProcessIsGrantedExactlyItsBusDeclaration(t *testing.T) {
|
|
p := mailSenderProcess()
|
|
if got := p.Username(); got != "novox.mesh-mail.send" {
|
|
t.Fatalf("username: %q", got)
|
|
}
|
|
got := perms(t, p)
|
|
// review round 1, finding 8: the whole publish set, not merely that each wanted subject is somewhere in
|
|
// it — AC1 says "exactly". Dumped from a passing run (TestZZDumpProcessPublish, removed): 16 subjects,
|
|
// correct today.
|
|
wantPublish := []string{
|
|
"$JS.ACK.MAIL_SENDS.novox_mesh-mail_send.>",
|
|
"$JS.API.CONSUMER.CREATE.KV_mesh-mail_sends.>",
|
|
"$JS.API.CONSUMER.DELETE.KV_mesh-mail_sends.>",
|
|
"$JS.API.CONSUMER.INFO.MAIL_SENDS.novox_mesh-mail_send",
|
|
"$JS.API.CONSUMER.MSG.NEXT.MAIL_SENDS.novox_mesh-mail_send",
|
|
"$JS.API.DIRECT.GET.KV_mesh-mail_sends.>",
|
|
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-mail.>",
|
|
"$JS.API.STREAM.INFO.KV_mesh-mail_sends",
|
|
"$JS.FC.KV_mesh-mail_sends.>",
|
|
"$KV.mesh-mail_sends.>",
|
|
"mesh.mod.mesh-mail.event.mail.send.failed",
|
|
"mesh.mod.mesh-mail.event.mail.send.sent",
|
|
"mesh.seat.operator-channel.call.tell.*.novox~mesh-mail~send",
|
|
"mesh.seat.operator-channel.call.tell.novox~mesh-mail~send",
|
|
"mesh.seat.operator-channel.tool.tell",
|
|
"mesh.seat.operator-channel.tool.tell.*",
|
|
}
|
|
pub := append([]string(nil), got.Publish...)
|
|
sort.Strings(pub)
|
|
want := append([]string(nil), wantPublish...)
|
|
sort.Strings(want)
|
|
if !slices.Equal(pub, want) {
|
|
t.Fatalf("the process's whole publish set:\n%v\nwant exactly:\n%v", pub, want)
|
|
}
|
|
if !allowed(got.Subscribe, "_INBOX.novox.mesh-mail.send.x") {
|
|
t.Error("the process does not hear its own inbox")
|
|
}
|
|
if len(got.Subscribe) != 1 {
|
|
t.Errorf("the process subscribes %v; want only its own inbox", got.Subscribe)
|
|
}
|
|
}
|
|
|
|
// 1180/AC3: a module with no bus process composes as main does, and adding one to mesh-mail changes
|
|
// neither the tool runner's grants for a module it carries nor another module's own.
|
|
func TestAddingAProcessLeavesOtherGrantsUnchanged(t *testing.T) {
|
|
plain := Principal{Kind: KindModule, Node: "novox", Module: "other", PasswordHash: "x"}
|
|
before := perms(t, plain)
|
|
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
|
|
{Module: "other"},
|
|
{Module: "mesh-mail", Processes: []ProcessPrincipal{{Process: "send",
|
|
Emits: []string{"mail.send.sent"}}}, NoAccount: true},
|
|
}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sawProcess, sawOther bool
|
|
for _, u := range users {
|
|
if u.Kind == KindProcess && u.Module == "mesh-mail" {
|
|
sawProcess = true
|
|
}
|
|
if u.Module == "other" {
|
|
sawOther = true
|
|
if got := perms(t, u); !equalStringSlices(got.Publish, before.Publish) || !equalStringSlices(got.Subscribe, before.Subscribe) {
|
|
t.Errorf("another module's grants changed: %+v, want %+v", got, before)
|
|
}
|
|
}
|
|
if u.Kind == KindModule && u.Module == "mesh-mail" {
|
|
t.Error("mesh-mail has no own secret broker (NoAccount), and should have no module principal of its own")
|
|
}
|
|
}
|
|
if !sawProcess {
|
|
t.Fatal("the process account was not composed at all")
|
|
}
|
|
if !sawOther {
|
|
t.Fatal("the module with no process was not composed")
|
|
}
|
|
}
|
|
|
|
// 1180/AC3: a process account is never carried by the machine's tool runner, so adding one to a module the
|
|
// runtime carries changes nothing of the runtime's own grants for that module.
|
|
func TestAddingAProcessLeavesTheToolRunnersCarryingUnchanged(t *testing.T) {
|
|
assigned := func(processes []ProcessPrincipal) map[string][]Declared {
|
|
return map[string][]Declared{"novox": {
|
|
{Module: RuntimeModule},
|
|
{Module: "mesh-mail", Serves: []string{"status"}, Processes: processes},
|
|
}}
|
|
}
|
|
without, err := Users(Records{Nodes: []string{"novox"}, Assigned: assigned(nil)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
with, err := Users(Records{Nodes: []string{"novox"}, Assigned: assigned([]ProcessPrincipal{
|
|
{Process: "send", Emits: []string{"mail.send.sent"}},
|
|
})})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtimeOf := func(users []Principal) Principal {
|
|
for _, u := range users {
|
|
if u.Kind == KindNodeTools {
|
|
return u
|
|
}
|
|
}
|
|
t.Fatal("the tool runner was not composed")
|
|
return Principal{}
|
|
}
|
|
before, after := perms(t, runtimeOf(without)), perms(t, runtimeOf(with))
|
|
if !equalStringSlices(before.Publish, after.Publish) || !equalStringSlices(before.Subscribe, after.Subscribe) {
|
|
t.Errorf("the tool runner's grants changed when mesh-mail gained a process: before %+v, after %+v",
|
|
before, after)
|
|
}
|
|
}
|
|
|
|
// 1180/AC2, the grant test of ADR 0436 clause 3: a process account answers no tool call or seat verb,
|
|
// publishes no warrant, holds no seat (operator-channel included), and hears no restricted stream — however
|
|
// wide its own grants (emits, its queue, its state, its asks and its invokes) are.
|
|
func TestAProcessAccountAnswersNothing(t *testing.T) {
|
|
p := mailSenderProcess()
|
|
got := perms(t, p)
|
|
for _, s := range []string{
|
|
// Every shape a tool call or a seat verb is answered on: a module's own namespace, a seat's, and
|
|
// the caller-named `call` kind naming any caller — which calledSubscribe would add if a `tool`
|
|
// grant ever reached Subscribe.
|
|
"mesh.mod.mesh-mail.tool.send",
|
|
"mesh.mod.mesh-mail.tool.>",
|
|
"mesh.seat.operator-channel.tool.tell",
|
|
"mesh.seat.operator-channel.call.tell.*",
|
|
"mesh.seat.operator-channel.call.tell.mesh-controller",
|
|
} {
|
|
if allowed(got.Subscribe, s) {
|
|
t.Errorf("a process account may subscribe %s, and could answer a call or a verb", s)
|
|
}
|
|
}
|
|
// No warrant: only operator-channel's holder publishes the asker's decided event (ADR 0259 §3).
|
|
if allowed(got.Publish, "mesh.seat.operator-channel.event.decided.mesh-mail") {
|
|
t.Error("a process account may publish a warrant")
|
|
}
|
|
// No seat held: the holder-only subjects (its accept queue, its worker) are refused.
|
|
for _, s := range []string{
|
|
"mesh.seat.operator-channel.accept.ask.mesh-mail",
|
|
"$JS.API.CONSUMER.MSG.NEXT.SEAT_OPERATOR_CHANNEL.SEAT_OPERATOR_CHANNEL_worker",
|
|
} {
|
|
if allowed(got.Subscribe, s) || allowed(got.Publish, s) {
|
|
t.Errorf("a process account holds operator-channel: %s", s)
|
|
}
|
|
}
|
|
// No restricted stream: a process never hears a module stream at all (it has no Hears), restricted or
|
|
// not — the generic rule ADR 0376 §1 holds for it as it does for the tool runner.
|
|
if allowed(got.Subscribe, "mesh.stream.mesh-mail.event.mail.mesh.message.added") {
|
|
t.Error("a process account hears a module's stream")
|
|
}
|
|
if got.Subscribe == nil || len(got.Subscribe) != 1 {
|
|
t.Fatalf("a process account's whole subscribe list: %v; want only its own inbox", got.Subscribe)
|
|
}
|
|
}
|
|
|
|
// Every process account composed for a mesh is held to the same rule (ADR 0436 clause 3): checked across
|
|
// the whole shape Users() composes, not only the one fixture above.
|
|
func TestEveryComposedProcessAccountAnswersNothing(t *testing.T) {
|
|
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
|
|
{Module: "messenger", Holds: []Seat{operatorChannel()}, RunsAs: "messenger"},
|
|
{Module: "mesh-mail", RunsAs: "mesh-mail", Uses: []Seat{operatorChannel()},
|
|
Processes: []ProcessPrincipal{{Process: "send", Emits: []string{"mail.send.sent"},
|
|
Asks: true, Invokes: []string{"seat:operator-channel.tell"}}}},
|
|
}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checked := 0
|
|
for _, u := range users {
|
|
if u.Kind != KindProcess {
|
|
continue
|
|
}
|
|
checked++
|
|
got := perms(t, u)
|
|
for _, s := range []string{
|
|
"mesh.seat.operator-channel.call.tell.*", // answers any caller
|
|
"mesh.seat.operator-channel.accept.ask." + u.Module, // a caller's own submit, never a process's
|
|
"mesh.seat.operator-channel.event.decided." + u.Module, // the holder's warrant alone
|
|
} {
|
|
if allowed(got.Subscribe, s) || allowed(got.Publish, s) {
|
|
t.Errorf("%s may reach %s, which only a holder or an asking module's own principal may",
|
|
u.Username(), s)
|
|
}
|
|
}
|
|
}
|
|
if checked == 0 {
|
|
t.Fatal("no process account was composed to check")
|
|
}
|
|
}
|
|
|
|
func equalStringSlices(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, x := range a {
|
|
seen[x] = true
|
|
}
|
|
for _, x := range b {
|
|
if !seen[x] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|