broker: each credential may call tools only in its own name on the caller-named subjects (hq issue 365) #224
@@ -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.") {
|
if strings.Contains(p, ".event.") {
|
||||||
t.Errorf("a module that only invokes may publish %q, an event it never declared", p)
|
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.
|
// A role's tools are tools (ADR 0132), named by their caller or not (novox/hq issue 365); a role's work
|
||||||
if strings.HasPrefix(p, "mesh.seat.") && !strings.Contains(p, ".tool.") {
|
// 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)
|
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} {
|
for _, base := range []string{"mesh.seat." + v.Seat + ".tool." + v.Verb, "mesh.mod.*.tool." + v.Seat + "." + v.Verb} {
|
||||||
out = append(out, base, base+".*")
|
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
|
return out
|
||||||
}
|
}
|
||||||
@@ -875,6 +879,19 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
pub = append(pub, "$JS.ACK."+consumerStream(p)+"."+consumerDurable(p)+".>")
|
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(pub)
|
||||||
sort.Strings(sub)
|
sort.Strings(sub)
|
||||||
// One writer per piece of state (novox/hq to-be 45 §1): a grant that would make a second is
|
// 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",
|
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
Invokes: []string{"*"}, PasswordHash: "x"})
|
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
for _, p := range perms.Publish {
|
for _, p := range perms.Publish {
|
||||||
// A tool call, or asking what answers (novox/hq ADR 0197) — a question every service
|
// A tool call — named by its caller or not (novox/hq issue 365) — or asking what answers (novox/hq
|
||||||
// answers about itself, which claims nothing and controls nothing.
|
// ADR 0197), a question every service answers about itself, which claims nothing and controls nothing.
|
||||||
if !strings.Contains(p, ".tool.") && !strings.HasPrefix(p, "$SRV.") {
|
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)
|
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: {
|
{ 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"] }
|
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" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
{ user: "two.audit", password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa", permissions: {
|
{ 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"] }
|
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" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
{ user: "two.shop", password: "$2a$11$ssssssssssssssssssssss", permissions: {
|
{ 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"] }
|
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" }
|
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
|
// 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.
|
// 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
|
var tools []string
|
||||||
for _, s := range perms.Publish {
|
for _, s := range perms.Publish {
|
||||||
if !strings.HasPrefix(s, "$SRV.") {
|
if !strings.HasPrefix(s, "$SRV.") {
|
||||||
tools = append(tools, s)
|
tools = append(tools, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(tools) != 2 || tools[0] != "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",
|
||||||
tools[1] != "mesh.mod.mesh-catalog.tool.catalog_tools.*" {
|
"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)
|
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 {
|
for _, s := range perms.Publish {
|
||||||
|
|||||||
Reference in New Issue
Block a user