Merge pull request 'An unissued bus account holds back only its own module, never the whole node' (#490) from fix/vault-minted-accounts into main
This commit was merged in pull request #490.
This commit is contained in:
@@ -87,7 +87,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
}
|
||||
|
||||
// The node's own secrets: a module's superuser password, an administrator, an account. A bus credential nobody issued is
|
||||
// refused before this (unissuedBusCredential), a given value under an old key by the composition.
|
||||
// held out of the composition before this (holdBackUnissuedBusAccounts), a given value under an old key by the composition.
|
||||
for _, one := range d.StoodIn {
|
||||
module, name, ok := strings.Cut(one, "/")
|
||||
if !ok || strings.Contains(one, " ") {
|
||||
|
||||
@@ -184,11 +184,10 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos
|
||||
if err := refusedBus(in, node); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
if err := unissuedBusCredential(in, node, d); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
held := holdBackUnissuedBusAccounts(in, node, &d)
|
||||
sayFound(ctx, d)
|
||||
out := sendableOf(d)
|
||||
out.heldForCredential = held
|
||||
out.fileReferences = readsFileReferences(in.Input, node)
|
||||
if !withOrder {
|
||||
out.Sequence, out.Epoch, out.Generation = 0, 0, 0
|
||||
@@ -215,12 +214,19 @@ func refusedBus(in *meshInput, node string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unissuedBusCredential refuses a composition that stands in for a bus credential nobody issued: the module's `broker` own
|
||||
// secret (its account `<node>.<module>`) or the provisioner client's (`provisioner.<node>`). A bus credential is minted by
|
||||
// `module issue`, never invented: a send that made one would seal a placeholder its process cannot read (novox/hq issue
|
||||
// 203). mesh-sdk's composition stands one in, in the Foreseeing mode, since a node's answer to a proposal is asked before
|
||||
// the vault mints it (mesh-sdk #141, issue 774), so the send refuses it here, in the words the composition used to.
|
||||
func unissuedBusCredential(in *meshInput, node string, d compose.Declared) error {
|
||||
// holdBackUnissuedBusAccounts holds out of d every module that stands in for a bus credential nobody issued: the module's
|
||||
// `broker` own secret (its account `<node>.<module>`) or the provisioner client's (`provisioner.<node>`). A bus credential is
|
||||
// minted, never invented: a send that made one would seal a placeholder its process cannot read (novox/hq issue 203).
|
||||
// mesh-sdk's composition stands one in, in the Foreseeing mode, since a node's answer to a proposal is asked before the vault
|
||||
// mints it (mesh-sdk #141, issue 774).
|
||||
//
|
||||
// **That one module is held, never the node.** Its resources, its stand-ins and its build leave this composition, it is named
|
||||
// among the left out with issue 203's words (said where the composition's other lines go), and the rest of the node composes:
|
||||
// a refusal of the whole node stopped every take-in on the machine that runs the bus for one account (2026-11-01, novox's
|
||||
// mesh-hand-acts, which the vault had in fact minted; the controller cannot read the vault's accounts yet, so such a module
|
||||
// stays held until its account is in the controller's store). The modules held are answered, sorted.
|
||||
func holdBackUnissuedBusAccounts(in *meshInput, node string, d *compose.Declared) []string {
|
||||
why := map[string]string{}
|
||||
for _, one := range d.StoodIn {
|
||||
module, name, ok := strings.Cut(one, "/")
|
||||
if !ok || strings.Contains(one, " ") {
|
||||
@@ -235,13 +241,53 @@ func unissuedBusCredential(in *meshInput, node string, d compose.Declared) error
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(in.Input.BusMinted, user) {
|
||||
return fmt.Errorf("%s on %s has no bus credential: nothing was issued for %s, and a push would seal a "+
|
||||
"placeholder its process cannot read (novox/hq issue 203). `module issue %s --node %s`, then push again",
|
||||
module, node, user, module, node)
|
||||
if _, said := why[module]; said || slices.Contains(in.Input.BusMinted, user) {
|
||||
continue
|
||||
}
|
||||
why[module] = fmt.Sprintf("it has no bus credential: nothing was issued for %s, and a push would seal a placeholder "+
|
||||
"its process cannot read (novox/hq issue 203). `module issue %s --node %s`, then push again", user, module, node)
|
||||
}
|
||||
if len(why) == 0 {
|
||||
return nil
|
||||
}
|
||||
held := make([]string, 0, len(why))
|
||||
for module := range why {
|
||||
held = append(held, module)
|
||||
}
|
||||
sort.Strings(held)
|
||||
ofHeld := func(s string) bool {
|
||||
module, _, _ := strings.Cut(s, "/")
|
||||
_, out := why[module]
|
||||
return out
|
||||
}
|
||||
kept := d.Resources[:0:0]
|
||||
for _, r := range d.Resources {
|
||||
module := d.ModuleOf[fmt.Sprint(r["id"])]
|
||||
if module == "" {
|
||||
module, _ = r["module"].(string)
|
||||
}
|
||||
if _, out := why[module]; !out {
|
||||
kept = append(kept, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
d.Resources = kept
|
||||
d.StoodIn = slices.DeleteFunc(d.StoodIn, ofHeld)
|
||||
d.Foreseen = slices.DeleteFunc(d.Foreseen, ofHeld)
|
||||
d.Modules = slices.DeleteFunc(d.Modules, func(m string) bool { _, out := why[m]; return out })
|
||||
if d.LeftOutWhy == nil {
|
||||
d.LeftOutWhy = map[string]string{}
|
||||
}
|
||||
for _, module := range held {
|
||||
delete(d.BuildIDs, module)
|
||||
d.LeftOutWhy[module] = why[module]
|
||||
if !slices.Contains(d.LeftOut, module) {
|
||||
d.LeftOut = append(d.LeftOut, module)
|
||||
}
|
||||
d.Said = append(d.Said, fmt.Sprintf("%s on %s is held out of %s's composition, and the rest of %s composes: %s",
|
||||
module, node, node, node, why[module]))
|
||||
}
|
||||
sort.Strings(d.LeftOut)
|
||||
return held
|
||||
}
|
||||
|
||||
// markUnresolvable is an error of a node whose own set does not compose, as planFor marked it.
|
||||
@@ -288,10 +334,9 @@ func composeSendValidated(ctx context.Context, open *stores, node string) (senda
|
||||
if err := refusedBus(in, node); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
// Before perform makes what the composition stands in for: a bus credential is never one of them.
|
||||
if err := unissuedBusCredential(in, node, d); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
// Before perform makes what the composition stands in for: a bus credential is never one of them. The module
|
||||
// whose account nobody issued is held out of this composition, and the rest of the node composes.
|
||||
held := holdBackUnissuedBusAccounts(in, node, &d)
|
||||
if len(byController) > 0 {
|
||||
if d, problems, err = sealedByTheController(in, node, d, byController); err != nil {
|
||||
return sendable{}, nil, err
|
||||
@@ -308,6 +353,7 @@ func composeSendValidated(ctx context.Context, open *stores, node string) (senda
|
||||
if len(left) == 0 && len(in.wouldMake) == 0 {
|
||||
sayFound(ctx, d)
|
||||
out := sendableOf(d)
|
||||
out.heldForCredential = held
|
||||
out.fileReferences = readsFileReferences(in.Input, node)
|
||||
// The sender stamps the order it allots (order.stamp); a composition for a send carries none of its own.
|
||||
out.Sequence, out.Epoch, out.Generation = 0, 0, 0
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the
|
||||
@@ -85,3 +86,73 @@ func TestADeclarationThatStandsInForAValueIsStillForeseen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2026-11-01 (issue 203's refusal after the 1585 group): a module standing in for a bus account nobody issued is held out of
|
||||
// the composition, alone; the rest of the node composes, with no stand-in left. An issued account holds nothing. And a held
|
||||
// module with no fingerprint of the new form keeps the node's own composition as last written.
|
||||
func TestAnUnissuedBusAccountHoldsItsModuleAloneAndNeverTheNode(t *testing.T) {
|
||||
declared := func() compose.Declared {
|
||||
return compose.Declared{
|
||||
Resources: []map[string]any{
|
||||
{"id": "hands.broker", "type": "file", "sealed": catalogue.ForeseenSealed},
|
||||
{"id": "hands.unit", "type": "service"},
|
||||
{"id": "client.broker", "type": "file", "sealed": catalogue.ForeseenSealed},
|
||||
{"id": "web.config", "type": "file", "content": "ok"},
|
||||
},
|
||||
ModuleOf: map[string]string{"hands.broker": "hands", "hands.unit": "hands", "client.broker": "client",
|
||||
"web.config": "web"},
|
||||
BuildIDs: map[string]string{"hands": "b-1", "client": "b-2", "web": "b-3"},
|
||||
Modules: []string{"client", "hands", "web"},
|
||||
StoodIn: []string{"hands/broker", "client/" + compose.ProvisionerSecret},
|
||||
Foreseen: []string{"hands/broker", "client/" + compose.ProvisionerSecret},
|
||||
}
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
minted []string
|
||||
held []string
|
||||
}{
|
||||
"issued by nobody": {held: []string{"client", "hands"}},
|
||||
"the module's account issued": {minted: []string{"anchor.hands"}, held: []string{"client"}},
|
||||
"both issued": {minted: []string{"anchor.hands", "provisioner.anchor"}},
|
||||
} {
|
||||
in := &meshInput{Input: compose.Input{BusMinted: c.minted}}
|
||||
d := declared()
|
||||
held := holdBackUnissuedBusAccounts(in, "anchor", &d)
|
||||
if !reflect.DeepEqual(held, c.held) {
|
||||
t.Errorf("%s: held %v, want %v", name, held, c.held)
|
||||
continue
|
||||
}
|
||||
if len(c.held) < 2 {
|
||||
continue // an issued account's stand-in is the send's to make (perform), not this one's
|
||||
}
|
||||
if left := stillForeseen(d); len(left) != 0 {
|
||||
t.Errorf("%s: the rest of the node still stands in for %v", name, left)
|
||||
}
|
||||
if !reflect.DeepEqual(d.Modules, []string{"web"}) || len(d.Resources) != 1 || d.BuildIDs["hands"] != "" {
|
||||
t.Errorf("%s: the rest of the node is not what composes: %v %v %v", name, d.Modules, d.Resources, d.BuildIDs)
|
||||
}
|
||||
if !reflect.DeepEqual(d.LeftOut, []string{"client", "hands"}) ||
|
||||
!strings.Contains(d.LeftOutWhy["hands"], "module issue hands --node anchor") ||
|
||||
!strings.Contains(d.LeftOutWhy["client"], "provisioner.anchor") || len(d.Said) != 2 {
|
||||
t.Errorf("%s: the held modules are not said in issue 203's words: %v %v %v", name, d.LeftOut, d.LeftOutWhy, d.Said)
|
||||
}
|
||||
}
|
||||
|
||||
own, err := sdkdelivery.ComposedNodeKey("anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
fps map[string]string
|
||||
keepOwn bool
|
||||
}{
|
||||
"a held module of the new form": {fps: map[string]string{"hands": sdkdelivery.SourcePrefix2 + "x"}, keepOwn: true},
|
||||
"a held module of the former form": {fps: map[string]string{}},
|
||||
} {
|
||||
comps := map[string]sdkdelivery.Composed{own: {Node: "anchor"}}
|
||||
said := withoutNodeOwnForHeld("anchor", comps, c.fps, []string{"hands"})
|
||||
if _, kept := comps[own]; kept != c.keepOwn || (said == "") != c.keepOwn {
|
||||
t.Errorf("%s: the node's own composition kept %v (want %v), said %q", name, kept, c.keepOwn, said)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ func composeModulesByNode(ctx context.Context, open *stores, nodes []string) (ma
|
||||
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||
return
|
||||
}
|
||||
if out := withoutNodeOwnForHeld(node, comps, fps, declared.heldForCredential); out != "" {
|
||||
fmt.Printf("compositions: %s\n", out)
|
||||
}
|
||||
if len(moved) > 0 {
|
||||
fmt.Printf("compositions: %s's %s compose with the node's own: no fingerprint of the new form names them\n",
|
||||
node, strings.Join(moved, ", "))
|
||||
|
||||
@@ -486,3 +486,29 @@ func controllerSigner(ctx context.Context, open *stores) (func(sdkdelivery.Compo
|
||||
return sdkdelivery.SignComposedBy(c, public, func(m []byte) ([]byte, error) { return ident.Sign(ctx, m) })
|
||||
}, nil
|
||||
}
|
||||
|
||||
// withoutNodeOwnForHeld takes the node's own composition out of comps when a module held for a bus credential nobody
|
||||
// issued (holdBackUnissuedBusAccounts) has no fingerprint of the new form: its resources would have been in the node's own,
|
||||
// and the node's own written without them would have the node-engine remove a module that may be running. The node's
|
||||
// own stays as last written; every module's composition is written. Answers what it said, "" when nothing was taken.
|
||||
func withoutNodeOwnForHeld(node string, comps map[string]sdkdelivery.Composed, fps map[string]string, held []string) string {
|
||||
var old []string
|
||||
for _, module := range held {
|
||||
if !strings.HasPrefix(fps[module], sdkdelivery.SourcePrefix2) {
|
||||
old = append(old, module)
|
||||
}
|
||||
}
|
||||
if len(old) == 0 {
|
||||
return ""
|
||||
}
|
||||
own, err := sdkdelivery.ComposedNodeKey(node)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if _, ok := comps[own]; !ok {
|
||||
return ""
|
||||
}
|
||||
delete(comps, own)
|
||||
return fmt.Sprintf("%s's own composition is not written this pass: %s, held for a bus account nobody issued, would "+
|
||||
"leave it, and the node-engine would remove what may be running", node, strings.Join(old, ", "))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -39,17 +40,23 @@ func TestAssigningAModuleThatSpeaksOnTheBusNamesItsCredential(t *testing.T) {
|
||||
t.Fatalf("an assignment whose credential could not be issued does not name the verb:\n%s", said)
|
||||
}
|
||||
|
||||
// And the push refuses to send it, naming the same verb, rather than sealing a placeholder.
|
||||
// And the push holds talker out, naming the same verb, rather than sealing a placeholder; the rest of the
|
||||
// machine composes (2026-11-01: one unissued account refused the whole node that runs the bus).
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = declarationFor(ctx, open, "laptop", plan, settings)
|
||||
if err == nil {
|
||||
t.Fatal("a push sealed a placeholder where talker's bus credential belongs")
|
||||
held, err := declarationFor(ctx, open, "laptop", plan, settings)
|
||||
if err != nil {
|
||||
t.Fatalf("one unissued account refused the whole machine: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "module issue talker --node laptop") || !strings.Contains(err.Error(), "issue 203") {
|
||||
t.Fatalf("the refusal does not say what to run: %v", err)
|
||||
why := held.leftOutWhy["talker"]
|
||||
if !slices.Contains(held.LeftOut, "talker") || !strings.Contains(why, "module issue talker --node laptop") ||
|
||||
!strings.Contains(why, "issue 203") {
|
||||
t.Fatalf("talker is not held out in issue 203's words: %v %q", held.LeftOut, why)
|
||||
}
|
||||
if body, err := held.Body(); err != nil || strings.Contains(string(body), catalogue.ForeseenSealed) {
|
||||
t.Fatalf("a push would seal a placeholder where talker's bus credential belongs: %v\n%s", err, body)
|
||||
}
|
||||
|
||||
// Once the user is minted, the push goes on to the credential the mesh sealed, and re-assigning
|
||||
|
||||
@@ -27,9 +27,10 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
|
||||
Username: "laptop.mesh-provisioner", Kind: inventory.BusModule, Node: "laptop", Module: "mesh-provisioner"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The composition refuses it in the send's words (mesh-sdk's go/compose: TestTheSendsRefusalsAreRefusedInTheSendsWords).
|
||||
if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err == nil || !strings.Contains(err.Error(), "provisioner.laptop") {
|
||||
t.Fatalf("a push would seal a placeholder for the client's account before it was issued: %v", err)
|
||||
// The composition holds the module out in the send's words, and composes the rest of the machine.
|
||||
if held, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil ||
|
||||
!strings.Contains(held.leftOutWhy[m.Module], "provisioner.laptop") {
|
||||
t.Fatalf("a push would seal a placeholder for the client's account before it was issued: %v %v", err, held.leftOutWhy)
|
||||
}
|
||||
|
||||
if err := issueProvisionerClient(ctx, inv, m, "laptop", broker.Broker{Fingerprint: "ab"}, "10.0.0.1:4222"); err != nil {
|
||||
@@ -45,8 +46,9 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
|
||||
if !slices.ContainsFunc(states, func(s inventory.SecretState) bool { return s.Name == broker.ProvisionerSecret }) {
|
||||
t.Fatalf("the client's account is not sealed to the machine for its service: %+v", states)
|
||||
}
|
||||
if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil && strings.Contains(err.Error(), "provisioner.laptop") {
|
||||
t.Fatalf("an issued client account is refused: %v", err)
|
||||
if held, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil ||
|
||||
slices.Contains(held.heldForCredential, m.Module) {
|
||||
t.Fatalf("an issued client account is held: %v %v", err, held.leftOutWhy)
|
||||
}
|
||||
if !runsTheProvisionerClient(m) {
|
||||
t.Fatal("the module is not read as running the client")
|
||||
|
||||
@@ -73,6 +73,9 @@ type sendable struct {
|
||||
LeftOut []string
|
||||
// leftOutWhy is why each was, for push and plan to say; never on the wire.
|
||||
leftOutWhy map[string]string
|
||||
// heldForCredential are the modules held out because their bus account was issued by nobody
|
||||
// (holdBackUnissuedBusAccounts), sorted; never on the wire.
|
||||
heldForCredential []string
|
||||
// withheld is every consumer this machine's grants leave out, because its identity overflows the
|
||||
// provision's bound (novox/hq ADR 0225); for push and plan to say, never on the wire.
|
||||
withheld []catalogue.Overflow
|
||||
|
||||
Reference in New Issue
Block a user