146 lines
5.5 KiB
Go
146 lines
5.5 KiB
Go
package broker
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/nats-io/nats-server/v2/server"
|
|
)
|
|
|
|
// 1147/AC1: the composed accounts file declares $SYS with no user, exporting only the connect and disconnect events of
|
|
// MESH and the request listing MESH's connections, and MESH imports exactly those three as mesh.bus.connect,
|
|
// mesh.bus.disconnect and mesh.bus.connz (novox/hq ADR 0431 §1); the server's own grammar takes it with $SYS as its
|
|
// system account; the golden file shows it (TestTheComposedConfigMatchesTheGolden).
|
|
func TestTheComposedAccountsExportMeshsConnectionEventsFromTheSystemAccount(t *testing.T) { // 1147/AC1
|
|
text, err := ComposeAccounts([]Principal{{Kind: KindNode, Node: "one", PasswordHash: "x"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sys := between(text, `"$SYS" {`, "\n }\n")
|
|
if sys == "" {
|
|
t.Fatalf("no account $SYS in the composed accounts:\n%s", text)
|
|
}
|
|
if strings.Contains(sys, "users") || strings.Contains(sys, "user:") {
|
|
t.Errorf("$SYS holds a user:\n%s", sys)
|
|
}
|
|
for _, want := range []string{
|
|
`{ stream: "$SYS.ACCOUNT.MESH.CONNECT", accounts: ["MESH"] }`,
|
|
`{ stream: "$SYS.ACCOUNT.MESH.DISCONNECT", accounts: ["MESH"] }`,
|
|
`{ service: "$SYS.REQ.ACCOUNT.MESH.CONNZ", accounts: ["MESH"] }`,
|
|
} {
|
|
if !strings.Contains(sys, want) {
|
|
t.Errorf("$SYS does not export %s:\n%s", want, sys)
|
|
}
|
|
}
|
|
if n := strings.Count(sys, "{ stream:") + strings.Count(sys, "{ service:"); n != 3 {
|
|
t.Errorf("$SYS exports %d things, want exactly 3:\n%s", n, sys)
|
|
}
|
|
mesh := between(text, " MESH {", " users = [")
|
|
for _, want := range []string{
|
|
`{ stream: { account: "$SYS", subject: "$SYS.ACCOUNT.MESH.CONNECT" }, to: "mesh.bus.connect" }`,
|
|
`{ stream: { account: "$SYS", subject: "$SYS.ACCOUNT.MESH.DISCONNECT" }, to: "mesh.bus.disconnect" }`,
|
|
`{ service: { account: "$SYS", subject: "$SYS.REQ.ACCOUNT.MESH.CONNZ" }, to: "mesh.bus.connz" }`,
|
|
} {
|
|
if !strings.Contains(mesh, want) {
|
|
t.Errorf("MESH does not import %s:\n%s", want, mesh)
|
|
}
|
|
}
|
|
if n := strings.Count(mesh, "{ stream:") + strings.Count(mesh, "{ service:"); n != 3 {
|
|
t.Errorf("MESH imports %d things, want exactly 3:\n%s", n, mesh)
|
|
}
|
|
|
|
// The pinned server's grammar takes it, and makes $SYS its system account.
|
|
path := filepath.Join(t.TempDir(), "accounts.conf")
|
|
if err := os.WriteFile(path, []byte(text), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts, err := server.ProcessConfigFile(path)
|
|
if err != nil {
|
|
t.Fatalf("the server refuses the composed accounts: %v", err)
|
|
}
|
|
var found bool
|
|
for _, a := range opts.Accounts {
|
|
if a.Name != "$SYS" {
|
|
continue
|
|
}
|
|
found = true
|
|
if !a.IsExportService("$SYS.REQ.ACCOUNT.MESH.CONNZ") {
|
|
t.Error("$SYS does not export the connections request as a service")
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("the server read no account $SYS")
|
|
}
|
|
for _, u := range opts.Users {
|
|
if u.Account != nil && u.Account.Name == "$SYS" {
|
|
t.Errorf("user %s is in $SYS", u.Username)
|
|
}
|
|
}
|
|
}
|
|
|
|
// between is the text from the first start to the first end after it, or empty.
|
|
func between(s, start, end string) string {
|
|
i := strings.Index(s, start)
|
|
if i < 0 {
|
|
return ""
|
|
}
|
|
rest := s[i:]
|
|
j := strings.Index(rest[len(start):], end)
|
|
if j < 0 {
|
|
return rest
|
|
}
|
|
return rest[:len(start)+j]
|
|
}
|
|
|
|
// 1147/AC2: only mesh-delivery-watchman's principal may subscribe to mesh.bus.connect and mesh.bus.disconnect and
|
|
// publish to mesh.bus.connz; no other principal's grant covers them by a wildcard (ADR 0431 §2).
|
|
func TestOnlyTheWatchmanHearsTheBusConnectionEvents(t *testing.T) { // 1147/AC2
|
|
builder := Seat{Name: "node-build-agent", Scope: "node", Accepts: []string{"build"}, Emits: []string{"built"}}
|
|
watchman := Principal{Kind: KindModule, Node: "one", Module: "mesh-delivery-watchman", PasswordHash: "x"}
|
|
others := []Principal{
|
|
{Kind: KindController, PasswordHash: "x"},
|
|
{Kind: KindNode, Node: "one", PasswordHash: "x"},
|
|
{Kind: KindEnrolment, Node: "two", PasswordHash: "x"},
|
|
{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}, PasswordHash: "x"},
|
|
{Kind: KindProvisioner, Node: "one", PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "gitea", Emits: []string{"pull.merged"}, PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "mesh-delivery-view", PasswordHash: "x",
|
|
Reads: []string{"node-engine.have"}, Consumes: []string{"mesh-delivery-watchman.presence"}},
|
|
{Kind: KindModule, Node: "one", Module: "build-agent", Holds: []Seat{builder}, PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "nats", SnapshotsTheBus: true, PasswordHash: "x"},
|
|
{Kind: KindNodeTools, Node: "one", Module: RuntimeModule, PasswordHash: "x", Carries: []Declared{
|
|
{Module: "mesh-delivery-watchman"}, {Module: "build-agent", Holds: []Seat{builder}}}},
|
|
}
|
|
heard := []string{"mesh.bus.connect", "mesh.bus.disconnect"}
|
|
perms, err := PermissionsFor(watchman)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range heard {
|
|
has(t, perms.Subscribe, s)
|
|
}
|
|
has(t, perms.Publish, "mesh.bus.connz")
|
|
for _, s := range append(heard, "mesh.bus.connz") {
|
|
for _, p := range perms.Subscribe {
|
|
if p != s && SubjectsOverlap(p, s) && !strings.HasPrefix(p, "_INBOX.") {
|
|
t.Errorf("the watchman's subscription %q reaches %s beyond naming it", p, s)
|
|
}
|
|
}
|
|
}
|
|
for _, p := range others {
|
|
perms, err := PermissionsFor(p)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", p.Username(), err)
|
|
}
|
|
for _, s := range append(heard, "mesh.bus.connz") {
|
|
for _, g := range append(append([]string(nil), perms.Publish...), perms.Subscribe...) {
|
|
if SubjectsOverlap(g, s) {
|
|
t.Errorf("%s is granted %q, which covers %s", p.Username(), g, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|