Grant a bus process an account scoped to its own declaration (issue 1180)

This commit is contained in:
jochen
2026-10-19 02:44:38 +02:00
parent de651e0fd1
commit 93a82cb032
5 changed files with 367 additions and 2 deletions
+218
View File
@@ -0,0 +1,218 @@
package broker
import "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)
for _, s := range []string{
"mesh.mod.mesh-mail.event.mail.send.sent",
"mesh.mod.mesh-mail.event.mail.send.failed",
"$JS.API.CONSUMER.INFO.MAIL_SENDS.novox_mesh-mail_send",
"$JS.API.CONSUMER.MSG.NEXT.MAIL_SENDS.novox_mesh-mail_send",
"$JS.ACK.MAIL_SENDS.novox_mesh-mail_send.x",
"$KV.mesh-mail_sends.x",
"$JS.API.DIRECT.GET.KV_mesh-mail_sends.$KV.mesh-mail_sends.x",
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-mail.a1",
"mesh.seat.operator-channel.tool.tell",
"mesh.seat.operator-channel.call.tell.novox~mesh-mail~send",
} {
if !allowed(got.Publish, s) {
t.Errorf("the process may not publish %s", s)
}
}
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
}