Files
mesh-controller/internal/broker/genesis_template_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

189 lines
7.3 KiB
Go

package broker
import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
"golang.org/x/crypto/bcrypt"
"github.com/novox/mesh-controller/internal/beside"
)
// **The first user list the installer carries must be the one the controller would compose.**
//
// At genesis there is no mesh to write the bus's user list, so the installer carries one: the
// controller's own account, at a bootstrap password, the way the store is reached at
// `postgres:bootstrap` (novox/hq design 25 §4, task 1.7). It is written by hand in a template and
// derived in code here, which is two statements of one fact — so this compares them.
//
// Getting it wrong is the worst kind of silent: a controller whose carried permissions are narrower
// than the ones it derives comes up, connects, and is refused on the first thing it tries, with an
// authorisation error that names a subject and not the template that forgot it. And a mesh cannot be
// raised twice to find out.
func TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose(t *testing.T) {
accounts := theCarriedAccounts(t)
want, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
carriedPub := subjectsIn(accounts, "publish")
carriedSub := subjectsIn(accounts, "subscribe")
if diff := requiredPublishGap(t, want.Publish, carriedPub); len(diff) > 0 {
t.Errorf("the installer's user list does not let the controller publish %v — it would come up "+
"and be refused on the first thing it tried", diff)
}
if diff := missing(want.Subscribe, carriedSub); len(diff) > 0 {
t.Errorf("the installer's user list does not let the controller subscribe %v", diff)
}
// And nothing wider than what it derives, or genesis quietly grants a privilege the composition
// takes away again on the first push.
if diff := missing(carriedPub, want.Publish); len(diff) > 0 {
t.Errorf("the installer's user list lets the controller publish %v, which it does not derive", diff)
}
if diff := missing(carriedSub, want.Subscribe); len(diff) > 0 {
t.Errorf("the installer's user list lets the controller subscribe %v, which it does not derive", diff)
}
// The credential is the bootstrap one and the hash really is of it, because a hash of something
// else is a controller that cannot log in to the bus it was just given.
hash := regexp.MustCompile(`\$2[aby]?\$[0-9]+\$[A-Za-z0-9./]{53}`).FindString(accounts)
if hash == "" {
t.Fatal("the installer's user list carries no password hash")
}
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte("bootstrap")); err != nil {
t.Fatalf("the carried hash does not verify the bootstrap credential the template also carries: %v", err)
}
}
// requiredPublishGap is what in want is missing from carried, leaving out a grant DegradableGrants names
// and saying why in the test log instead (novox/hq issue 835, the forge check of mesh-controller#345): the
// merge check reads mesh-host at the commit the machines run, which a change to mesh-host's examples alone
// never moves, so the carried list can lag the controller's derived one without the controller refusing to
// come up.
func requiredPublishGap(t *testing.T, want, carried []string) []string {
t.Helper()
diff := missing(want, carried)
var required []string
for _, subject := range diff {
if reason, ok := degradableReason(subject); ok {
t.Logf("the installer's user list does not let the controller publish %s, but that falls back "+
"rather than refuses: %s", subject, reason)
continue
}
required = append(required, subject)
}
return required
}
// degradableReason says whether a subject is one of DegradableGrants, and why.
func degradableReason(subject string) (string, bool) {
for _, g := range DegradableGrants {
if g.Subject == subject {
return g.Reason, true
}
}
return "", false
}
// TestADegradableGrantMissingFromTheInstallersListPasses is the forge check's own case (mesh-controller#345,
// novox/hq issue 835): an installer list that lacks operator-channel.tell alone, because mesh-host has not
// yet built with the grant, still passes — the controller falls back to the secret-given condition rather
// than coming up refused.
func TestADegradableGrantMissingFromTheInstallersListPasses(t *testing.T) {
want := []string{"mesh.seat.operator-channel.tool.tell", "mesh.seat.node-service-manager.tool.restart.*"}
carried := []string{"mesh.seat.node-service-manager.tool.restart.*"}
if diff := requiredPublishGap(t, want, carried); len(diff) > 0 {
t.Errorf("a degradable grant missing from the installer's list must not fail the genesis test, got %v", diff)
}
}
// TestANonDegradableGrantMissingFromTheInstallersListStillFails proves the exemption is named, not general:
// every other grant the controller derives stays required.
func TestANonDegradableGrantMissingFromTheInstallersListStillFails(t *testing.T) {
want := []string{"mesh.seat.operator-channel.tool.tell", "mesh.seat.node-service-manager.tool.restart.*"}
carried := []string{"mesh.seat.operator-channel.tool.tell"}
diff := requiredPublishGap(t, want, carried)
if len(diff) != 1 || diff[0] != "mesh.seat.node-service-manager.tool.restart.*" {
t.Errorf("a missing grant that is not degradable must still fail the genesis test, got %v", diff)
}
}
// theCarriedAccounts is the accounts file the installer's template writes at genesis.
func theCarriedAccounts(t *testing.T) string {
t.Helper()
for _, r := range theTemplate(t) {
if r["id"] == "bus-accounts" {
content, _ := r["content"].(string)
if content == "" {
t.Fatal("the template's accounts file is empty, so the bus would refuse every connection")
}
return content
}
}
t.Fatal("the template carries no accounts file, so a mesh raised from it has a bus nobody may use")
return ""
}
// theTemplate is the installer's bundle, as resources: the node-engine's, as a merge check clones it at
// the commit the mesh runs, or as captured in testdata/beside (internal/beside, novox/hq issue 432).
func theTemplate(t *testing.T) []map[string]any {
t.Helper()
path := filepath.Join(beside.Dir(t, "mesh-host"), "examples", "foundation-first-node.lock")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
// The template is JSON with line comments, which is how every one of them is written.
var lines []string
for _, l := range strings.Split(string(raw), "\n") {
if !strings.HasPrefix(strings.TrimSpace(l), "//") {
lines = append(lines, l)
}
}
var bundle struct {
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal([]byte(strings.Join(lines, "\n")), &bundle); err != nil {
t.Fatalf("the template is not readable: %v", err)
}
return bundle.Resources
}
// subjectsIn reads one allow-list out of a composed accounts file.
func subjectsIn(accounts, which string) []string {
found := regexp.MustCompile(which + `: \{ allow: \[([^\]]*)\]`).FindStringSubmatch(accounts)
if len(found) != 2 {
return nil
}
var out []string
for _, part := range strings.Split(found[1], ",") {
if s := strings.Trim(strings.TrimSpace(part), `"`); s != "" {
out = append(out, s)
}
}
sort.Strings(out)
return out
}
// missing is what is in want and not in got.
func missing(want, got []string) []string {
have := map[string]bool{}
for _, g := range got {
have[g] = true
}
var out []string
for _, w := range want {
if !have[w] {
out = append(out, w)
}
}
return out
}