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).
228 lines
8.4 KiB
Go
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
|
|
}
|