Let module issue mint and deliver a bus process's account (issue 1181)
This commit is contained in:
@@ -930,9 +930,24 @@ func mayIssue(m catalogue.Manifest) error {
|
||||
return fmt.Errorf("%s declares no own secret named broker, so there is nowhere to deliver "+
|
||||
"an account; a module that speaks on the bus declares \"own-secrets\": {\"broker\": <path>}", m.Module)
|
||||
}
|
||||
// And every process with a bus declaration (novox/hq ADR 0436 §2): its own account is delivered as the
|
||||
// module's own secret broker-<process id>, never the module's `broker`, so a process with a bus and no
|
||||
// such secret has nowhere to read an account either.
|
||||
for id := range m.ProcessBuses() {
|
||||
name := processSecretName(id)
|
||||
if _, reads := m.OwnSecrets[name]; !reads {
|
||||
return fmt.Errorf("%s's process %s declares bus, and declares no own secret named %s, so there "+
|
||||
"is nowhere to deliver its account; a process with a bus declares \"own-secrets\": "+
|
||||
"{%q: <path>} (novox/hq ADR 0436 §2)", m.Module, id, name, name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSecretName is the own secret a process's bus account is delivered in (novox/hq ADR 0436 §2):
|
||||
// broker-<process id>, never the module's own `broker`.
|
||||
func processSecretName(processID string) string { return "broker-" + processID }
|
||||
|
||||
// issueOnTheNewBus gives an assigned module its credential on the bus being built.
|
||||
//
|
||||
// **Three things differ from a management call, and each is the point of the move.** The credential
|
||||
@@ -968,12 +983,82 @@ func issueOnTheNewBus(ctx context.Context, inv *inventory.Inventory, m catalogue
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Every process with a bus declaration gets an account of its own (novox/hq ADR 0436 §1, §2), minted and
|
||||
// delivered whether or not the module itself speaks on the bus — its own secret is broker-<process id>,
|
||||
// never the module's `broker`.
|
||||
for _, pr := range inventory.ProcessesOf(m) {
|
||||
if err := issueProcess(ctx, inv, m, pr, node, busAddress, known, reachable); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !runsTheProvisionerClient(m) {
|
||||
return nil
|
||||
}
|
||||
return issueProvisionerClient(ctx, inv, m, node, known, reachable)
|
||||
}
|
||||
|
||||
// issueProcess gives one process its account: minted under its own name <node>.<module>.<process id>,
|
||||
// delivered as the module's own secret broker-<process id> in the provisioner client's form (novox/hq ADR
|
||||
// 0436 §2), handed to the vault as any covered account is, and its work-queue consumer or consumers
|
||||
// created where the bus can be reached — never the module's claims, since a process holds no seat.
|
||||
func issueProcess(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest, pr broker.ProcessPrincipal,
|
||||
node, busAddress string, known broker.Broker, reachable string) error {
|
||||
user := broker.Principal{Kind: broker.KindProcess, Node: node, Module: m.Module, Process: pr.Process}.Username()
|
||||
password, err := inv.MintBusPassword(ctx, inventory.BusUser{
|
||||
Username: user, Kind: inventory.BusProcess, Node: node, Module: m.Module,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := processSecretName(pr.Process)
|
||||
held, err := heldProcessAccount(known, reachable, node, m.Module, user, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inv.AcceptSecretForModule(ctx, node, m.Module, name, string(held)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := handIssuedToVault(ctx, inv, m, node, name, string(held)); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pr.Queues) > 0 {
|
||||
if busAddress == "" {
|
||||
fmt.Printf(" %s's process %s consumes its work queue; its consumer is created when the bus is "+
|
||||
"reachable (`push --behind`, then `rollout mint` again is harmless)\n", m.Module, pr.Process)
|
||||
} else {
|
||||
js, err := broker.Dial(busAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the credential is minted and the mesh cannot reach the bus to create "+
|
||||
"%s's process %s's work-queue consumer: %w", m.Module, pr.Process, err)
|
||||
}
|
||||
defer js.Close()
|
||||
who := broker.Principal{Kind: broker.KindProcess, Node: node, Module: m.Module, Process: pr.Process}
|
||||
for _, q := range pr.Queues {
|
||||
if err := js.EnsureConsumer(broker.QueueConsumer(who, q)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("bus user %s minted for %s's process %s, scoped to its bus declaration\n", user, m.Module, pr.Process)
|
||||
fmt.Printf(" sealed to %s as %s. It arrives with the next push — `push %s` to send it\n", node, name, node)
|
||||
return nil
|
||||
}
|
||||
|
||||
// heldProcessAccount is a process's account, in the provisioner client's form (novox/hq ADR 0436 §2):
|
||||
// {url, fingerprint, node, module, user, password} — never claims, since a process holds no seat (§3).
|
||||
func heldProcessAccount(known broker.Broker, reachable, node, module, user, password string) ([]byte, error) {
|
||||
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: "nats://" + reachable, Fingerprint: known.Fingerprint, Node: node, Module: module, User: user,
|
||||
Password: password})
|
||||
}
|
||||
|
||||
// runsTheProvisionerClient says a module runs its machine's provisioner client, which reaches the bus on an
|
||||
// account of its own (novox/hq ADR 0349 clause 11, design 57 P3): it claims the seat and declares the own secret
|
||||
// that account is delivered in. The same test the bus's user list makes (inventory's declaredFor).
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// mailSenderWithProcess is mesh-mail's future manifest shape (novox/hq ADR 0436 §7, §8): the process "send"
|
||||
// declares bus, and its own secret broker-send is where module issue delivers its account (ADR 0436 §2) —
|
||||
// owned by mesh-mail-send, the user resource the process declares, at mode 0600 as every own secret is.
|
||||
func mailSenderWithProcess() catalogue.Manifest {
|
||||
m, err := catalogue.ParseManifest([]byte(`{"module":"mesh-mail","version":"1","emits":["mail.send.sent"],
|
||||
"own-secrets":{"broker":{"path":"/var/lib/mesh-mail/broker"},
|
||||
"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}},
|
||||
"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},
|
||||
{"id":"send","type":"process","run":["./mesh-mail","send"],"user":"mesh-mail-send",
|
||||
"bus":{"emits":["mail.send.sent"]}}]}`))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// 1181/AC1: a process with a bus declaration and no own secret broker-<process id> cannot be issued — the
|
||||
// same refusal mayIssue already gives a module with no `broker` (novox/hq ADR 0436 §2).
|
||||
func TestAProcessWithNoBrokerSecretCannotBeIssued(t *testing.T) {
|
||||
m := mailSenderWithProcess()
|
||||
delete(m.OwnSecrets, "broker-send")
|
||||
err := mayIssue(m)
|
||||
if err == nil {
|
||||
t.Fatal("a process with no broker-<id> own secret was issued an account")
|
||||
}
|
||||
for _, want := range []string{"mesh-mail", "send", "broker-send", "own-secrets"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not say %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
if err := mayIssue(mailSenderWithProcess()); err != nil {
|
||||
t.Errorf("a process declaring its own broker-<id> secret was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1181/AC1: the account held for a process is the provisioner client's form — {url, fingerprint, node,
|
||||
// module, user, password} — named for the process, never the module's own account.
|
||||
func TestAProcesssHeldAccountIsTheProvisionersForm(t *testing.T) {
|
||||
user := broker.Principal{Kind: broker.KindProcess, Node: "novox", Module: "mesh-mail", Process: "send"}.Username()
|
||||
if user != "novox.mesh-mail.send" {
|
||||
t.Fatalf("the process's username: %q", user)
|
||||
}
|
||||
held, err := heldProcessAccount(broker.Broker{Fingerprint: "abc"}, "novox.example:4222", "novox", "mesh-mail",
|
||||
user, "a-password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(held, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]any{"url": "nats://novox.example:4222", "fingerprint": "abc", "node": "novox",
|
||||
"module": "mesh-mail", "user": "novox.mesh-mail.send", "password": "a-password"}
|
||||
for k, v := range want {
|
||||
if got[k] != v {
|
||||
t.Errorf("%s: %v, want %v", k, got[k], v)
|
||||
}
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Errorf("the held account carries %v; want exactly %v (never claims — a process holds no seat)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 1181/AC1: composed, the process's own secret belongs to its own account at 0600 (novox/hq ADR 0436 §1, §2;
|
||||
// the owner mechanism is issue 1170's, proven generically there — this is mesh-mail's own shape).
|
||||
func TestAProcessesOwnSecretIsOwnedByItsOwnAccount(t *testing.T) {
|
||||
m := mailSenderWithProcess()
|
||||
r := catalogue.Resolution{Node: "novox", Modules: []catalogue.Manifest{m}}
|
||||
out, err := r.Declaration(catalogue.Rendering{
|
||||
Needed: map[string]map[string]string{"mesh-mail": {"broker": "sealed-broker", "broker-send": "sealed-account"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("composing the declaration: %v", err)
|
||||
}
|
||||
var found bool
|
||||
for _, res := range out {
|
||||
if res["type"] == "file" && res["path"] == "/var/lib/mesh-mail/broker-send" {
|
||||
found = true
|
||||
if got := res["owner"]; got != "mesh-mail-send" {
|
||||
t.Fatalf("broker-send's owner: %v; want mesh-mail-send", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("broker-send was not composed at all")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user