Warn instead of holding the machine for an unissued process account, and drive the delivery path in the test (issue 1226, review round 1)

This commit is contained in:
2026-10-21 19:25:44 +02:00
parent 6bcfbeb5b2
commit faf8beee84
4 changed files with 109 additions and 67 deletions
+4 -8
View File
@@ -234,14 +234,10 @@ func prepareSend(ctx context.Context, open *stores, node string) ([]string, erro
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
}
// A bus process's account nobody issued still gets the stand-in, until the composition leaves
// the module off (issue 1753): the warning names the verb (issue 1226).
if line := processAccountWarning(m, node, name, in.BusMinted); line != "" {
sayWarning(ctx, line)
}
}
if _, err := inv.SecretForModule(ctx, node, m.Module, name); err == nil {
+22 -12
View File
@@ -3,6 +3,8 @@ package main
import (
"context"
"fmt"
"io"
"os"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
@@ -16,28 +18,36 @@ 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 {
// processAccountWarning is the warning line 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): "" for any other secret and for an issued
// account. The send goes on with the stand-in until the composition leaves the module off (issue 1753), so the
// line is the only loud thing about it.
func processAccountWarning(m catalogue.Manifest, node, secret string, minted []string) string {
id, isProcess := strings.CutPrefix(secret, "broker-")
if !isProcess {
return nil
return ""
}
if _, declared := m.ProcessBuses()[id]; !declared {
return nil
return ""
}
user := processUser(node, m.Module, id)
for _, u := range minted {
if u == user {
return nil
return ""
}
}
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)
return fmt.Sprintf("%s/%s process %s: its bus account is not issued — mesh-controller.issue %s %s",
node, m.Module, id, node, m.Module)
}
// sayWarning writes a line where the compose's other warnings go: the act's answer when it carries a writer,
// else the controller's journal.
func sayWarning(ctx context.Context, line string) {
var w io.Writer = os.Stderr
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
w = lw
}
fmt.Fprintln(w, line)
}
// processAccountsMissing is each process of m that declares bus and whose account is not minted yet on node.
@@ -3,6 +3,8 @@
package main
import (
"bytes"
"encoding/json"
"path/filepath"
"strings"
"testing"
@@ -10,11 +12,24 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/testbus"
)
// 1226/AC2: given an assigned module whose new definition adds a bus process, when the delivery records it,
// that process's account is issued and the next compose carries a real account; and (AC1) before it, the
// composition refuses the module naming issue; (AC3) a later record does not replace the account.
func gainedAndPlain(t *testing.T) (plain, gained catalogue.Manifest) {
t.Helper()
plain, err := catalogue.ParseManifest([]byte(`{"module":"mesh-mail","version":"1",
"own-secrets":{"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}},
"resources":[{"id":"account","type":"user","name":"mesh-mail-send"}]}`))
if err != nil {
t.Fatal(err)
}
return plain, mailSenderWithProcess()
}
// 1226/AC2: given an assigned module whose new definition adds a bus process, when the delivery records it
// (takeIn, the function that records a build result), the process's account is minted; and 1226/AC3: the same
// build heard again replaces nothing.
func TestAGainedProcessIsIssuedWhenTheDeliveryRecordsIt(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
@@ -24,41 +39,68 @@ func TestAGainedProcessIsIssuedWhenTheDeliveryRecordsIt(t *testing.T) {
if err := busCertificate([]string{"--into", dir}); err != nil {
t.Fatal(err)
}
t.Setenv(broker.AddressVar, "127.0.0.1:4222")
t.Setenv(broker.AddressVar, strings.TrimPrefix(testbus.URL(t), "nats://"))
t.Setenv(broker.CertificateVar, filepath.Join(dir, "tls.crt"))
// Assigned without the process, then a definition that gains it.
plain, err := catalogue.ParseManifest([]byte(`{"module":"mesh-mail","version":"1",
"own-secrets":{"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}},
"resources":[{"id":"account","type":"user","name":"mesh-mail-send"}]}`))
if err != nil {
t.Fatal(err)
}
plain, gained := gainedAndPlain(t)
register(t, open, plain)
if _, err := assign(ctx, open, "laptop", "mesh-mail"); err != nil {
t.Fatal(err)
}
gained := mailSenderWithProcess()
register(t, open, gained)
// 1226/AC1 on the store: nothing issued, so the compose refuses, naming issue.
if _, err := prepareSend(ctx, open, "laptop"); err == nil || !strings.Contains(err.Error(), "mesh-controller.issue laptop mesh-mail") {
t.Fatalf("1226/AC1: the composition did not refuse the unissued account naming issue: %v", err)
if _, minted, _ := inv.BusUserHash(ctx, "laptop.mesh-mail.send"); minted {
t.Fatal("the account was minted before the module gained the process")
}
issueGainedProcessAccounts(ctx, inv, gained)
manifest, _ := json.Marshal(gained)
build := func(id, commit string) {
t.Helper()
if _, _, err := takeIn(ctx, inv, asTheOperator(t, inv, link.BuildResult{
ID: id, Repository: "http://forge.internal:20000/novox/mesh-mail.git", Path: "modules/mesh-mail",
Ref: "main", On: "anchor", Commit: commit, Manifest: manifest,
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-mail"},
})); err != nil {
t.Fatal(err)
}
}
build("b-1", "aaaaaaa1111")
hash, held, err := inv.BusUserHash(ctx, "laptop.mesh-mail.send")
if err != nil || !held {
t.Fatalf("1226/AC2: the gained process's account was not issued: held %v, %v", held, err)
t.Fatalf("1226/AC2: recording the gained definition did not mint the process's account: held %v, %v", held, err)
}
if _, err := prepareSend(ctx, open, "laptop"); err != nil {
t.Fatalf("1226/AC2: the next compose does not carry the account: %v", err)
}
// 1226/AC3: recording the definition again, and composing, replaces nothing.
issueGainedProcessAccounts(ctx, inv, gained)
build("b-2", "bbbbbbb2222")
again, _, err := inv.BusUserHash(ctx, "laptop.mesh-mail.send")
if err != nil || again != hash {
t.Fatalf("1226/AC3: the issued account was replaced (%v)", err)
t.Fatalf("1226/AC3: recording again replaced the issued account (%v)", err)
}
}
// 1226/AC1 and AC4 against the store: a process account nobody issued is warned of in the push's answer, naming
// issue, and the send is not refused, so the machine's other modules are composed.
func TestAnUnissuedProcessAccountIsWarnedOfAndTheRestIsComposed(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
_, gained := gainedAndPlain(t)
register(t, open, gained)
other := catalogue.Manifest{Module: "other", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/other/token"}}}
register(t, open, other)
for _, m := range []string{"mesh-mail", "other"} {
if _, err := assign(ctx, open, "laptop", m); err != nil {
t.Fatal(err)
}
}
var said bytes.Buffer
if _, err := prepareSend(leftOutSaidBy(ctx, &said), open, "laptop"); err != nil {
t.Fatalf("1226/AC4: an unissued process account held the machine: %v", err)
}
want := "laptop/mesh-mail process send: its bus account is not issued — mesh-controller.issue laptop mesh-mail"
if !strings.Contains(said.String(), want) {
t.Fatalf("1226/AC1: the push says %q, want %q", said.String(), want)
}
if strings.Contains(said.String(), "other") {
t.Fatalf("1226/AC4: the other module is warned of: %q", said.String())
}
if _, held, err := open.inventory.ModuleSecretIfIssued(ctx, "laptop", "other", "token"); err != nil || !held {
t.Fatalf("1226/AC4: the other module's secret was not composed: held %v, %v", held, err)
}
}
+15 -21
View File
@@ -1,33 +1,27 @@
package main
import (
"strings"
"testing"
)
import "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) {
// 1226/AC1: given a module whose process declares bus and has no issued account, when a machine is composed, the
// push's warning names issue, and the machine's other modules are composed (AC4: nothing is refused).
func TestAProcessAccountNobodyIssuedIsWarnedOfNamingIssue(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")
got := processAccountWarning(m, "laptop", "broker-send", nil)
want := "laptop/mesh-mail process send: its bus account is not issued — mesh-controller.issue laptop mesh-mail"
if got != want {
t.Fatalf("1226/AC1: the warning is %q, want %q", got, want)
}
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.
if processAccountWarning(m, "laptop", "broker", nil) != "" || processAccountWarning(m, "laptop", "broker-other", nil) != "" ||
processAccountWarning(m, "laptop", "token", nil) != "" {
t.Fatal("1226/AC1: a name that is no declared process's account was warned of")
}
// 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)
if got := processAccountWarning(m, "laptop", "broker-send", []string{"laptop.mesh-mail.send"}); got != "" {
t.Fatalf("1226/AC3: an issued account was warned of: %q", got)
}
}
// 1226/AC3: an account already issued is not replaced by a later compose, nor issued again by a delivery.
// 1226/AC3: an account already issued is not issued again by a later delivery or assignment.
func TestAnIssuedProcessAccountIsNotIssuedAgain(t *testing.T) {
m := mailSenderWithProcess()
if got := processAccountsMissing(m, "laptop", nil); len(got) != 1 || got[0].Process != "send" {