Files
mesh-controller/internal/broker/callers_live_test.go
T
jschoubben 9bed7d6398 broker: grant each credential the tool calls that name it as the caller, and no other (hq issue 365)
Every grant to call a tool is granted again under the call kind, with the
credential's own bus user as the last token, and every grant to answer one is
granted for calls naming any caller: the caller of a tool call becomes a fact
the bus enforces, as ADR 0259 section 3 made the asker of an ask. A kind of its
own because every existing tool grant is a wildcard that would match any caller
appended. The old tool grants stay for one release so nothing loses its way to a
tool (hq issue 464); the controller's own grants move with that issue, since
they are also the installer's first user list.
2026-10-11 05:25:29 +02:00

101 lines
3.2 KiB
Go

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):
}
}