Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83a298e7e0 |
+11
-6
@@ -266,9 +266,13 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
for _, e := range p.Emits {
|
||||
pub = append(pub, own+".event."+e)
|
||||
}
|
||||
for _, t := range p.Serves {
|
||||
sub = append(sub, own+".tool."+t)
|
||||
}
|
||||
// Every tool under its own name, not a list: the tools a module serves are what its code
|
||||
// answers, and a second copy of that list in the manifest would be a second source of
|
||||
// truth for the mesh to keep in step (2026-09-28: every module that served a tool was
|
||||
// refused the subscription, because none had written the list twice). Nothing is given
|
||||
// away — no other principal may subscribe this namespace, and a caller's authority is
|
||||
// still granted per tool, by name, on the publish side.
|
||||
sub = append(sub, own+".tool.>")
|
||||
|
||||
// 2. What it consumes, by the emitter's own subject — an event is addressed to its
|
||||
// emitter, because the emitter's identity is the meaning (ADR 0118).
|
||||
@@ -348,9 +352,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
return Permissions{
|
||||
Publish: pub,
|
||||
Subscribe: sub,
|
||||
// Only something that serves is ever answering. A pure consumer is granted nothing here.
|
||||
AllowResponses: p.Kind == KindModule && (len(p.Serves) > 0 || len(p.Holds) > 0) ||
|
||||
p.Kind == KindController,
|
||||
// A module answers what it was asked — a tool call reaches it on its own namespace, so the
|
||||
// authority is bounded by having been asked — and so does the controller. A node and a
|
||||
// person are never asked anything, and are granted nothing here.
|
||||
AllowResponses: p.Kind == KindModule || p.Kind == KindController,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -89,17 +90,30 @@ func TestAnInboxIsScopedToItsOwner(t *testing.T) {
|
||||
}
|
||||
|
||||
// A responder answers on the caller's inbox, which it has no permission for. allow_responses is
|
||||
// what makes a scoped inbox workable at all — the authority is bounded by having been asked.
|
||||
func TestOnlySomethingThatServesMayAnswer(t *testing.T) {
|
||||
serving, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "billing",
|
||||
Serves: []string{"status"}, PasswordHash: "x"})
|
||||
if !serving.AllowResponses {
|
||||
t.Fatal("a module serving a tool cannot answer the caller's inbox")
|
||||
}
|
||||
consumer, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit",
|
||||
// what makes a scoped inbox workable at all — the authority is bounded by having been asked. A
|
||||
// module is asked on its own namespace and may answer; a node and a person are never asked.
|
||||
func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) {
|
||||
module, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit",
|
||||
Consumes: []string{"shop.order.placed"}, PasswordHash: "x"})
|
||||
if consumer.AllowResponses {
|
||||
t.Fatal("a pure consumer was granted the right to answer, which nothing asked it to do")
|
||||
if !module.AllowResponses {
|
||||
t.Fatal("a module cannot answer a tool call on its own namespace")
|
||||
}
|
||||
node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"})
|
||||
if node.AllowResponses {
|
||||
t.Fatal("a node was granted the right to answer, and nothing asks a node anything")
|
||||
}
|
||||
}
|
||||
|
||||
// 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"})
|
||||
if !slices.Contains(p.Subscribe, "mesh.mod.gitea.tool.>") {
|
||||
t.Fatalf("a module may not serve its own tools: %v", p.Subscribe)
|
||||
}
|
||||
for _, s := range p.Subscribe {
|
||||
if strings.HasPrefix(s, "mesh.mod.") && !strings.HasPrefix(s, "mesh.mod.gitea.") {
|
||||
t.Fatalf("a module may subscribe another's namespace: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -38,16 +38,18 @@ accounts {
|
||||
} }
|
||||
{ user: "one.telegram", password: "$2a$11$tttttttttttttttttttttt", permissions: {
|
||||
publish: { allow: ["$JS.ACK.EVENTS.one_telegram.>", "$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.>", "$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "mesh.seat.telegram-sender.event.delivered", "mesh.seat.telegram-sender.event.failed"] }
|
||||
subscribe: { allow: ["_DELIVER.SEAT_TELEGRAM_SENDER_worker", "_DELIVER.one_telegram", "_INBOX.one.telegram.>", "mesh.mod.telegram.tool.status", "mesh.seat.telegram-sender.accept.send"] }
|
||||
subscribe: { allow: ["_DELIVER.SEAT_TELEGRAM_SENDER_worker", "_DELIVER.one_telegram", "_INBOX.one.telegram.>", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
{ user: "two.audit", password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa", permissions: {
|
||||
publish: { allow: ["$JS.ACK.EVENTS.two_audit.>"] }
|
||||
subscribe: { allow: ["_DELIVER.two_audit", "_INBOX.two.audit.>", "mesh.mod.shop.event.order.placed"] }
|
||||
subscribe: { allow: ["_DELIVER.two_audit", "_INBOX.two.audit.>", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
{ user: "two.shop", password: "$2a$11$ssssssssssssssssssssss", permissions: {
|
||||
publish: { allow: ["$JS.ACK.EVENTS.two_shop.>", "mesh.mod.shop.event.order.placed", "mesh.seat.telegram-sender.accept.send"] }
|
||||
subscribe: { allow: ["_DELIVER.two_shop", "_INBOX.two.shop.>"] }
|
||||
subscribe: { allow: ["_DELIVER.two_shop", "_INBOX.two.shop.>", "mesh.mod.shop.tool.>"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user