From b1f758ed71e339d5759bfacd30b5ea6d0aa243ad Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 01:42:28 +0200 Subject: [PATCH] Grant each node-engine its own access-act ask, which it has subscribed to since ADR 0325 and was refused at every connect (issue 1945) --- internal/broker/nats.go | 9 ++++++++- internal/broker/nats_test.go | 11 +++++++++++ internal/broker/testdata/composed.conf | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/broker/nats.go b/internal/broker/nats.go index 9b93db72..2add5851 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -526,6 +526,11 @@ func AskJudgeDirectoriesSubject(node string) string { // for the same reason. func AskMoveSecretSubject(node string) string { return "mesh.node." + node + ".ask.move-secret" } +// AskAccessActSubject is where the controller asks one machine's node-engine to run an act on one access's folder (novox/hq +// ADR 0328 §7): a request answered once, signed as a hand-over is (the node-engine's link.AccessActContext). The +// node-engine has subscribed to it since ADR 0325; the grant was missing, so each connect logged a permissions violation. +func AskAccessActSubject(node string) string { return "mesh.node." + node + ".ask.access-act" } + // inbox is a principal's own reply space. No user is ever granted a bare `_INBOX.>` (design 25 // §4): with one account, inbox privacy is the permission list or it is nothing, so each user's // inbox is derived from its own identity and its permissions name that prefix and no other. @@ -841,7 +846,9 @@ func PermissionsFor(p Principal) (Permissions, error) { AskJudgeDirectoriesSubject(p.Node), // And asking it to move a value sealed to it into the vault (design 57's V3, issue 676), answered the // same way. - AskMoveSecretSubject(p.Node)} + AskMoveSecretSubject(p.Node), + // And asking it to run an act on an access's folder (novox/hq ADR 0328 §7), answered the same way. + AskAccessActSubject(p.Node)} // The node-engine witnesses the core builds it places (novox/hq to-be 45 §8, ADR 0236; the // contract is lease/witness.go): it asks its own machine's node tools PING, and, where the // machine runs the controller, reads the lease's one key — read, never written. diff --git a/internal/broker/nats_test.go b/internal/broker/nats_test.go index bc2695c2..c2bea94e 100644 --- a/internal/broker/nats_test.go +++ b/internal/broker/nats_test.go @@ -125,6 +125,17 @@ func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) { } } +// The node-engine hears the act on an access's folder on its own ask subject (novox/hq ADR 0328 §7), which it has +// subscribed to since ADR 0325: without the grant every node-engine logs a permissions violation at every connect, and +// the act could never reach it. Its own node's, and no other's. +func TestANodeMayHearTheAccessActAskedOfIt(t *testing.T) { + node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"}) + if !slices.Contains(node.Subscribe, "mesh.node.one.ask.access-act") || + slices.Contains(node.Subscribe, "mesh.node.two.ask.access-act") { + t.Fatalf("the node's subscriptions: %v", node.Subscribe) + } +} + // A module serves every tool under its own name, and no other module's. func TestAModuleServesItsOwnNamespaceAndNoOthers(t *testing.T) { p, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "gitea", PasswordHash: "x"}) diff --git a/internal/broker/testdata/composed.conf b/internal/broker/testdata/composed.conf index b64cf1f5..0cc3eab2 100644 --- a/internal/broker/testdata/composed.conf +++ b/internal/broker/testdata/composed.conf @@ -39,7 +39,7 @@ accounts { } } { user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: { publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one"] } - subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.restic.event.backed-up", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] } + subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.restic.event.backed-up", "mesh.node.one.ask.access-act", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] } allow_responses: { max: 1, ttl: "1m" } } } { user: "one.mesh-delivery-watchman", password: "$2a$11$wwwwwwwwwwwwwwwwwwwwww", permissions: {