Files
mesh-controller/internal/broker/vault_calls_live_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

228 lines
8.4 KiB
Go

package broker
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"golang.org/x/crypto/bcrypt"
)
// vaultRecords is a mesh of two machines: anchor, a desk whose runtime carries the desk's prompt (rofi holding
// node-launcher) beside an issuing provider (plex) and a module with a tool of its own; and novox, where the vault
// runs in a runtime of its own on its own account (ADR 0259 §8's runs-as).
func vaultRecords() Records {
launcher := Seat{Name: "node-launcher", Scope: "node", Serves: []string{"menu", "secret"}}
vault := Seat{Name: VaultSeat, Scope: "mesh", Serves: []string{"get", "accept", "secret-ask", "issued", "rotate"}}
return Records{Nodes: []string{"anchor", "novox"}, Assigned: map[string][]Declared{
"anchor": {{Module: RuntimeModule}, {Module: "rofi", Holds: []Seat{launcher}, NoAccount: true},
{Module: "plex", Serves: []string{"plex_issued_credential", "plex_status"},
Issued: []string{"plex_issued_credential"}, NoAccount: true},
{Module: "other", Serves: []string{"tool"}, NoAccount: true}},
"novox": {{Module: RuntimeModule}, {Module: "mesh-vault", Holds: []Seat{vault}, RunsAs: "mesh-vault"}},
}}
}
// The vault's holder may ask the desk's prompt and each issuing tool in its own name, and nothing else outside its
// seat; no other module and no runtime is granted it (issue 723).
func TestTheVaultAsksTheDeskAndTheIssuingToolsInItsOwnNameAlone(t *testing.T) {
users, err := Users(vaultRecords())
if err != nil {
t.Fatal(err)
}
perms := map[string]Permissions{}
for _, u := range users {
p, err := PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
perms[u.Username()] = p
}
vault, ok := perms["novox.mesh-vault"]
if !ok {
t.Fatal("the vault has no account of its own")
}
token := CallerToken("novox.mesh-vault")
for _, s := range []string{
"mesh.seat.node-launcher.call.secret.anchor." + token,
"mesh.mod.plex.call.plex_issued_credential.anchor." + token,
} {
if !MayPublish(vault, s) {
t.Errorf("the vault may not ask %s", s)
}
}
for _, s := range []string{
"mesh.seat.node-launcher.tool.secret.anchor",
"mesh.seat.node-launcher.call.secret.anchor.controller",
"mesh.seat.node-launcher.call.menu.anchor." + token,
"mesh.mod.rofi.call.node-launcher.secret.anchor." + token,
"mesh.mod.plex.call.plex_status.anchor." + token,
"mesh.mod.plex.tool.plex_issued_credential.anchor",
"mesh.mod.other.call.tool.anchor." + token,
"mesh.mod.other.tool.tool.anchor",
"mesh.seat.mesh-controller.call.status." + token,
} {
if MayPublish(vault, s) {
t.Errorf("the vault may publish %s", s)
}
}
// Outside its own namespace and its own seat, the vault publishes nothing but those two calls, its inbox's
// answers and the stream API's reads of what it keeps.
for _, s := range vault.Publish {
switch {
case strings.HasPrefix(s, "mesh.mod.mesh-vault."), strings.HasPrefix(s, "mesh.seat."+VaultSeat+"."),
s == "mesh.seat.node-launcher.call.secret.*."+token,
s == "mesh.mod.plex.call.plex_issued_credential.*."+token,
strings.HasPrefix(s, "$JS."), strings.HasPrefix(s, "$KV."), strings.HasPrefix(s, "_INBOX."),
strings.HasPrefix(s, "mesh.discover."), strings.HasPrefix(s, "$SRV."):
default:
t.Errorf("the vault is granted %s, outside its seat", s)
}
}
// Nobody else is granted the vault's calls; the controller keeps today's path until R1 (issue 682).
for name, p := range perms {
if name == "novox.mesh-vault" {
continue
}
for _, s := range []string{"mesh.seat.node-launcher.call.secret.anchor." + token,
"mesh.mod.plex.call.plex_issued_credential.anchor." + token} {
if MayPublish(p, s) {
t.Errorf("%s may call in the vault's name: %s", name, s)
}
}
}
controller := perms["controller"]
for _, s := range []string{"mesh.seat.node-launcher.tool.secret.anchor",
"mesh.mod.plex.call.plex_issued_credential.anchor.controller"} {
if !MayPublish(controller, s) {
t.Errorf("the controller lost today's path %s before R1", s)
}
}
if MayPublish(perms["anchor.node-tools"], "mesh.seat.node-launcher.call.secret.anchor."+CallerToken("anchor.node-tools")) {
t.Error("a machine's runtime may ask the desk's prompt")
}
}
// **On a real server, as composed** (issue 723): the vault's account asks the desk's prompt and an issuing tool
// and is answered, naming itself; the server refuses it any other call; and the controller is still answered on
// today's path. That only sealed values cross is the vault's test (mesh-catalog, mesh-vault intake_test.go).
func TestOnARealBusTheVaultsAccountIsAnsweredByTheDeskAndTheProviderAndNothingElse(t *testing.T) {
users, err := Users(vaultRecords())
if err != nil {
t.Fatal(err)
}
hash, err := bcrypt.GenerateFromPassword([]byte("pw"), bcrypt.MinCost)
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\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()
// anchor's runtime serves the desk's prompt and plex's issuing tool, on every subject it is granted, and says
// which subject it heard.
holder, err := nats.Connect(s.ClientURL(), nats.UserInfo("anchor.node-tools", "pw"))
if err != nil {
t.Fatal(err)
}
defer holder.Close()
heard := make(chan string, 8)
answer := func(m *nats.Msg) {
heard <- m.Subject
_ = m.Respond([]byte(`{"result":{"sealed":"c2VhbGVkLXRvLXRoZS1jYWxs"}}`))
}
for _, subject := range []string{"mesh.seat.node-launcher.call.secret.anchor.*",
"mesh.seat.node-launcher.tool.secret.anchor", "mesh.mod.plex.call.plex_issued_credential.anchor.*",
"mesh.mod.other.call.tool.anchor.*", "mesh.seat.node-launcher.call.menu.anchor.*"} {
sub, err := holder.Subscribe(subject, answer)
if err != nil {
t.Fatal(err)
}
_ = holder.Flush()
if !sub.IsValid() {
t.Fatalf("the desk's runtime may not hear %s", subject)
}
}
refusals := make(chan error, 16)
vault, err := nats.Connect(s.ClientURL(), nats.UserInfo("novox.mesh-vault", "pw"),
nats.CustomInboxPrefix("_INBOX.novox.mesh-vault"),
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { refusals <- err }))
if err != nil {
t.Fatalf("the vault's own account cannot connect: %v", err)
}
defer vault.Close()
token := CallerToken("novox.mesh-vault")
for _, subject := range []string{"mesh.seat.node-launcher.call.secret.anchor." + token,
"mesh.mod.plex.call.plex_issued_credential.anchor." + token} {
if _, err := vault.Request(subject, []byte(`{"seal_to":"a2V5LW9mLXRoZS1jYWxs"}`), 3*time.Second); err != nil {
t.Fatalf("the vault's call %s was not answered: %v", subject, err)
}
if got := <-heard; got != subject {
t.Fatalf("the holder heard %s, want %s", got, subject)
}
}
for _, subject := range []string{"mesh.seat.node-launcher.tool.secret.anchor",
"mesh.seat.node-launcher.call.secret.anchor.controller",
"mesh.seat.node-launcher.call.menu.anchor." + token,
"mesh.mod.other.call.tool.anchor." + token} {
if err := vault.Publish(subject, []byte(`{}`)); err != nil {
t.Fatal(err)
}
_ = vault.Flush()
select {
case err := <-refusals:
if !errors.Is(err, nats.ErrPermissionViolation) {
t.Errorf("%s: the server said %v, want a permissions violation", subject, err)
}
case <-time.After(3 * time.Second):
t.Errorf("the server did not refuse the vault %s", subject)
}
}
select {
case got := <-heard:
t.Errorf("a call the vault may not make reached the holder: %s", got)
case <-time.After(200 * time.Millisecond):
}
// The controller is still answered on today's path, for the modules filled the old way (until R1).
controller, err := nats.Connect(s.ClientURL(), nats.UserInfo("controller", "pw"),
nats.CustomInboxPrefix("_INBOX.controller"))
if err != nil {
t.Fatal(err)
}
defer controller.Close()
if _, err := controller.Request("mesh.seat.node-launcher.tool.secret.anchor", []byte(`{}`), 3*time.Second); err != nil {
t.Errorf("the controller's call to the desk on today's path: %v", err)
}
<-heard
}