From deac3e232ccb38d8a1283765706e246bbd17a707 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 19:21:11 +0200 Subject: [PATCH] Refuse a process account's stand-in, naming issue (issue 1226) --- cmd/mesh-controller/compose_perform.go | 10 ++ cmd/mesh-controller/process_accounts.go | 120 +++++++++++++++++++ cmd/mesh-controller/process_accounts_test.go | 42 +++++++ 3 files changed, 172 insertions(+) create mode 100644 cmd/mesh-controller/process_accounts.go create mode 100644 cmd/mesh-controller/process_accounts_test.go diff --git a/cmd/mesh-controller/compose_perform.go b/cmd/mesh-controller/compose_perform.go index c71c4bfe..ed29ac69 100644 --- a/cmd/mesh-controller/compose_perform.go +++ b/cmd/mesh-controller/compose_perform.go @@ -233,6 +233,16 @@ func prepareSend(ctx context.Context, open *stores, node string) ([]string, erro if !slices.Contains(in.BusMinted, "provisioner."+node) { continue } + default: + // A bus process's own account is minted by `issue`, never made as a random value (issue 1226). + if err := processAccountRefusal(m, node, name, in.BusMinted); err != nil { + return nil, err + } + if strings.HasPrefix(name, "broker-") && len(m.ProcessBuses()) > 0 { + if _, isProcess := m.ProcessBuses()[strings.TrimPrefix(name, "broker-")]; isProcess { + continue // issued, and not held under this key: issued again, never made + } + } } if _, err := inv.SecretForModule(ctx, node, m.Module, name); err == nil { did = true diff --git a/cmd/mesh-controller/process_accounts.go b/cmd/mesh-controller/process_accounts.go new file mode 100644 index 00000000..4dee11d3 --- /dev/null +++ b/cmd/mesh-controller/process_accounts.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + + "github.com/novox/mesh-controller/internal/broker" + "github.com/novox/mesh-controller/internal/inventory" +) + +// processUser is the bus user of one process of a module on a node (novox/hq ADR 0436 §1). +func processUser(node, module, process string) string { + return broker.Principal{Kind: broker.KindProcess, Node: node, Module: module, Process: process}.Username() +} + +// processAccountRefusal is the refusal for the own secret broker- of a process that declares bus when +// nobody issued its account (novox/hq ADR 0436 §2, issue 1226): the account is never invented as a random value +// the process cannot read, it is minted by `mesh-controller.issue`. Nil for any other secret, and for an account +// that is issued. +func processAccountRefusal(m catalogue.Manifest, node, secret string, minted []string) error { + id, isProcess := strings.CutPrefix(secret, "broker-") + if !isProcess { + return nil + } + if _, declared := m.ProcessBuses()[id]; !declared { + return nil + } + user := processUser(node, m.Module, id) + for _, u := range minted { + if u == user { + return nil + } + } + return fmt.Errorf("%s on %s has no account for its process %s: nothing was issued for %s, and a push would "+ + "seal a random value its process cannot read (novox/hq ADR 0436 §2, issue 1226). "+ + "`mesh-controller.issue %s %s` (module issue %s --node %s), then push again", + m.Module, node, id, user, node, m.Module, m.Module, node) +} + +// processAccountsMissing is each process of m that declares bus and whose account is not minted yet on node. +func processAccountsMissing(m catalogue.Manifest, node string, minted []string) []broker.ProcessPrincipal { + have := map[string]bool{} + for _, u := range minted { + have[u] = true + } + var out []broker.ProcessPrincipal + for _, pr := range inventory.ProcessesOf(m) { + if !have[processUser(node, m.Module, pr.Process)] { + out = append(out, pr) + } + } + return out +} + +// issueMissingProcessAccounts issues the accounts of m's bus processes on node that are not minted, and no +// other: an account already issued is rotated on purpose, never by a later delivery or assignment (issue 1226). +// It says what it issued, one line, and nothing when nothing was missing. +func issueMissingProcessAccounts(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest, node string) (string, error) { + if len(m.ProcessBuses()) == 0 || mayIssue(m) != nil { + return "", nil + } + minted, err := inv.BusMinted(ctx) + if err != nil { + return "", err + } + missing := processAccountsMissing(m, node, minted) + if len(missing) == 0 { + return "", nil + } + known, err := broker.FromEnvironment() + if err != nil { + return "", fmt.Errorf("cannot deliver a credential without knowing where the bus is: %w", err) + } + reachable, err := brokerReachableAt(ctx, inv, known, node) + if err != nil { + return "", err + } + busAddress, err := broker.BusAddress() + if err != nil { + busAddress = "" + } + var ids []string + for _, pr := range missing { + if err := issueProcess(ctx, inv, m, pr, node, busAddress, known, reachable); err != nil { + return "", err + } + ids = append(ids, pr.Process) + } + return fmt.Sprintf("its process account(s) %s issued and sealed to %s, and arrive with the push", + strings.Join(ids, ", "), node), nil +} + +// issueGainedProcessAccounts runs after a module's new definition is registered: on each machine assigned it, the +// accounts of its bus processes not yet minted are issued, so a process a delivery adds is not pushed with a +// random stand-in (issue 1226). Never fatal: the build worked and the module is registered, so a failure is said, +// and the push refuses the module until `mesh-controller.issue` is run. +func issueGainedProcessAccounts(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest) { + if len(m.ProcessBuses()) == 0 { + return + } + nodes, err := inv.Running(ctx, m.Module) + if err != nil { + fmt.Printf("%s: its machines are not read to issue its process accounts: %v\n", m.Module, err) + return + } + for _, node := range nodes { + line, err := issueMissingProcessAccounts(ctx, inv, m, node) + if err != nil { + fmt.Printf("%s on %s: its process accounts are not issued (%v): `mesh-controller.issue %s %s` first\n", + m.Module, node, err, node, m.Module) + continue + } + if line != "" { + fmt.Printf("%s on %s: %s\n", m.Module, node, line) + } + } +} diff --git a/cmd/mesh-controller/process_accounts_test.go b/cmd/mesh-controller/process_accounts_test.go new file mode 100644 index 00000000..84c7fea8 --- /dev/null +++ b/cmd/mesh-controller/process_accounts_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "strings" + "testing" +) + +// 1226/AC1: given a module whose process declares bus and has no issued account, when a machine's declaration +// is composed, the push refuses that module, naming issue. (The composition's wiring is proven against the store +// in TestAGainedProcessIsIssuedWhenTheDeliveryRecordsIt.) +func TestAProcessAccountNobodyIssuedIsRefusedNamingIssue(t *testing.T) { + m := mailSenderWithProcess() + err := processAccountRefusal(m, "laptop", "broker-send", nil) + if err == nil { + t.Fatal("1226/AC1: a process account nobody issued was not refused, so a random value would be made") + } + if !strings.Contains(err.Error(), "mesh-controller.issue laptop mesh-mail") { + t.Fatalf("1226/AC1: the refusal does not name issue: %v", err) + } + // Other names are not this rule's: the module's own broker and a secret of no process. + if processAccountRefusal(m, "laptop", "broker", nil) != nil || processAccountRefusal(m, "laptop", "broker-other", nil) != nil { + t.Fatal("1226/AC1: a name that is no declared process's account was refused") + } + // Issued, it is not refused. + if err := processAccountRefusal(m, "laptop", "broker-send", []string{"laptop.mesh-mail.send"}); err != nil { + t.Fatalf("1226/AC1: an issued account was refused: %v", err) + } +} + +// 1226/AC3: an account already issued is not replaced by a later compose, nor issued again by a delivery. +func TestAnIssuedProcessAccountIsNotIssuedAgain(t *testing.T) { + m := mailSenderWithProcess() + if got := processAccountsMissing(m, "laptop", nil); len(got) != 1 || got[0].Process != "send" { + t.Fatalf("1226/AC3: an account not issued is not missing: %v", got) + } + if got := processAccountsMissing(m, "laptop", []string{"laptop.mesh-mail.send"}); len(got) != 0 { + t.Fatalf("1226/AC3: an issued account would be issued again: %v", got) + } + if got := processAccountsMissing(m, "ace", []string{"laptop.mesh-mail.send"}); len(got) != 1 { + t.Fatalf("1226/AC3: another machine's account stands in for this one's: %v", got) + } +}