package broker import ( "encoding/json" "os" "path/filepath" "regexp" "sort" "strings" "testing" "golang.org/x/crypto/bcrypt" ) // **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 := missing(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) } } // theCarriedAccounts is the accounts file the installer's template writes at genesis. func theCarriedAccounts(t *testing.T) string { t.Helper() path := filepath.Join("..", "..", "..", "mesh-host", "examples", "foundation-first-node-nats.lock") raw, err := os.ReadFile(path) if err != nil { t.Skipf("the host's checkout is not beside this one: %v", 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) } for _, r := range bundle.Resources { 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 "" } // 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 }