Merge pull request 'broker: each credential may call tools only in its own name on the caller-named subjects (hq issue 365)' (#224) from fix/365-a-tool-call-names-its-caller into main
This commit was merged in pull request #224.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A tool call names its caller (novox/hq issue 365, by ADR 0259 §3's precedent for asks).
|
||||
//
|
||||
// mesh.mod.<module>.call.<tool>[.<node>].<caller>
|
||||
// mesh.seat.<seat>.call.<verb>[.<node>].<caller>
|
||||
//
|
||||
// The last token is the bus user that published the call, and each user is granted these subjects with its own
|
||||
// name there and no other — the caller is a fact the server enforces, and the tool runtime hands it to the
|
||||
// module from the subject a call arrived on (mesh-tools node-tools internal/bus caller.go holds the same shape).
|
||||
//
|
||||
// **A kind of its own, `call`, not the `tool` subject with a token appended.** Every grant to call a tool is a
|
||||
// wildcard over the `tool` kind — `.tool.<t>.*` for the instance on any machine, `mesh.mod.*.tool.>` for every
|
||||
// tool — and either would match a `tool` subject with any caller appended, so a caller could name another.
|
||||
// Under `call` nothing is granted but the caller-named subjects. No stream's filter covers it: a tool call is
|
||||
// never persisted (design 25 §3).
|
||||
//
|
||||
// **Derived from the `tool` grants, in one place** (callerNamed): wherever a principal may call or answer a
|
||||
// tool, it may call it in its own name, or answer it naming any caller — so no kind of principal is left
|
||||
// unable to call in its own name, and a new grant to call is one in both shapes by construction.
|
||||
//
|
||||
// The `tool` grants stay beside these for one release, so every caller and every runtime moves without a gap:
|
||||
// their retirement is hq issue 464.
|
||||
const CallKind = "call"
|
||||
|
||||
var userName = regexp.MustCompile(`^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$`)
|
||||
|
||||
// CallerToken is a bus user's name as the last token of a call it publishes: each dot written `~`, which no part
|
||||
// of a user's name may hold (safeSubject), so the token names that user and no other. "" for a name that is not
|
||||
// a user's.
|
||||
func CallerToken(user string) string {
|
||||
if !userName.MatchString(user) {
|
||||
return ""
|
||||
}
|
||||
return strings.ReplaceAll(user, ".", "~")
|
||||
}
|
||||
|
||||
// toolGrant splits a grant on the `tool` kind — `mesh.mod.<m>.tool.<rest>` or `mesh.seat.<s>.tool.<rest>`, any
|
||||
// part possibly a wildcard — at the kind; ok is false for any other subject.
|
||||
func toolGrant(subject string) (head, rest string, ok bool) {
|
||||
parts := strings.SplitN(subject, ".", 5)
|
||||
if len(parts) != 5 || parts[0] != "mesh" || (parts[1] != "mod" && parts[1] != "seat") || parts[3] != "tool" ||
|
||||
parts[2] == "" || parts[4] == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[0] + "." + parts[1] + "." + parts[2], parts[4], true
|
||||
}
|
||||
|
||||
// CalledSubject is where a call to a tool subject goes naming its caller; "" when the subject is not a tool's
|
||||
// or the user is not a bus user.
|
||||
func CalledSubject(subject, user string) string {
|
||||
head, rest, ok := toolGrant(subject)
|
||||
token := CallerToken(user)
|
||||
if !ok || token == "" || strings.ContainsAny(rest, "*>") {
|
||||
return ""
|
||||
}
|
||||
return head + "." + CallKind + "." + rest + "." + token
|
||||
}
|
||||
|
||||
// CalledPattern is what a holder answering a tool subject also subscribes, to hear the calls that name their
|
||||
// caller: the same address under the `call` kind, any caller last. "" for a subject that is not a tool's.
|
||||
func CalledPattern(subject string) string {
|
||||
head, rest, ok := toolGrant(subject)
|
||||
if !ok || strings.ContainsAny(rest, "*>") {
|
||||
return ""
|
||||
}
|
||||
return head + "." + CallKind + "." + rest + ".*"
|
||||
}
|
||||
|
||||
// calledPublish is a grant to call on the `tool` kind, as the same grant in the caller's own name: its last
|
||||
// token the caller's, and nothing that reaches past it. A `>` is every tail a call carries — the tool alone or
|
||||
// the tool and the machine — so it becomes both, each ending in the caller.
|
||||
func calledPublish(grant, token string) []string {
|
||||
head, rest, ok := toolGrant(grant)
|
||||
if !ok || token == "" {
|
||||
return nil
|
||||
}
|
||||
base := head + "." + CallKind + "."
|
||||
switch {
|
||||
case rest == ">":
|
||||
return []string{base + "*." + token, base + "*.*." + token}
|
||||
case strings.HasSuffix(rest, ".>"):
|
||||
return []string{base + strings.TrimSuffix(rest, ">") + "*." + token}
|
||||
}
|
||||
return []string{base + rest + "." + token}
|
||||
}
|
||||
|
||||
// calledSubscribe is a grant to answer on the `tool` kind, as the same grant for the calls naming any caller.
|
||||
func calledSubscribe(grant string) []string {
|
||||
head, rest, ok := toolGrant(grant)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
base := head + "." + CallKind + "."
|
||||
if strings.HasSuffix(rest, ">") {
|
||||
return []string{base + rest}
|
||||
}
|
||||
return []string{base + rest + ".*"}
|
||||
}
|
||||
|
||||
// callerNamed adds, beside a principal's grants on the `tool` kind, the same grants under `call`: to publish in
|
||||
// its own name, to subscribe naming anybody.
|
||||
func callerNamed(user string, pub, sub []string) ([]string, []string) {
|
||||
token := CallerToken(user)
|
||||
for _, g := range pub {
|
||||
pub = append(pub, calledPublish(g, token)...)
|
||||
}
|
||||
for _, g := range sub {
|
||||
sub = append(sub, calledSubscribe(g)...)
|
||||
}
|
||||
return unique(pub), unique(sub)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// **On a real server, as composed** (novox/hq issue 365): a machine's runtime calls in its own name, and the
|
||||
// server refuses it a call naming another — the grant, not the runtime, is what makes the caller a fact.
|
||||
func TestAServerComposedFromTheGrantsRefusesACallNamingAnother(t *testing.T) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("pw"), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ledger := Seat{Name: "issue-tracker", Scope: "mesh", Serves: []string{"open"}}
|
||||
accounts, err := ComposeAccounts([]Principal{
|
||||
{Kind: KindNodeTools, Node: "novox", Module: RuntimeModule, PasswordHash: string(hash),
|
||||
Carries: []Declared{{Module: "mesh-issues", Holds: []Seat{ledger}}}},
|
||||
{Kind: KindNodeTools, Node: "shanks", Module: RuntimeModule, PasswordHash: string(hash)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf := filepath.Join(t.TempDir(), "bus.conf")
|
||||
if err := os.WriteFile(conf, []byte("listen: 127.0.0.1:-1\njetstream { store_dir: "+
|
||||
`"`+t.TempDir()+`"`+" }\n"+accounts), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
opts, err := server.ProcessConfigFile(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("the composed accounts do not parse: %v", err)
|
||||
}
|
||||
opts.NoLog, opts.NoSigs = true, true
|
||||
s, err := server.NewServer(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go s.Start()
|
||||
if !s.ReadyForConnections(10 * time.Second) {
|
||||
t.Fatal("the bus did not come up")
|
||||
}
|
||||
defer s.Shutdown()
|
||||
|
||||
holder, err := nats.Connect(s.ClientURL(), nats.UserInfo("novox.node-tools", "pw"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer holder.Close()
|
||||
heard := make(chan string, 4)
|
||||
sub, err := holder.Subscribe("mesh.seat.issue-tracker.call.open.*", func(m *nats.Msg) {
|
||||
heard <- m.Subject
|
||||
_ = m.Respond([]byte(`{"result":{}}`))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = holder.Flush()
|
||||
if !sub.IsValid() {
|
||||
t.Fatal("the holder may not hear the caller-named calls to its seat")
|
||||
}
|
||||
|
||||
refusals := make(chan error, 4)
|
||||
caller, err := nats.Connect(s.ClientURL(), nats.UserInfo("shanks.node-tools", "pw"),
|
||||
nats.CustomInboxPrefix("_INBOX.shanks.node-tools"),
|
||||
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { refusals <- err }))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer caller.Close()
|
||||
if _, err := caller.Request("mesh.seat.issue-tracker.call.open.shanks~node-tools", []byte(`{}`), 3*time.Second); err != nil {
|
||||
t.Fatalf("a call in the caller's own name was not answered: %v", err)
|
||||
}
|
||||
if got := <-heard; got != "mesh.seat.issue-tracker.call.open.shanks~node-tools" {
|
||||
t.Fatalf("heard %s", got)
|
||||
}
|
||||
if err := caller.Publish("mesh.seat.issue-tracker.call.open.novox~node-tools", []byte(`{}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = caller.Flush()
|
||||
select {
|
||||
case err := <-refusals:
|
||||
if !errors.Is(err, nats.ErrPermissionViolation) {
|
||||
t.Errorf("the server said %v, want a permissions violation", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("the server did not refuse a call naming another caller")
|
||||
}
|
||||
select {
|
||||
case got := <-heard:
|
||||
t.Errorf("a call naming another reached the holder: %s", got)
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **A tool call names its caller, and the bus lets each user name itself alone** (novox/hq issue 365, by ADR
|
||||
// 0259 §3's precedent for asks): wherever a principal may call a tool, it may call it on the caller-named
|
||||
// subject — kind `call`, its own bus user last, dots written `~` — and on no subject naming anybody else.
|
||||
func TestEachCredentialMayCallOnlyInItsOwnName(t *testing.T) {
|
||||
ledger := Seat{Name: "node-desk", Scope: "node", Serves: []string{"who"}}
|
||||
tracker := Seat{Name: "issue-tracker", Scope: "mesh", Serves: []string{"open"}}
|
||||
for _, c := range []struct {
|
||||
p Principal
|
||||
may []string
|
||||
mayNot []string
|
||||
subject []string // what it subscribes, to answer calls naming any caller
|
||||
}{
|
||||
{p: Principal{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}},
|
||||
may: []string{"mesh.mod.ledger.call.who.person~jochen", "mesh.mod.ledger.call.who.anchor.person~jochen",
|
||||
"mesh.seat.issue-tracker.call.open.person~jochen", "mesh.seat.node-desk.call.who.anchor.person~jochen"},
|
||||
mayNot: []string{"mesh.mod.ledger.call.who.shanks~node-tools", "mesh.mod.ledger.call.who.anchor.controller",
|
||||
"mesh.seat.issue-tracker.call.open.person~somebody", "mesh.mod.ledger.call.who.person"}},
|
||||
{p: Principal{Kind: KindNodeTools, Node: "shanks", Module: RuntimeModule,
|
||||
Carries: []Declared{{Module: "ledger", Holds: []Seat{ledger, tracker}}}},
|
||||
may: []string{"mesh.mod.ledger.call.who.shanks~node-tools", "mesh.seat.issue-tracker.call.open.shanks~node-tools"},
|
||||
mayNot: []string{"mesh.mod.ledger.call.who.novox~node-tools", "mesh.seat.issue-tracker.call.open.controller"},
|
||||
subject: []string{"mesh.mod.ledger.call.who.novox~node-tools", "mesh.seat.node-desk.call.who.shanks.person~jochen", "mesh.seat.issue-tracker.call.open.controller"}},
|
||||
{p: Principal{Kind: KindModule, Node: "two", Module: "shop", Invokes: []string{"ledger.who", "seat:issue-tracker.open"}},
|
||||
may: []string{"mesh.mod.ledger.call.who.two~shop", "mesh.mod.ledger.call.who.anchor.two~shop",
|
||||
"mesh.seat.issue-tracker.call.open.two~shop"},
|
||||
mayNot: []string{"mesh.mod.ledger.call.other.two~shop", "mesh.mod.ledger.call.who.one~shop",
|
||||
"mesh.seat.issue-tracker.call.open.one~telegram"}},
|
||||
{p: Principal{Kind: KindNode, Node: "one", Checks: []string{"ledger.health"}},
|
||||
may: []string{"mesh.mod.ledger.call.health.one.node~one"},
|
||||
mayNot: []string{"mesh.mod.ledger.call.health.two.node~one", "mesh.mod.ledger.call.health.one.node~two"}},
|
||||
{p: Principal{Kind: KindModule, Node: "one", Module: "ledger", Holds: []Seat{ledger, tracker}},
|
||||
subject: []string{"mesh.mod.ledger.call.who.person~jochen", "mesh.mod.ledger.call.who.one.controller",
|
||||
"mesh.seat.node-desk.call.who.one.two~shop", "mesh.seat.issue-tracker.call.open.two~shop"}},
|
||||
} {
|
||||
perms, err := PermissionsFor(c.p)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.p.Username(), err)
|
||||
}
|
||||
for _, s := range c.may {
|
||||
if !MayPublish(perms, s) {
|
||||
t.Errorf("%s may not call %s, in its own name", c.p.Username(), s)
|
||||
}
|
||||
}
|
||||
for _, s := range c.mayNot {
|
||||
if MayPublish(perms, s) {
|
||||
t.Errorf("%s may call %s, naming somebody else", c.p.Username(), s)
|
||||
}
|
||||
}
|
||||
for _, s := range c.subject {
|
||||
if !MaySubscribe(perms, s) {
|
||||
t.Errorf("%s does not hear %s, a call to what it serves", c.p.Username(), s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The subjects that name no caller stay granted beside the caller-named ones for one release, so a caller
|
||||
// moves over without a gap (their retirement: hq issue 464).
|
||||
func TestTheSubjectsThatNameNoCallerStayGrantedForOneRelease(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "two", Module: "shop", Invokes: []string{"ledger.who"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range []string{"mesh.mod.ledger.tool.who", "mesh.mod.ledger.tool.who.anchor"} {
|
||||
if !MayPublish(perms, s) {
|
||||
t.Errorf("the old subject %s is no longer granted", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The desk's hidden prompt is the controller's alone on the caller-named subjects too (the review of
|
||||
// 2026-10-09, M4): a grant of every tool does not reach it there either.
|
||||
func TestTheDesksPromptIsTheControllersAloneUnderCallToo(t *testing.T) {
|
||||
for _, p := range []Principal{{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}},
|
||||
{Kind: KindNodeTools, Node: "shanks", Module: RuntimeModule}} {
|
||||
perms, err := PermissionsFor(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token := CallerToken(p.Username())
|
||||
for _, s := range []string{"mesh.seat.node-launcher.call.secret.shanks." + token,
|
||||
"mesh.mod.shell.call.node-launcher.secret.shanks." + token, "mesh.mod.shell.call.node-launcher.secret." + token} {
|
||||
if MayPublish(perms, s) {
|
||||
t.Errorf("%s may publish %s, the desk's hidden prompt", p.Username(), s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The controller's grants are the installer's first user list too, so they move with hq issue 464: this release
|
||||
// it calls and serves on the subjects that name no caller alone, and nobody may call in its name.
|
||||
func TestTheControllerKeepsTheSubjectsThatNameNoCallerThisRelease(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindController})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range append(perms.Publish, perms.Subscribe...) {
|
||||
if strings.Contains(s, "."+CallKind+".") {
|
||||
t.Errorf("the controller is granted %s, which the installer's first user list does not carry", s)
|
||||
}
|
||||
}
|
||||
for _, p := range []Principal{{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}},
|
||||
{Kind: KindNodeTools, Node: "shanks", Module: RuntimeModule}} {
|
||||
perms, err := PermissionsFor(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if MayPublish(perms, "mesh.mod.ledger.call.who.controller") || MayPublish(perms, "mesh.seat.mesh-controller.call.status.controller") {
|
||||
t.Errorf("%s may call in the controller's name", p.Username())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,9 @@ func TestInvokingGrantsNothingButTheCall(t *testing.T) {
|
||||
if strings.Contains(p, ".event.") {
|
||||
t.Errorf("a module that only invokes may publish %q, an event it never declared", p)
|
||||
}
|
||||
// A role's tools are tools (ADR 0132); a role's work queue and events are not.
|
||||
if strings.HasPrefix(p, "mesh.seat.") && !strings.Contains(p, ".tool.") {
|
||||
// A role's tools are tools (ADR 0132), named by their caller or not (novox/hq issue 365); a role's work
|
||||
// queue and events are not.
|
||||
if strings.HasPrefix(p, "mesh.seat.") && !strings.Contains(p, ".tool.") && !strings.Contains(p, ".call.") {
|
||||
t.Errorf("a module that only invokes may publish %q, a seat it neither holds nor uses", p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,10 @@ func ControllerOnly() []string {
|
||||
for _, base := range []string{"mesh.seat." + v.Seat + ".tool." + v.Verb, "mesh.mod.*.tool." + v.Seat + "." + v.Verb} {
|
||||
out = append(out, base, base+".*")
|
||||
}
|
||||
// And where a call names its caller (novox/hq issue 365): any machine, any caller.
|
||||
for _, base := range []string{"mesh.seat." + v.Seat + "." + CallKind + "." + v.Verb, "mesh.mod.*." + CallKind + "." + v.Seat + "." + v.Verb} {
|
||||
out = append(out, base+".>")
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -875,6 +879,19 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
pub = append(pub, "$JS.ACK."+consumerStream(p)+"."+consumerDurable(p)+".>")
|
||||
}
|
||||
|
||||
// **And every tool grant again, naming its caller** (novox/hq issue 365): a call in this principal's own
|
||||
// name, and an answer to a call naming anybody. The `tool` grants above stay for one release beside these,
|
||||
// so callers and runtimes move without a gap; their retirement is hq issue 464.
|
||||
//
|
||||
// **Not the controller's, this release.** Its grants are also the installer's first user list
|
||||
// (TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose), judged against the node-engine the mesh
|
||||
// runs, so a change to them waits on a node-engine delivery. It calls and serves on the subjects that name
|
||||
// no caller meanwhile — a caller-named call to its seat reaches nobody and is asked again on those at once —
|
||||
// and moves with issue 464.
|
||||
if p.Kind != KindController {
|
||||
pub, sub = callerNamed(p.Username(), pub, sub)
|
||||
}
|
||||
|
||||
sort.Strings(pub)
|
||||
sort.Strings(sub)
|
||||
// One writer per piece of state (novox/hq to-be 45 §1): a grant that would make a second is
|
||||
|
||||
@@ -240,9 +240,9 @@ func TestAPersonReachesNothingButTools(t *testing.T) {
|
||||
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||
for _, p := range perms.Publish {
|
||||
// A tool call, or asking what answers (novox/hq ADR 0197) — a question every service
|
||||
// answers about itself, which claims nothing and controls nothing.
|
||||
if !strings.Contains(p, ".tool.") && !strings.HasPrefix(p, "$SRV.") {
|
||||
// A tool call — named by its caller or not (novox/hq issue 365) — or asking what answers (novox/hq
|
||||
// ADR 0197), a question every service answers about itself, which claims nothing and controls nothing.
|
||||
if !strings.Contains(p, ".tool.") && !strings.Contains(p, ".call.") && !strings.HasPrefix(p, "$SRV.") {
|
||||
t.Errorf("a person may publish %q, which is not a tool call", p)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -43,17 +43,17 @@ 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.EVENTS.one_telegram", "$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram", "$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram", "mesh.seat.telegram-sender.event.delivered", "mesh.seat.telegram-sender.event.failed"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "$SRV.STATS", "$SRV.STATS.telegram", "$SRV.STATS.telegram.>", "_INBOX.one.telegram.>", "mesh.assignment.one.telegram", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "$SRV.STATS", "$SRV.STATS.telegram", "$SRV.STATS.telegram.>", "_INBOX.one.telegram.>", "mesh.assignment.one.telegram", "mesh.mod.telegram.call.>", "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.>", "$JS.API.CONSUMER.INFO.EVENTS.two_audit", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_audit", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.audit"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "$SRV.STATS", "$SRV.STATS.audit", "$SRV.STATS.audit.>", "_INBOX.two.audit.>", "mesh.assignment.two.audit", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "$SRV.STATS", "$SRV.STATS.audit", "$SRV.STATS.audit.>", "_INBOX.two.audit.>", "mesh.assignment.two.audit", "mesh.mod.audit.call.>", "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.>", "$JS.API.CONSUMER.INFO.EVENTS.two_shop", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop", "mesh.mod.shop.event.order.placed", "mesh.seat.telegram-sender.accept.send"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "$SRV.STATS", "$SRV.STATS.shop", "$SRV.STATS.shop.>", "_INBOX.two.shop.>", "mesh.assignment.two.shop", "mesh.mod.shop.tool.>"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "$SRV.STATS", "$SRV.STATS.shop", "$SRV.STATS.shop.>", "_INBOX.two.shop.>", "mesh.assignment.two.shop", "mesh.mod.shop.call.>", "mesh.mod.shop.tool.>"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
]
|
||||
|
||||
@@ -44,15 +44,17 @@ func TestAPersonMayCallToolsAndNothingElse(t *testing.T) {
|
||||
}
|
||||
// The one tool, both ways it is addressed (novox/hq ADR 0159): to whichever instance
|
||||
// answers, and to the instance on one machine. Nothing else.
|
||||
// And asking what answers (novox/hq ADR 0197), which claims nothing and calls nothing.
|
||||
// And asking what answers (novox/hq ADR 0197), which claims nothing and calls nothing. Each way again
|
||||
// naming ada as the caller and nobody else (novox/hq issue 365).
|
||||
var tools []string
|
||||
for _, s := range perms.Publish {
|
||||
if !strings.HasPrefix(s, "$SRV.") {
|
||||
tools = append(tools, s)
|
||||
}
|
||||
}
|
||||
if len(tools) != 2 || tools[0] != "mesh.mod.mesh-catalog.tool.catalog_tools" ||
|
||||
tools[1] != "mesh.mod.mesh-catalog.tool.catalog_tools.*" {
|
||||
want := []string{"mesh.mod.mesh-catalog.call.catalog_tools.*.person~ada", "mesh.mod.mesh-catalog.call.catalog_tools.person~ada",
|
||||
"mesh.mod.mesh-catalog.tool.catalog_tools", "mesh.mod.mesh-catalog.tool.catalog_tools.*"}
|
||||
if strings.Join(tools, " ") != strings.Join(want, " ") {
|
||||
t.Errorf("ada may publish %v, which should be the one tool, both ways addressed, and nothing else", perms.Publish)
|
||||
}
|
||||
for _, s := range perms.Publish {
|
||||
|
||||
Reference in New Issue
Block a user