Refuse a process account's stand-in, naming issue (issue 1226)

This commit is contained in:
2026-10-21 19:21:11 +02:00
parent 5ba2ffe8e6
commit deac3e232c
3 changed files with 172 additions and 0 deletions
+10
View File
@@ -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
+120
View File
@@ -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-<process id> 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)
}
}
}
@@ -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)
}
}