From c40e15268e17ecd13910812135591ea3d3c881d2 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:59:06 +0200 Subject: [PATCH] Compose where a declaration was sent: secrets, credentials, acceptances, the hub's peers, adoption, assignments and the bus's user list (issue 1945, B2, B6) Each act that changed what a node runs now writes that node's compositions, and its wants follow by the assigner's rules; a bus upgrade composes the bus's machines and answers the want the operator writes, since the assigner never moves the bus (ADR 0539 clauses 5, 7 and 15). A node-engine may ask its own backup holder before a data module moves. --- cmd/mesh-controller/accept_outside.go | 10 +-- cmd/mesh-controller/acts.go | 23 ++++-- cmd/mesh-controller/adoption.go | 6 +- cmd/mesh-controller/bus_step.go | 82 ++++++++++++++++++++-- cmd/mesh-controller/bus_step_wants_test.go | 33 +++++++++ cmd/mesh-controller/given.go | 10 +-- cmd/mesh-controller/issued.go | 5 +- cmd/mesh-controller/nodes.go | 9 +-- cmd/mesh-controller/own_grants.go | 4 +- cmd/mesh-controller/pending.go | 6 +- cmd/mesh-controller/rotate.go | 21 +++--- cmd/mesh-controller/secret.go | 11 +-- cmd/mesh-controller/states_announced.go | 7 +- internal/broker/composed.go | 17 +++++ internal/broker/composed_test.go | 22 ++++++ internal/broker/nats.go | 6 ++ internal/broker/testdata/composed.conf | 2 +- 17 files changed, 223 insertions(+), 51 deletions(-) create mode 100644 cmd/mesh-controller/bus_step_wants_test.go diff --git a/cmd/mesh-controller/accept_outside.go b/cmd/mesh-controller/accept_outside.go index 2489f542..dd83125e 100644 --- a/cmd/mesh-controller/accept_outside.go +++ b/cmd/mesh-controller/accept_outside.go @@ -112,11 +112,13 @@ func acceptForProvision(ctx context.Context, provider, providerModule, provision if len(accepted.Sealed) == 0 { return nil } - if err := sendTo(ctx, open, accepted.Machines); err != nil { - return fmt.Errorf("%s is sealed and could not be sent: %w; `push %s` sends it", provision, err, - strings.Join(accepted.Machines, "`, `push ")) + // Composed into each consumer's composition (novox/hq ADR 0539 clause 5): a new revision its want then names. + if _, err := composeModules(ctx, open, accepted.Machines); err != nil { + return fmt.Errorf("%s is sealed and its compositions on %s could not be written: %w", provision, + strings.Join(accepted.Machines, ", "), err) } - fmt.Printf(" sent %s; each consumer starts again on it\n", strings.Join(accepted.Machines, ", ")) + fmt.Printf(" composed for %s; each consumer starts again on it once its want names the new composition\n", + strings.Join(accepted.Machines, ", ")) return nil } diff --git a/cmd/mesh-controller/acts.go b/cmd/mesh-controller/acts.go index 9a8cd694..f5c56fd8 100644 --- a/cmd/mesh-controller/acts.go +++ b/cmd/mesh-controller/acts.go @@ -171,8 +171,21 @@ func assignWith(ctx context.Context, open *stores, node string, opts assignOptio answer += "\n but " + catalogue.WrongMachine(u.Module, c, node) } } - return answer + fmt.Sprintf("\n run `push %s` to send it", node) + - blockedElsewhere(ctx, open, node), nil + return answer + composedAfterAct(ctx, open, node) + blockedElsewhere(ctx, open, node), nil +} + +// composedAfterAct composes the node an assignment or its end changed (novox/hq ADR 0539 clause 5): what the +// answer says of it. The assigner then writes the node's wants by its rules; nothing is sent. +func composedAfterAct(ctx context.Context, open *stores, node string) string { + wrote, err := composeModules(ctx, open, []string{node}) + if err != nil { + return fmt.Sprintf("\n but its compositions on %s could not all be written: %v", node, firstLine(err.Error())) + } + if len(wrote[node]) == 0 { + return fmt.Sprintf("\n %s's compositions were already as composed now", node) + } + return fmt.Sprintf("\n composed for %s (%d composition(s) written): its wants follow by the assigner's rules", node, + len(wrote[node])) } // seatDependenciesOnAssign is the refusal ADR 0207 makes at assignment, or nothing, with the @@ -273,8 +286,8 @@ func unassign(ctx context.Context, open *stores, node string, modules ...string) return "", err } } - answer := fmt.Sprintf("%s no longer runs %s — run `push %s` to make it so", - node, strings.Join(modules, ", "), node) + answer := fmt.Sprintf("%s no longer runs %s: its want is written removed by the assigner", + node, strings.Join(modules, ", ")) after := resolvedAfter(ctx, open, shelf, node) for _, line := range unheldChangeTo(shelf, node, assigned, after.modules, after.unheld) { answer += "\n " + line @@ -295,7 +308,7 @@ func unassign(ctx context.Context, open *stores, node string, modules ...string) for _, line := range keptOnUnassign(ctx, open.inventory, node, modules) { answer += "\n " + line } - return answer + blockedElsewhere(ctx, open, node), nil + return answer + composedAfterAct(ctx, open, node) + blockedElsewhere(ctx, open, node), nil } // afterAct is the one resolution of a node an act's answer is said from, taken after its write diff --git a/cmd/mesh-controller/adoption.go b/cmd/mesh-controller/adoption.go index 0ff12403..ad5c3b3a 100644 --- a/cmd/mesh-controller/adoption.go +++ b/cmd/mesh-controller/adoption.go @@ -205,9 +205,9 @@ func adoptedNodes(nodes []inventory.Node) []string { // The operator's acts on an adopted node (novox/hq ADR 0100). Called by the command line and the // command API alike, so a refusal is the same refusal in the same words at both (ADR 0035). -// sendNodes sends the named machines what they should be now. A variable so a test can see what -// an act would send without a broker. -var sendNodes = sendTo +// sendNodes composes the named machines as they should be now (novox/hq ADR 0539 clause 5): each node's want then +// names what changed. A variable so a test can see what an act would compose without a broker. +var sendNodes = composeAndSay // DefaultFilter is the module converging a node assigns to load the mesh's derived filter. const DefaultFilter = "nftables" diff --git a/cmd/mesh-controller/bus_step.go b/cmd/mesh-controller/bus_step.go index f7e6e6f5..d8026078 100644 --- a/cmd/mesh-controller/bus_step.go +++ b/cmd/mesh-controller/bus_step.go @@ -10,8 +10,10 @@ import ( "time" "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" @@ -229,17 +231,85 @@ func busCommand(ctx context.Context, args []string) error { fmt.Printf("bus upgrade %d: %s %s → %s on %s; streams snapshotted at %s; %s\n", step.ID, b.module, step.From, short(b.to), strings.Join(moving, ", "), where, map[bool]string{true: "reversible: putting the old build back undoes it", false: "NOT reversible: the snapshot is the only way back"}[*reversible]) - sent, err := sendToEach(withBusStep(ctx), open, moving) + // **Nothing is sent, and the assigner never moves the bus** (novox/hq ADR 0539 clause 15): the bus's machines are + // composed at the build the mesh holds, and the operator writes each machine's want with the operator's credential, + // recorded, naming that composition. + wants, err := busWantsToWrite(withBusStep(ctx), open, b.module, moving) if err != nil { - _ = inv.EndBusStep(ctx, step.ID, "failed", "the send was refused: "+err.Error()) - return fmt.Errorf("the bus's machine could not be sent its new build: %w — nothing was replaced", err) + _ = inv.EndBusStep(ctx, step.ID, "failed", "the bus's composition could not be written: "+err.Error()) + return fmt.Errorf("the bus's machines could not be composed at its new build: %w — nothing was replaced", err) + } + fmt.Printf("composed %s at the bus's new build. Write each want below with the operator's credential, and record it "+ + "in the hand-act log; `bus-maintenance` is open until the bus answers healthy again — every stream, every durable "+ + "consumer, a round trip to the machines (H-bus) — within %s, or the step is said failed with its snapshot as the "+ + "way back. `bus` says how it went\n", strings.Join(moving, ", "), busStepBound) + for _, w := range wants { + fmt.Println(" " + w) } - fmt.Printf("sent %s; `bus-maintenance` is open until the bus answers healthy again — every stream, every durable "+ - "consumer, a round trip to the machines (H-bus) — within %s, or the step is said failed with its snapshot "+ - "as the way back. `bus` says how it went\n", strings.Join(sent, ", "), busStepBound) return nil } +// busWantsToWrite composes the bus's machines and answers, for each, the want the operator writes: its key in the +// assigner's bucket and its value, naming the fingerprint the bus module is registered at and that machine's +// composition of it. +func busWantsToWrite(ctx context.Context, open *stores, module string, machines []string) ([]string, error) { + if _, err := composeModules(ctx, open, machines); err != nil { + return nil, err + } + fps, err := currentFingerprints(ctx, open.inventory) + if err != nil { + return nil, err + } + bus, err := aBus() + if err != nil { + return nil, err + } + defer bus.Close() + js, err := jetstream.New(bus.Conn()) + if err != nil { + return nil, err + } + digests, err := js.KeyValue(ctx, sdkdelivery.ControllerComposedBucket) + if err != nil { + return nil, err + } + digestOf := func(node string) (string, error) { + key, err := sdkdelivery.ComposedKey(node, module, fps[module]) + if err != nil { + return "", err + } + e, err := digests.Get(ctx, key) + if err != nil { + return "", fmt.Errorf("%s: %w", key, err) + } + v, err := sdkdelivery.DecodeComposedDigest(e.Value()) + return v.Digest, err + } + return busWants(module, fps[module], machines, digestOf) +} + +// busWants is each machine's want for the bus module, as the operator writes it: ` `. +func busWants(module, fp string, machines []string, digestOf func(node string) (string, error)) ([]string, error) { + var out []string + for _, node := range machines { + digest, err := digestOf(node) + if err != nil { + return nil, err + } + key, err := sdkdelivery.WantKey(node, module) + if err != nil { + return nil, err + } + value, err := sdkdelivery.EncodeWant(sdkdelivery.Want{FP: fp, Composed: digest, + Words: "the bus moved by a person's step (bus upgrade, ADR 0539 clause 15)"}) + if err != nil { + return nil, err + } + out = append(out, fmt.Sprintf("%s %s %s", sdkdelivery.AssignerWantsBucket, key, value)) + } + return out, nil +} + // busStatus is `bus`: what an upgrade would do, and the last step. func busStatus(ctx context.Context) error { open, err := openStores(ctx) diff --git a/cmd/mesh-controller/bus_step_wants_test.go b/cmd/mesh-controller/bus_step_wants_test.go new file mode 100644 index 00000000..77647c8d --- /dev/null +++ b/cmd/mesh-controller/bus_step_wants_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "strings" + "testing" + + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// 1945/AC7: a bus upgrade answers, for each machine, the want the operator writes: the assigner's bucket, the +// machine's key for the bus module, and a want naming the bus's fingerprint and that machine's composition of it. It +// sends nothing: nothing it calls publishes on a node's declaration subject (busWants composes no send at all). +func TestABusUpgradeAnswersTheWantTheOperatorWrites(t *testing.T) { + fp := sdkdelivery.SourcePrefix2 + strings.Repeat("b", 64) + digests := map[string]string{"novox": "sha256:" + strings.Repeat("1", 64), "ace": "sha256:" + strings.Repeat("2", 64)} + lines, err := busWants("nats", fp, []string{"ace", "novox"}, func(node string) (string, error) { return digests[node], nil }) + if err != nil || len(lines) != 2 { + t.Fatalf("busWants = %v, %v", lines, err) + } + for i, node := range []string{"ace", "novox"} { + parts := strings.SplitN(lines[i], " ", 3) + if len(parts) != 3 || parts[0] != sdkdelivery.AssignerWantsBucket || parts[1] != "want."+node+".nats" { + t.Fatalf("line %q", lines[i]) + } + w, err := sdkdelivery.DecodeWant([]byte(parts[2])) + if err != nil || w.FP != fp || w.Composed != digests[node] { + t.Errorf("%s's want = %+v, %v", node, w, err) + } + } + if _, err := busWants("nats", fp, []string{"ace"}, func(string) (string, error) { return "", nil }); err == nil { + t.Errorf("a want naming no composition was answered") + } +} diff --git a/cmd/mesh-controller/given.go b/cmd/mesh-controller/given.go index 88aec7c9..30ed5cf6 100644 --- a/cmd/mesh-controller/given.go +++ b/cmd/mesh-controller/given.go @@ -62,12 +62,14 @@ func replaceGiven(ctx context.Context, open *stores, report link.Report) { for _, r := range replaced { said := SecretReplaced{Node: report.Node, Module: r.Module, Name: r.Name, Given: r.Given.UTC(), Sent: r.Machines, Why: givenWhy} - log.Printf("replaced %q of %s on %s, given %s, with a value the mesh made: %s; sending %s", + log.Printf("replaced %q of %s on %s, given %s, with a value the mesh made: %s; composing %s", r.Name, r.Module, report.Node, r.Given.UTC().Format(time.RFC3339), givenWhy, strings.Join(r.Machines, ", ")) - if err := sendTo(ctx, open, r.Machines); err != nil { + // Composed (novox/hq ADR 0539 clause 5): the compositions that use it get a new revision, which each node's + // want then names. + if _, err := composeModules(ctx, open, r.Machines); err != nil { said.Sent, said.Unsent = nil, err.Error() - log.Printf("the new %q of %s is sealed and not yet delivered to %s — the next push carries it: %v", - r.Name, r.Module, strings.Join(r.Machines, ", "), err) + log.Printf("the new %q of %s is sealed and its compositions on %s are not yet written — the next act that "+ + "composes them carries it: %v", r.Name, r.Module, strings.Join(r.Machines, ", "), err) } stateReplaced(ctx, said) } diff --git a/cmd/mesh-controller/issued.go b/cmd/mesh-controller/issued.go index 4c553807..9f1a2ece 100644 --- a/cmd/mesh-controller/issued.go +++ b/cmd/mesh-controller/issued.go @@ -315,7 +315,10 @@ func secretIssued(ctx context.Context, args []string) error { return err }) }, - send: func(machines []string) error { return sendTo(ctx, open, machines) }, + send: func(machines []string) error { + _, err := composeModules(ctx, open, machines) + return err + }, ofTheVault: filledTheNewWay, vault: theVault(ctx), } diff --git a/cmd/mesh-controller/nodes.go b/cmd/mesh-controller/nodes.go index 70f275f4..f1ae066f 100644 --- a/cmd/mesh-controller/nodes.go +++ b/cmd/mesh-controller/nodes.go @@ -616,10 +616,11 @@ func throughTheTunnel(ctx context.Context, open *stores, node inventory.Node, ke } } } - if err := sendTo(ctx, open, []string{hub.Name}); err != nil { - return nil, "", fmt.Errorf("%s was made a peer of the hub, and the hub could not be sent "+ - "it, so the tunnel would not answer — the token is not shown; issue it again once %s "+ - "can be pushed: %w", node.Name, hub.Name, err) + // The hub's own composition carries the private network (novox/hq ADR 0539 clause 5), its want then names it. + if _, err := composeModules(ctx, open, []string{hub.Name}); err != nil { + return nil, "", fmt.Errorf("%s was made a peer of the hub, and the hub's composition could not be "+ + "written, so the tunnel would not answer — the token is not shown; issue it again once %s "+ + "composes: %w", node.Name, hub.Name, err) } // An address, not a name — nothing resolves before the machine has joined (novox/hq ADR 0004). return &token.Tunnel{ diff --git a/cmd/mesh-controller/own_grants.go b/cmd/mesh-controller/own_grants.go index b4fe9a5b..5588e2f9 100644 --- a/cmd/mesh-controller/own_grants.go +++ b/cmd/mesh-controller/own_grants.go @@ -62,7 +62,9 @@ func ownGrantsOf(open *stores) ownGrantsCarrier { return ownGrantsCarrier{ behind: func(ctx context.Context) (string, bool, error) { return brokerBehind(ctx, open, nil) }, send: func(ctx context.Context, holder string) error { - _, err := sendToEach(ctx, open, []string{holder}) + // The user list is a file of the bus module's composition (novox/hq ADR 0539 clause 7): composed, and the + // assigner writes the bus node's want for that revision at once. + _, err := composeModules(ctx, open, []string{holder}) return err }, } diff --git a/cmd/mesh-controller/pending.go b/cmd/mesh-controller/pending.go index b2c63231..93757976 100644 --- a/cmd/mesh-controller/pending.go +++ b/cmd/mesh-controller/pending.go @@ -435,9 +435,9 @@ func applyPending(ctx context.Context, open *stores, p inventory.PendingAssignme case strings.Contains(answer, "already runs"): note = fmt.Sprintf("%s already runs %s: the pending assignment is met", p.Node, p.Module) default: - // The same as a person's assign: nothing is sent by this act. - note = fmt.Sprintf("%s is assigned %s: build %s registered it (pending since %s); `push %s` sends it", - p.Node, p.Module, by, clock(p.Since), p.Node) + // The same as a person's assign: composed, and its want follows by the assigner's rules. + note = fmt.Sprintf("%s is assigned %s: build %s registered it (pending since %s); composed, its want follows", + p.Node, p.Module, by, clock(p.Since)) } settled, serr := inv.SettlePending(ctx, p.ID, inventory.PendingApplying, state, note) if serr != nil { diff --git a/cmd/mesh-controller/rotate.go b/cmd/mesh-controller/rotate.go index 3ecdc25d..a05fd04c 100644 --- a/cmd/mesh-controller/rotate.go +++ b/cmd/mesh-controller/rotate.go @@ -142,20 +142,19 @@ func rotateCommand(ctx context.Context, args []string) error { } } - // **Both ends, in one send.** There is a window either way — a role's password changes on the - // provider and the file changes on the consumer, and they cannot be simultaneous — so the - // honest thing is to make it as short as the broker allows and to never leave it open across - // a command boundary, where it depends on somebody's memory. - fmt.Printf("\nsending to both ends:\n") - if err := sendTo(ctx, open, machines); err != nil { + // **Both ends, composed together** (novox/hq ADR 0539 clause 5). There is a window either way — a role's + // password changes on the provider and the file changes on the consumer, and they cannot be simultaneous — + // so both ends' compositions are written in one act, never left across a command boundary. + fmt.Printf("\ncomposing both ends:\n") + if _, err := composeModules(ctx, open, machines); err != nil { return fmt.Errorf( - "%w\n\nThe old credentials are gone from the mesh and the new ones have not arrived. "+ - "Nothing on those machines has changed yet, so what is running keeps working "+ - "until the provider next applies. Fix the cause and run `push --behind`", err) + "%w\n\nThe old credentials are gone from the mesh and the new compositions are not all written. "+ + "Nothing on those machines has changed yet, so what is running keeps working until each "+ + "node applies the composition its want names. Fix the cause and rotate again", err) } - fmt.Printf("\n%d machine(s) told. Until both ends have applied, a consumer whose password "+ - "changed cannot authenticate — `status` says who is still behind\n", len(machines)) + fmt.Printf("\n%d machine(s) composed. Until both ends have applied what their wants name, a consumer whose "+ + "password changed cannot authenticate — the delivery view says each machine's want and have\n", len(machines)) return nil } diff --git a/cmd/mesh-controller/secret.go b/cmd/mesh-controller/secret.go index f2d5aae9..2b6dd79c 100644 --- a/cmd/mesh-controller/secret.go +++ b/cmd/mesh-controller/secret.go @@ -651,13 +651,14 @@ func rotateKept(ctx context.Context, node, module, name string, why handActFlags machines = []string{node} } if len(machines) == 1 { - fmt.Printf("sending %s, so %s starts again on the new value:\n", node, module) + fmt.Printf("composing %s, so %s starts again on the new value once its want names it:\n", node, module) } else { - fmt.Printf("shared with every consumer; sending %s together:\n", strings.Join(machines, ", ")) + fmt.Printf("shared with every consumer; composing %s together:\n", strings.Join(machines, ", ")) } - if err := sendTo(ctx, open, machines); err != nil { - return fmt.Errorf("%w\n\nThe new value is sealed and not yet delivered; what runs keeps the old "+ - "one until the machines next apply. Fix the cause and run `push --behind`", err) + // Composed (novox/hq ADR 0539 clause 5): a new revision of the compositions that use it. + if _, err := composeModules(ctx, open, machines); err != nil { + return fmt.Errorf("%w\n\nThe new value is sealed and its compositions are not all written; what runs "+ + "keeps the old one. Fix the cause and move it again", err) } return nil } diff --git a/cmd/mesh-controller/states_announced.go b/cmd/mesh-controller/states_announced.go index 0d15efb4..d399528e 100644 --- a/cmd/mesh-controller/states_announced.go +++ b/cmd/mesh-controller/states_announced.go @@ -191,11 +191,12 @@ func composeAnnouncedReads(ctx context.Context, open *stores) { if holder == "" || !behind { return } - if _, err := sendToEach(ctx, open, []string{holder}); err != nil { - fmt.Printf("states: the user list the states announced compose could not be sent to %s: %v\n", holder, err) + // A file of the bus module's composition (novox/hq ADR 0539 clause 7), written into the bus node's want at once. + if _, err := composeModules(ctx, open, []string{holder}); err != nil { + fmt.Printf("states: the user list the states announced compose could not be composed for %s: %v\n", holder, err) return } - fmt.Printf("states: sent %s, which holds the bus, the user list the states announced compose\n", holder) + fmt.Printf("states: composed for %s, which holds the bus, the user list the states announced compose\n", holder) } // notYetAnnounced are the owners some module reads the state of and that have announced nothing yet: what `status` diff --git a/internal/broker/composed.go b/internal/broker/composed.go index 4791d87a..1c3fbeb2 100644 --- a/internal/broker/composed.go +++ b/internal/broker/composed.go @@ -96,3 +96,20 @@ func ComposedReads(node string) ([]string, error) { } return append(readGrants(bucket, ""), readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...), nil } + +// VerbsTheNodeAgentAsks are the seat verbs a node's node-engine calls of its own machine while it applies a want (ADR +// 0539 clause 16, ADR 0429 §14): its backup holder backs a data module up before it moves, and says when it was. Asked +// of its own machine only, `mesh.seat..tool..`. +var VerbsTheNodeAgentAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}, {Seat: "node-backup", Verb: "backed-up"}} + +// NodeAgentAsks are the subjects of VerbsTheNodeAgentAsks for one node. +func NodeAgentAsks(node string) ([]string, error) { + if !safeSubject.MatchString(node) { + return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node) + } + var out []string + for _, v := range VerbsTheNodeAgentAsks { + out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb+"."+node) + } + return out, nil +} diff --git a/internal/broker/composed_test.go b/internal/broker/composed_test.go index 95489842..8b3569bd 100644 --- a/internal/broker/composed_test.go +++ b/internal/broker/composed_test.go @@ -47,3 +47,25 @@ func TestANodeReadsOnlyItsOwnCompositions(t *testing.T) { t.Errorf("a node name with a wildcard was granted") } } + +// 1945 (for issue 1944): a node-engine asks its own machine's backup holder to back a data module up before it moves +// and when it last was, and no other machine's. +func TestANodeAgentAsksItsOwnBackupHolder(t *testing.T) { + asks, err := NodeAgentAsks("ace") + if err != nil || strings.Join(asks, " ") != "mesh.seat.node-backup.tool.now.ace mesh.seat.node-backup.tool.backed-up.ace" { + t.Errorf("NodeAgentAsks(ace) = %v, %v", asks, err) + } + p, err := PermissionsFor(Principal{Kind: KindNode, Node: "ace", PasswordHash: "x"}) + if err != nil { + t.Fatal(err) + } + all := strings.Join(p.Publish, " ") + for _, want := range append(asks, "$JS.API.DIRECT.GET.KV_composed-ace.>") { + if !strings.Contains(all, want) { + t.Errorf("a node-engine of ace may not publish %s", want) + } + } + if strings.Contains(all, "node-backup.tool.now.*") || strings.Contains(all, "KV_composed-g14") { + t.Errorf("a node-engine of ace reaches another machine: %v", p.Publish) + } +} diff --git a/internal/broker/nats.go b/internal/broker/nats.go index 29c48bc0..af7d26a0 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -865,6 +865,12 @@ func PermissionsFor(p Principal) (Permissions, error) { return Permissions{}, err } pub = append(pub, composedReads...) + // And its own machine's backup holder, before a data module moves (ADR 0539 clause 16). + backups, err := NodeAgentAsks(p.Node) + if err != nil { + return Permissions{}, err + } + pub = append(pub, backups...) // And, while the vault serves it, the vault's move-in in its own name and nobody else's (issue 676): the // values sealed to this machine, which only it opens, handed in sealed to the vault's key for the call. if p.MovesIntoTheVault { diff --git a/internal/broker/testdata/composed.conf b/internal/broker/testdata/composed.conf index 5e67cf01..90941656 100644 --- a/internal/broker/testdata/composed.conf +++ b/internal/broker/testdata/composed.conf @@ -38,7 +38,7 @@ accounts { subscribe: { allow: ["_INBOX.enrol.one.>"] } } } { user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: { - publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>"] } + publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one"] } subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] } allow_responses: { max: 1, ttl: "1m" } } }