diff --git a/internal/broker/config_owner_live_test.go b/internal/broker/config_owner_live_test.go index 5a5cd72b..6d364778 100644 --- a/internal/broker/config_owner_live_test.go +++ b/internal/broker/config_owner_live_test.go @@ -160,3 +160,87 @@ func TestNatsOnlyMeshConfigsAccountPutsToItsBuckets(t *testing.T) { } } } + +// **The controller asks mesh-config's `config` in its own name** (issue 925): on a real server whose users are +// composed from records that hold mesh-config on novox serving `config`, the controller's request on +// `mesh.seat.mesh-config.call.config.controller` reaches mesh-config's account and its reply reaches the +// controller's inbox; the same request under another caller's name is refused by the server. +func TestNatsTheControllerAsksMeshConfigInItsOwnName(t *testing.T) { + hash, err := bcrypt.GenerateFromPassword([]byte("pw"), bcrypt.MinCost) + if err != nil { + t.Fatal(err) + } + seat := Seat{Name: "mesh-config", Scope: "mesh", Serves: []string{"config"}} + users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{ + "novox": {{Module: RuntimeModule}, {Module: "mesh-config", RunsAs: "mesh-config", Holds: []Seat{seat}}}, + }}) + if err != nil { + t.Fatal(err) + } + for i := range users { + users[i].PasswordHash = string(hash) + } + accounts, err := ComposeAccounts(users) + 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\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") + } + t.Cleanup(s.Shutdown) + connect := func(user string) (*nats.Conn, chan error) { + t.Helper() + refused := make(chan error, 8) + nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, "pw"), nats.CustomInboxPrefix("_INBOX."+user), + nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { refused <- err })) + if err != nil { + t.Fatalf("%s cannot connect: %v", user, err) + } + t.Cleanup(nc.Close) + return nc, refused + } + owner, _ := connect("novox.mesh-config") + subject := CalledSubject("mesh.seat.mesh-config.tool.config", ControllerName) + if subject != "mesh.seat.mesh-config.call.config.controller" { + t.Fatalf("the subject is %s", subject) + } + if _, err := owner.Subscribe("mesh.seat.mesh-config.call.config.*", func(m *nats.Msg) { + _ = m.Respond([]byte(`{"ok":true}`)) + }); err != nil { + t.Fatal(err) + } + if err := owner.Flush(); err != nil { + t.Fatal(err) + } + controller, refused := connect(ControllerName) + reply, err := controller.Request(subject, []byte(`{"action":"set"}`), 5*time.Second) + if err != nil || string(reply.Data) != `{"ok":true}` { + t.Fatalf("the controller's own-name request got %v (%v), want mesh-config's reply", reply, err) + } + if err := controller.Publish(CalledSubject("mesh.seat.mesh-config.tool.config", "anchor.node-tools"), nil); err != nil { + t.Fatal(err) + } + _ = controller.Flush() + select { + case err := <-refused: + 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 the controller a request in another's name") + } +}