From 8d9b3f779d25d97c8ce9b62720a888f50aba89ea Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 27 Oct 2026 22:19:53 +0100 Subject: [PATCH] =?UTF-8?q?Build=20a=20minted=20account's=20value=20as=20t?= =?UTF-8?q?he=20controller=20does,=20byte=20for=20byte,=20so=20a=20runtime?= =?UTF-8?q?=20reads=20the=20vault's=20unchanged=20(issue=20774,=20ADR=2005?= =?UTF-8?q?61=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/buscompose/accounts.go | 41 ++++++++++++++ go/buscompose/buscompose_test.go | 53 +++++++++++++++++++ .../testdata/controller-credentials.json | 21 ++++++++ 3 files changed, 115 insertions(+) create mode 100644 go/buscompose/testdata/controller-credentials.json diff --git a/go/buscompose/accounts.go b/go/buscompose/accounts.go index a08bdbf..1402804 100644 --- a/go/buscompose/accounts.go +++ b/go/buscompose/accounts.go @@ -145,3 +145,44 @@ type AccountIssued struct { Generation int `json:"generation"` At time.Time `json:"at"` } + +// HeldCredential is the value the account's holder reads, exactly as the controller builds it today (issueWith, +// sealProcess and sealProvisionerClient): a module's `{url, fingerprint, node, module, user, password, claims}`, a +// process's without claims, a provisioner client's without module or claims; an empty fingerprint or claims left out. +func HeldCredential(w WantedAccount, password string) ([]byte, error) { + if err := w.Valid(); err != nil { + return nil, err + } + if password == "" { + return nil, fmt.Errorf("%s: an account is held with its password", w.User) + } + switch w.Kind { + case KindProcess: + return json.Marshal(struct { + URL string `json:"url"` + Fingerprint string `json:"fingerprint,omitempty"` + Node string `json:"node"` + Module string `json:"module"` + User string `json:"user"` + Password string `json:"password"` + }{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, Module: w.Module, User: w.User, Password: password}) + case KindProvisioner: + return json.Marshal(struct { + URL string `json:"url"` + Fingerprint string `json:"fingerprint,omitempty"` + Node string `json:"node"` + User string `json:"user"` + Password string `json:"password"` + }{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, User: w.User, Password: password}) + } + return json.Marshal(struct { + URL string `json:"url"` + Fingerprint string `json:"fingerprint,omitempty"` + Node string `json:"node"` + Module string `json:"module"` + User string `json:"user"` + Password string `json:"password"` + Claims []ClaimedSeat `json:"claims,omitempty"` + }{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, Module: w.Module, User: w.User, Password: password, + Claims: w.Claims}) +} diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 69cb306..2b55e87 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -344,4 +344,57 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Errorf("account-issued says %s at %s", raw, AccountIssuedSubject) } }) + // 774/AC3 (novox/hq ADR 0561 ยง2): the account the vault seals is, byte for byte, what the controller hands for each of + // its three kinds: testdata/controller-credentials.json was written by the controller's own code (its "how"). + t.Run("a held account is the controller's shape byte for byte", func(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "controller-credentials.json")) + if err != nil { + t.Fatal(err) + } + var captured struct { + Rows []struct{ Name, Held string } + } + if err := json.Unmarshal(raw, &captured); err != nil { + t.Fatal(err) + } + vault := ClaimedSeat{Seat: "mesh-vault", Scope: "mesh", Serves: []string{"get", "accept", "secret-ask", "issued", + "rotate", "move-in", "node-key", "own", "move-asks"}} + controller := ClaimedSeat{Seat: "mesh-controller", Scope: "mesh", Serves: strings.Split("tools,calls,status,nodes,"+ + "node,placement-export,modules,seats,builds,plans,delivery-plan,delivery-order,delivery-check,deliver,"+ + "delivery-stop,delivery-backups-again,delivery-walks,plan,assign,unassign,pin,unpin,push,rotate,give,issued,"+ + "secret-move,secret-ask,issue,token,settings,command,queue,cancel,clear,replay,try,kill,pause,resume,hand-act,"+ + "drill,warranted,hand-acts,durations,conditions,healers,doctor,upgrade,bus,retire,cleanup,judge-directories,"+ + "root-free,dead-letters,data,build,artifacts,held,collect,move-placement,move-config,mirrors,images", ",")} + at := "nats://10.0.0.1:4222" + wanted := map[string]struct { + w WantedAccount + password string + }{ + "module": {WantedAccount{User: "laptop.cap-bot", Kind: KindModule, Node: "laptop", Module: "cap-bot", URL: at, + Fingerprint: "ab:cd", Claims: []ClaimedSeat{vault, {Seat: "node-service-manager", Scope: "node", + Serves: []string{"status"}}, controller, {Seat: "no-such-seat", Scope: "node"}}}, "pw-module"}, + "module-no-claims-no-fingerprint": {WantedAccount{User: "laptop.cap-plain", Kind: KindModule, Node: "laptop", + Module: "cap-plain", URL: at}, "pw-plain"}, + "provisioner": {WantedAccount{User: "provisioner.laptop", Kind: KindProvisioner, Node: "laptop", Module: "nats", + URL: at, Fingerprint: "ab:cd"}, "pw-prov"}, + "process": {WantedAccount{User: "laptop.mesh-mail.send", Kind: KindProcess, Node: "laptop", Module: "mesh-mail", + Process: "send", URL: at, Fingerprint: "ab:cd"}, "pw-proc"}, + } + if len(captured.Rows) != len(wanted) { + t.Fatalf("%d rows captured, %d wanted", len(captured.Rows), len(wanted)) + } + for _, row := range captured.Rows { + c, ok := wanted[row.Name] + if !ok { + t.Fatalf("no wanted account for the captured %s", row.Name) + } + got, err := HeldCredential(c.w, c.password) + if err != nil || string(got) != row.Held { + t.Errorf("%s:\n got %s (%v)\nwant %s", row.Name, got, err, row.Held) + } + } + if _, err := HeldCredential(wanted["module"].w, ""); err == nil { + t.Error("an account was held with no password") + } + }) } diff --git a/go/buscompose/testdata/controller-credentials.json b/go/buscompose/testdata/controller-credentials.json new file mode 100644 index 0000000..ab11cf1 --- /dev/null +++ b/go/buscompose/testdata/controller-credentials.json @@ -0,0 +1,21 @@ +{ + "how": "Written by mesh-controller 0fbb5ef's own issueWith, sealProvisionerClient and heldProcessAccount (cmd/mesh-controller), run unchanged in a scratch integration test against a local store with the default seats seeded, on 2026-10-27 (issue 774): each row is the exact value the controller handed the vault. Never edit a row by hand: capture it again.", + "rows": [ + { + "name": "module", + "held": "{\"url\":\"nats://10.0.0.1:4222\",\"fingerprint\":\"ab:cd\",\"node\":\"laptop\",\"module\":\"cap-bot\",\"user\":\"laptop.cap-bot\",\"password\":\"pw-module\",\"claims\":[{\"seat\":\"mesh-vault\",\"scope\":\"mesh\",\"serves\":[\"get\",\"accept\",\"secret-ask\",\"issued\",\"rotate\",\"move-in\",\"node-key\",\"own\",\"move-asks\"]},{\"seat\":\"node-service-manager\",\"scope\":\"node\",\"serves\":[\"status\"]},{\"seat\":\"mesh-controller\",\"scope\":\"mesh\",\"serves\":[\"tools\",\"calls\",\"status\",\"nodes\",\"node\",\"placement-export\",\"modules\",\"seats\",\"builds\",\"plans\",\"delivery-plan\",\"delivery-order\",\"delivery-check\",\"deliver\",\"delivery-stop\",\"delivery-backups-again\",\"delivery-walks\",\"plan\",\"assign\",\"unassign\",\"pin\",\"unpin\",\"push\",\"rotate\",\"give\",\"issued\",\"secret-move\",\"secret-ask\",\"issue\",\"token\",\"settings\",\"command\",\"queue\",\"cancel\",\"clear\",\"replay\",\"try\",\"kill\",\"pause\",\"resume\",\"hand-act\",\"drill\",\"warranted\",\"hand-acts\",\"durations\",\"conditions\",\"healers\",\"doctor\",\"upgrade\",\"bus\",\"retire\",\"cleanup\",\"judge-directories\",\"root-free\",\"dead-letters\",\"data\",\"build\",\"artifacts\",\"held\",\"collect\",\"move-placement\",\"move-config\",\"mirrors\",\"images\"]},{\"seat\":\"no-such-seat\",\"scope\":\"node\"}]}" + }, + { + "name": "module-no-claims-no-fingerprint", + "held": "{\"url\":\"nats://10.0.0.1:4222\",\"node\":\"laptop\",\"module\":\"cap-plain\",\"user\":\"laptop.cap-plain\",\"password\":\"pw-plain\"}" + }, + { + "name": "provisioner", + "held": "{\"url\":\"nats://10.0.0.1:4222\",\"fingerprint\":\"ab:cd\",\"node\":\"laptop\",\"user\":\"provisioner.laptop\",\"password\":\"pw-prov\"}" + }, + { + "name": "process", + "held": "{\"url\":\"nats://10.0.0.1:4222\",\"fingerprint\":\"ab:cd\",\"node\":\"laptop\",\"module\":\"mesh-mail\",\"user\":\"laptop.mesh-mail.send\",\"password\":\"pw-proc\"}" + } + ] +}