Files
mesh-controller/internal/broker/genesis_template_test.go
T
jochen 5cf8938f55
mesh/merge-gate head 5cf8938f55b3 merged into trunk 0ab61d1be8c5 — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head 5cf8938f55b3 merged into trunk 0ab61d1be8c5 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/mail-seats delivered: every member is delivered
Let the genesis test accept a degradable grant missing from the installer's list (issue 835)
Forge check of mesh-controller#345: the merge check clones mesh-host at the commit the
machines run, not its main, so a change that only edits mesh-host's examples (the
operator-channel.tell grant) can never be seen there until mesh-host's own module next
builds. operator-channel.tell is the one grant with a fallback for a refusal (the
secret-given condition, ADR 0380 §9), so mark it degradable and let
TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose pass without it while every
other grant stays required.
2026-10-15 19:29:13 +02: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
}