Forward module issue and an assignment's issue to the vault once one is installed, so the controller mints no bus account and none waits on a node reporting here (issue 774; research 072, the controller NATS-native: gap "vault mints after the lease", the minting piece moves to the vault)
This commit is contained in:
@@ -372,7 +372,7 @@ func TestTheAccountsAModuleSpeaksWithOnANode(t *testing.T) {
|
|||||||
t.Error("sealed no password")
|
t.Error("sealed no password")
|
||||||
}
|
}
|
||||||
return c.sealErr
|
return c.sealErr
|
||||||
})
|
}, func() error { acts.did("after"); return nil })
|
||||||
if (err != nil) != (c.sealErr != nil) {
|
if (err != nil) != (c.sealErr != nil) {
|
||||||
t.Fatalf("error %v, want %v", err, c.sealErr)
|
t.Fatalf("error %v, want %v", err, c.sealErr)
|
||||||
}
|
}
|
||||||
@@ -385,6 +385,52 @@ func TestTheAccountsAModuleSpeaksWithOnANode(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue 774: where a vault is installed the controller mints nothing; `module issue` and an assignment's issue are
|
||||||
|
// forwarded to the vault's issue-account, and what the account needs beside its value still runs. A refusal fails
|
||||||
|
// `module issue` and only says itself on an assignment's issue: the vault mints on the want when it hears it.
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
only bool
|
||||||
|
answer string
|
||||||
|
refuse error
|
||||||
|
want string
|
||||||
|
fails bool
|
||||||
|
}{
|
||||||
|
{"module issue: forwarded, minted by the vault", false, `{"minted":true,"account":{"secret":"own/ace/mesh-build-go/broker","generation":1}}`, nil, "after", false},
|
||||||
|
{"an assignment's issue: forwarded, held already", true, `{"minted":false,"why":"held already"}`, nil, "after", false},
|
||||||
|
{"an assignment's issue: refused, said", true, "", errors.New("nobody wants it yet"), "after", false},
|
||||||
|
{"module issue: refused, failed", false, "", errors.New("nobody wants it"), "", true},
|
||||||
|
} {
|
||||||
|
t.Run("forwarded to the vault: "+c.name, func(t *testing.T) {
|
||||||
|
was := vaultIssuer
|
||||||
|
t.Cleanup(func() { vaultIssuer = was })
|
||||||
|
var asked []string
|
||||||
|
vaultIssuer = func(context.Context, accountIssuer) (vaultAsker, bool, error) {
|
||||||
|
return func(verb string, args map[string]any) (json.RawMessage, error) {
|
||||||
|
asked = append(asked, verb+" "+fmt.Sprint(args["user"]))
|
||||||
|
return json.RawMessage(c.answer), c.refuse
|
||||||
|
}, true, nil
|
||||||
|
}
|
||||||
|
acts := &anAccountStore{}
|
||||||
|
ctx, got := withIssued(t.Context())
|
||||||
|
if c.only {
|
||||||
|
ctx = onlyIfAbsent(ctx)
|
||||||
|
}
|
||||||
|
err := issueAccount(ctx, acts, inventory.BusUser{Username: "ace.mesh-build-go", Kind: inventory.BusModule},
|
||||||
|
func(string) error { acts.did("seal"); return nil }, func() error { acts.did("after"); return nil })
|
||||||
|
if (err != nil) != c.fails || strings.Join(acts.acts, " ") != c.want {
|
||||||
|
t.Fatalf("error %v, acts %q; want failed %v, acts %q (nothing minted, locked or sealed here)", err,
|
||||||
|
strings.Join(acts.acts, " "), c.fails, c.want)
|
||||||
|
}
|
||||||
|
if strings.Join(asked, ",") != "issue-account ace.mesh-build-go" {
|
||||||
|
t.Errorf("the vault was asked %v", asked)
|
||||||
|
}
|
||||||
|
if minted := len(got.list()) == 1; minted != strings.Contains(c.answer, `"minted":true`) {
|
||||||
|
t.Errorf("said issued %v", got.list())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// The accounts of a module that no longer resolves on its node are stale, its provisioner client's among them; a
|
// The accounts of a module that no longer resolves on its node are stale, its provisioner client's among them; a
|
||||||
// module still resolved (assigned or required), an account of another kind, and a node whose resolution is unknown
|
// module still resolved (assigned or required), an account of another kind, and a node whose resolution is unknown
|
||||||
// are left alone (issue 1945: ace's mesh-build-go rows from 17:19, marked sealed by migration 0108).
|
// are left alone (issue 1945: ace's mesh-build-go rows from 17:19, marked sealed by migration 0108).
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import (
|
|||||||
// the vault alone opens it (ADR 0349 clause 8). The vault keeps it as the module's own secret on that machine and
|
// the vault alone opens it (ADR 0349 clause 8). The vault keeps it as the module's own secret on that machine and
|
||||||
// raises `secret-changed`; a re-issue is the same hand with a new value, which replaces the old in place. An account
|
// raises `secret-changed`; a re-issue is the same hand with a new value, which replaces the old in place. An account
|
||||||
// not covered is minted and kept as it was and handed to nobody.
|
// not covered is minted and kept as it was and handed to nobody.
|
||||||
|
//
|
||||||
|
// **Since issue 774 an installed vault mints every account itself** (issueAccount forwards to its issue-account), so
|
||||||
|
// this hand is reached only where the controller still mints: before a vault is installed (where it hands nothing) and
|
||||||
|
// `rollout mint`'s re-issue of a module's account onto a new bus. The vault never lets such a hand replace an account it
|
||||||
|
// minted.
|
||||||
|
|
||||||
// vaultAskerForIssue is the vault as `module issue` asks it. A test replaces it.
|
// vaultAskerForIssue is the vault as `module issue` asks it. A test replaces it.
|
||||||
var vaultAskerForIssue = func(ctx context.Context) vaultAsker { return theVault(ctx) }
|
var vaultAskerForIssue = func(ctx context.Context) vaultAsker { return theVault(ctx) }
|
||||||
|
|||||||
@@ -55,19 +55,24 @@ func TestModuleIssueHandsACoveredAccountToTheVaultAndOnlyThat(t *testing.T) {
|
|||||||
issue(core, "first-core-password")
|
issue(core, "first-core-password")
|
||||||
issue(moved, "first-moved-password")
|
issue(moved, "first-moved-password")
|
||||||
issue(plain, "kept-password")
|
issue(plain, "kept-password")
|
||||||
|
// Issue 774: with the vault installed an issue mints nothing here; it is forwarded to the vault's issue-account.
|
||||||
if err := issueProvisionerClient(ctx, inv, core, "laptop", known, "10.0.0.1:4222"); err != nil {
|
if err := issueProvisionerClient(ctx, inv, core, "laptop", known, "10.0.0.1:4222"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if _, held, err := inv.ModuleSecretIfIssued(ctx, "laptop", "nats", broker.ProvisionerSecret); err != nil || held {
|
||||||
|
t.Errorf("the controller kept a value of its own for the client's account (held %v, %v)", held, err)
|
||||||
|
}
|
||||||
|
|
||||||
said := vault.said()
|
said := vault.said()
|
||||||
for _, want := range []string{`"module":"nats"`, `"module":"moved-bot"`, `"name":"broker"`,
|
client := broker.Principal{Kind: broker.KindProvisioner, Node: "laptop", Module: "nats"}.Username()
|
||||||
`"name":"` + broker.ProvisionerSecret + `"`, `"node":"laptop"`} {
|
for _, want := range []string{`"module":"nats"`, `"module":"moved-bot"`, `"name":"broker"`, `"node":"laptop"`,
|
||||||
|
`"verb":"issue-account"`, `"user":"` + client + `"`} {
|
||||||
if !strings.Contains(said, want) {
|
if !strings.Contains(said, want) {
|
||||||
t.Errorf("the vault was told %s, without %s", said, want)
|
t.Errorf("the vault was told %s, without %s", said, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if n := strings.Count(said, `"origin":"issued"`); n != 3 {
|
if n := strings.Count(said, `"origin":"issued"`); n != 2 {
|
||||||
t.Errorf("%d accounts were handed with origin issued, want 3 (the core's two and the moved module's): %s", n, said)
|
t.Errorf("%d accounts were handed with origin issued, want 2 (the core's and the moved module's): %s", n, said)
|
||||||
}
|
}
|
||||||
if strings.Contains(said, "kept-bot") {
|
if strings.Contains(said, "kept-bot") {
|
||||||
t.Errorf("an account that is neither core nor moved was handed to the vault: %s", said)
|
t.Errorf("an account that is neither core nor moved was handed to the vault: %s", said)
|
||||||
@@ -156,4 +161,8 @@ func TestModuleIssueBeforeTheVaultIsInstalledKeepsTheValueAndHandsNothing(t *tes
|
|||||||
if err := issueWith(ctx, inv, core, "laptop", "", known, "10.0.0.1:4222", "module.nats.laptop", "pw2"); err == nil {
|
if err := issueWith(ctx, inv, core, "laptop", "", known, "10.0.0.1:4222", "module.nats.laptop", "pw2"); err == nil {
|
||||||
t.Error("a vault that is installed and does not answer did not fail the issue")
|
t.Error("a vault that is installed and does not answer did not fail the issue")
|
||||||
}
|
}
|
||||||
|
if err := issueProvisionerClient(ctx, inv, core, "laptop", known, "10.0.0.1:4222"); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "one issuer") {
|
||||||
|
t.Errorf("`module issue` forwarded to a vault that does not answer: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-33
@@ -1018,8 +1018,21 @@ const issueLockedFor = 2 * time.Minute
|
|||||||
// left ace's mesh-build-go with no credential), under the store's lock on the account, so two issuers never interleave
|
// left ace's mesh-build-go with no credential), under the store's lock on the account, so two issuers never interleave
|
||||||
// their look, mint and seal: the hash the bus holds is always the password sealed. A seal that fails leaves the
|
// their look, mint and seal: the hash the bus holds is always the password sealed. A seal that fails leaves the
|
||||||
// account unsealed, issued again by the next issue.
|
// account unsealed, issued again by the next issue.
|
||||||
func issueAccount(ctx context.Context, accounts accountIssuer, u inventory.BusUser, seal func(password string) error) error {
|
//
|
||||||
|
// **Where a vault is installed the controller mints nothing** (issue 774, research 072's gap "vault mints after the
|
||||||
|
// lease"; the controller is deprecated and hands its duties over): the vault is the one issuer of every account the
|
||||||
|
// grants part wants, minted on that want, so the issue is forwarded to the vault's issue-account and only after runs
|
||||||
|
// (what the account needs beside its value: its durable consumers). The controller's store keeps no value for it and its
|
||||||
|
// own user list no hash: no account waits on a node reporting here for the vault to take it. Until a vault is installed
|
||||||
|
// (genesis, ADR 0113 §15 to §18) the controller mints as before.
|
||||||
|
func issueAccount(ctx context.Context, accounts accountIssuer, u inventory.BusUser, seal func(password string) error,
|
||||||
|
after func() error) error {
|
||||||
only, _ := ctx.Value(mintOnlyIfAbsent{}).(bool)
|
only, _ := ctx.Value(mintOnlyIfAbsent{}).(bool)
|
||||||
|
if ask, installed, err := vaultIssuer(ctx, accounts); err != nil {
|
||||||
|
return fmt.Errorf("whether the vault is installed cannot be read, so %s is not issued: %w", u.Username, err)
|
||||||
|
} else if installed {
|
||||||
|
return forwardToTheVault(ctx, ask, u.Username, only, after)
|
||||||
|
}
|
||||||
if !only {
|
if !only {
|
||||||
password, err := accounts.MintBusPassword(ctx, u)
|
password, err := accounts.MintBusPassword(ctx, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1062,6 +1075,63 @@ func issueAccount(ctx context.Context, accounts accountIssuer, u inventory.BusUs
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vaultIssuer answers the vault to forward an issue to, and whether one is installed (vaultInstalled); a store that is
|
||||||
|
// not the controller's inventory (a test's) has none. A test replaces it.
|
||||||
|
var vaultIssuer = func(ctx context.Context, accounts accountIssuer) (vaultAsker, bool, error) {
|
||||||
|
inv, ok := accounts.(*inventory.Inventory)
|
||||||
|
if !ok {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
installed, err := vaultInstalled(ctx, inv)
|
||||||
|
if err != nil || !installed {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return vaultAskerForIssue(ctx), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// issuedByTheVault is the vault's answer to issue-account (mesh-vault heldBackOrMinted).
|
||||||
|
type issuedByTheVault struct {
|
||||||
|
Minted bool `json:"minted"`
|
||||||
|
Why string `json:"why"`
|
||||||
|
Account struct {
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Generation int `json:"generation"`
|
||||||
|
} `json:"account"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardToTheVault asks the vault to issue user (its verb issue-account) and then runs after. The vault mints only
|
||||||
|
// what the grants part wants and never anew a value it holds, so asking twice, or with the want's own mint, is harmless.
|
||||||
|
// An issue on an assignment (only) is not failed by a refusal: the vault mints on the want itself when it is heard, and
|
||||||
|
// the refusal is said; `module issue` fails with it.
|
||||||
|
func forwardToTheVault(ctx context.Context, ask vaultAsker, user string, only bool, after func() error) error {
|
||||||
|
raw, err := ask("issue-account", map[string]any{"user": user})
|
||||||
|
if err != nil {
|
||||||
|
if !only {
|
||||||
|
return fmt.Errorf("the vault is the one issuer of bus accounts and did not issue %s: %w", user, err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s is the vault's to mint, on the grants part's want of it; issue-account said: %v\n", user, err)
|
||||||
|
} else {
|
||||||
|
var got issuedByTheVault
|
||||||
|
if err := json.Unmarshal(raw, &got); err != nil {
|
||||||
|
return fmt.Errorf("the vault's answer to issue-account %s does not read: %w", user, err)
|
||||||
|
}
|
||||||
|
if got.Minted {
|
||||||
|
fmt.Printf("bus user %s minted by the vault as %s (generation %d)\n", user, got.Account.Secret, got.Account.Generation)
|
||||||
|
if g, _ := ctx.Value(issuedNowKey{}).(*issuedNow); g != nil {
|
||||||
|
g.mu.Lock()
|
||||||
|
g.users = append(g.users, user)
|
||||||
|
g.mu.Unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("bus user %s not minted anew by the vault: %s\n", user, got.Why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if after == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return after()
|
||||||
|
}
|
||||||
|
|
||||||
// issueOnTheNewBus gives an assigned module its credential on the bus being built.
|
// 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
|
// **Three things differ from a management call, and each is the point of the move.** The credential
|
||||||
@@ -1090,7 +1160,7 @@ func issueOnTheNewBus(ctx context.Context, inv *inventory.Inventory, m catalogue
|
|||||||
if err := issueAccount(ctx, inv, inventory.BusUser{Username: user, Kind: busKindOf(m.Module), Node: node,
|
if err := issueAccount(ctx, inv, inventory.BusUser{Username: user, Kind: busKindOf(m.Module), Node: node,
|
||||||
Module: m.Module}, func(password string) error {
|
Module: m.Module}, func(password string) error {
|
||||||
return issueWith(ctx, inv, m, node, busAddress, known, reachable, user, password)
|
return issueWith(ctx, inv, m, node, busAddress, known, reachable, user, password)
|
||||||
}); err != nil {
|
}, func() error { return ensureModuleConsumer(m, node, busAddress) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1118,7 +1188,7 @@ func issueProcess(ctx context.Context, inv *inventory.Inventory, m catalogue.Man
|
|||||||
return issueAccount(ctx, inv, inventory.BusUser{Username: user, Kind: inventory.BusProcess, Node: node, Module: m.Module},
|
return issueAccount(ctx, inv, inventory.BusUser{Username: user, Kind: inventory.BusProcess, Node: node, Module: m.Module},
|
||||||
func(password string) error {
|
func(password string) error {
|
||||||
return sealProcess(ctx, inv, m, pr, node, busAddress, known, reachable, user, password)
|
return sealProcess(ctx, inv, m, pr, node, busAddress, known, reachable, user, password)
|
||||||
})
|
}, func() error { return ensureProcessQueues(m, pr, node, busAddress) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// sealProcess seals a process's minted account to its node, hands it to the vault and makes its work-queue consumers.
|
// sealProcess seals a process's minted account to its node, hands it to the vault and makes its work-queue consumers.
|
||||||
@@ -1135,15 +1205,29 @@ func sealProcess(ctx context.Context, inv *inventory.Inventory, m catalogue.Mani
|
|||||||
if err := handIssuedToVault(ctx, inv, m, node, name, string(held)); err != nil {
|
if err := handIssuedToVault(ctx, inv, m, node, name, string(held)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(pr.Queues) > 0 {
|
if err := ensureProcessQueues(m, pr, node, busAddress); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureProcessQueues makes a process's work-queue consumers where the bus can be reached: what its account needs beside
|
||||||
|
// its value, whoever minted it.
|
||||||
|
func ensureProcessQueues(m catalogue.Manifest, pr broker.ProcessPrincipal, node, busAddress string) error {
|
||||||
|
if len(pr.Queues) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if busAddress == "" {
|
if busAddress == "" {
|
||||||
fmt.Printf(" %s's process %s consumes its work queue; its consumer is created when the bus is "+
|
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)
|
"reachable (`push --behind`, then `rollout mint` again is harmless)\n", m.Module, pr.Process)
|
||||||
} else {
|
return nil
|
||||||
|
}
|
||||||
js, err := broker.Dial(busAddress)
|
js, err := broker.Dial(busAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("the credential is minted and the mesh cannot reach the bus to create "+
|
return fmt.Errorf("the mesh cannot reach the bus to create %s's process %s's work-queue consumer: %w",
|
||||||
"%s's process %s's work-queue consumer: %w", m.Module, pr.Process, err)
|
m.Module, pr.Process, err)
|
||||||
}
|
}
|
||||||
defer js.Close()
|
defer js.Close()
|
||||||
who := broker.Principal{Kind: broker.KindProcess, Node: node, Module: m.Module, Process: pr.Process}
|
who := broker.Principal{Kind: broker.KindProcess, Node: node, Module: m.Module, Process: pr.Process}
|
||||||
@@ -1152,10 +1236,6 @@ func sealProcess(ctx context.Context, inv *inventory.Inventory, m catalogue.Mani
|
|||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,7 +1271,7 @@ func issueProvisionerClient(ctx context.Context, inv *inventory.Inventory, m cat
|
|||||||
return issueAccount(ctx, inv, inventory.BusUser{Username: user, Kind: inventory.BusProvisioner, Node: node,
|
return issueAccount(ctx, inv, inventory.BusUser{Username: user, Kind: inventory.BusProvisioner, Node: node,
|
||||||
Module: m.Module}, func(password string) error {
|
Module: m.Module}, func(password string) error {
|
||||||
return sealProvisionerClient(ctx, inv, m, node, known, reachable, user, password)
|
return sealProvisionerClient(ctx, inv, m, node, known, reachable, user, password)
|
||||||
})
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sealProvisionerClient seals the provisioner client's minted account to its machine and hands it to the vault.
|
// sealProvisionerClient seals the provisioner client's minted account to its machine and hands it to the vault.
|
||||||
@@ -1263,29 +1343,9 @@ func issueWith(ctx context.Context, inv *inventory.Inventory, m catalogue.Manife
|
|||||||
if err := handIssuedToVault(ctx, inv, m, node, "broker", string(held)); err != nil {
|
if err := handIssuedToVault(ctx, inv, m, node, "broker", string(held)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := ensureModuleConsumer(m, node, busAddress); err != nil {
|
||||||
// And how it hears what it consumes. Derived from its declaration, and only when it declared
|
|
||||||
// something: a module that consumes nothing needs no consumer, and creating one would be a
|
|
||||||
// durable subscription nobody reads.
|
|
||||||
if consumer, needed := broker.ConsumerFor(broker.Principal{
|
|
||||||
Kind: broker.KindModule, Node: node, Module: m.Module,
|
|
||||||
Emits: m.EmitsAll(), Consumes: m.Consumes, Serves: m.Tools,
|
|
||||||
}); needed {
|
|
||||||
if busAddress == "" {
|
|
||||||
fmt.Printf(" %s consumes; its consumer is created when the bus is reachable (`push --behind`, then "+
|
|
||||||
"`rollout mint` again is harmless)\n", m.Module)
|
|
||||||
} 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 "+
|
|
||||||
"how %s hears what it consumes: %w", m.Module, err)
|
|
||||||
}
|
|
||||||
defer js.Close()
|
|
||||||
if err := js.EnsureConsumer(consumer); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("bus user %s minted for %s, scoped to what it emits and consumes\n", user, m.Module)
|
fmt.Printf("bus user %s minted for %s, scoped to what it emits and consumes\n", user, m.Module)
|
||||||
fmt.Printf(" sealed to %s. It arrives with the next push — `push %s` to send it\n", node, node)
|
fmt.Printf(" sealed to %s. It arrives with the next push — `push %s` to send it\n", node, node)
|
||||||
@@ -1294,6 +1354,30 @@ func issueWith(ctx context.Context, inv *inventory.Inventory, m catalogue.Manife
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureModuleConsumer makes how a module hears what it consumes, where the bus can be reached: what its account needs
|
||||||
|
// beside its value, whoever minted it. Derived from its declaration, and only when it declared something: a module that
|
||||||
|
// consumes nothing needs no consumer, and creating one would be a durable subscription nobody reads.
|
||||||
|
func ensureModuleConsumer(m catalogue.Manifest, node, busAddress string) error {
|
||||||
|
consumer, needed := broker.ConsumerFor(broker.Principal{
|
||||||
|
Kind: broker.KindModule, Node: node, Module: m.Module,
|
||||||
|
Emits: m.EmitsAll(), Consumes: m.Consumes, Serves: m.Tools,
|
||||||
|
})
|
||||||
|
if !needed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if busAddress == "" {
|
||||||
|
fmt.Printf(" %s consumes; its consumer is created when the bus is reachable (`push --behind`, then "+
|
||||||
|
"`rollout mint` again is harmless)\n", m.Module)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
js, err := broker.Dial(busAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("the mesh cannot reach the bus to create how %s hears what it consumes: %w", m.Module, err)
|
||||||
|
}
|
||||||
|
defer js.Close()
|
||||||
|
return js.EnsureConsumer(consumer)
|
||||||
|
}
|
||||||
|
|
||||||
// whereItComesFrom is the provenance a module handed over by hand records, and what a record must
|
// whereItComesFrom is the provenance a module handed over by hand records, and what a record must
|
||||||
// say to be worth anything later.
|
// say to be worth anything later.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -19,17 +19,16 @@ import (
|
|||||||
//
|
//
|
||||||
// review round 1, findings 3 and 4 (1181/AC1, AC2): module issue mints and delivers a process's own
|
// review round 1, findings 3 and 4 (1181/AC1, AC2): module issue mints and delivers a process's own
|
||||||
// account against the store — no bus to dial, busAddress "" — for mesh-mail's real manifest shape (review
|
// account against the store — no bus to dial, busAddress "" — for mesh-mail's real manifest shape (review
|
||||||
// round 1 finding 1: no own secret `broker` at all, its tools served by the tool runner), and issuing it
|
// round 1 finding 1: no own secret `broker` at all, its tools served by the tool runner), as long as no vault is
|
||||||
// again is the path every bus account takes (`rollout mint`'s KindProcess case, and a re-run of `module
|
// installed (genesis). Issue 774: once a vault is installed, the controller mints nothing: the issue is forwarded
|
||||||
// issue`): it rotates the password, and the old hash no longer matches.
|
// to the vault's issue-account for the process's user, and the controller's hash and value stay as they were.
|
||||||
func TestModuleIssueMintsAndDeliversAProcesssAccount(t *testing.T) {
|
func TestModuleIssueMintsAndDeliversAProcesssAccount(t *testing.T) {
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
theVaultIsInstalled(t, open)
|
|
||||||
|
|
||||||
m := mailSenderWithProcess()
|
m := mailSenderWithProcess()
|
||||||
m.Moved = true // so handIssuedToVault's accountIsCovered is true, and the hand-off is exercised whole
|
m.Moved = true
|
||||||
register(t, open, m)
|
register(t, open, m)
|
||||||
|
|
||||||
vault := newStandInVault(t)
|
vault := newStandInVault(t)
|
||||||
@@ -58,30 +57,24 @@ func TestModuleIssueMintsAndDeliversAProcesssAccount(t *testing.T) {
|
|||||||
if err != nil || !held || hash1 == "" {
|
if err != nil || !held || hash1 == "" {
|
||||||
t.Fatalf("the process's account was not minted: held %v, %v", held, err)
|
t.Fatalf("the process's account was not minted: held %v, %v", held, err)
|
||||||
}
|
}
|
||||||
|
// 1181/AC1: kept in the provisioner client's JSON — the url and the fingerprint being exactly what issueProcess
|
||||||
|
// was given above, not a guess at what it computes.
|
||||||
|
sealed, held, err := inv.ModuleSecretIfIssued(ctx, "laptop", "mesh-mail", "broker-send")
|
||||||
|
if err != nil || !held || sealed == "" {
|
||||||
|
t.Fatalf("the process's account is not kept as broker-send: held %v, %v", held, err)
|
||||||
|
}
|
||||||
|
if said := vault.said(); said != "" {
|
||||||
|
t.Errorf("the vault was asked before it was installed: %s", said)
|
||||||
|
}
|
||||||
|
|
||||||
// 1181/AC1: accepted with the provisioner client's JSON, and handed to the vault whole — the url and
|
// Issue 774: with the vault installed, forwarded; nothing minted or rotated here.
|
||||||
// the fingerprint being exactly what issueProcess was given above, not a guess at what it computes.
|
theVaultIsInstalled(t, open)
|
||||||
taken := vault.taken["accept"]
|
|
||||||
for _, want := range []string{`"url":"nats://` + reachable + `"`, `"fingerprint":"ab"`, `"node":"laptop"`,
|
|
||||||
`"module":"mesh-mail"`, `"user":"laptop.mesh-mail.send"`, `"password"`} {
|
|
||||||
if !strings.Contains(taken, want) {
|
|
||||||
t.Errorf("the vault was handed %q, without %s", taken, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !strings.Contains(vault.said(), `"name":"broker-send"`) {
|
|
||||||
t.Errorf("the vault was not told the secret's name broker-send: %s", vault.said())
|
|
||||||
}
|
|
||||||
// The file's own owner (mesh-mail-send, review round 1 finding 7's area) is 1170's mechanism, proven
|
|
||||||
// generically there and for this fixture in TestAProcessesOwnSecretIsOwnedByItsOwnAccount.
|
|
||||||
|
|
||||||
// 1181/AC2: issued again — the path `rollout mint`'s KindProcess case and a re-run of `module issue`
|
|
||||||
// both take — rotates the password; the old hash no longer matches what is recorded.
|
|
||||||
issue()
|
issue()
|
||||||
hash2, held, err := inv.BusUserHash(ctx, user)
|
hash2, held, err := inv.BusUserHash(ctx, user)
|
||||||
if err != nil || !held {
|
if err != nil || !held || hash1 != hash2 {
|
||||||
t.Fatalf("the process's account is gone after a re-issue: held %v, %v", held, err)
|
t.Fatalf("the controller minted again with a vault installed (held %v, %v)", held, err)
|
||||||
}
|
}
|
||||||
if hash1 == hash2 {
|
if said := vault.said(); !strings.Contains(said, `"verb":"issue-account"`) || !strings.Contains(said, `"user":"`+user+`"`) {
|
||||||
t.Fatal("issuing the process's account again did not rotate its password")
|
t.Errorf("the vault was not asked to issue %s: %s", user, said)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ func (v *standInVault) ask(verb string, args map[string]any) (json.RawMessage, e
|
|||||||
// The vault asks the desk's prompt and the provider's tool itself, in its own name (issue 723): one step from
|
// The vault asks the desk's prompt and the provider's tool itself, in its own name (issue 723): one step from
|
||||||
// the controller, which carries no key and no sealed value.
|
// the controller, which carries no key and no sealed value.
|
||||||
switch verb {
|
switch verb {
|
||||||
|
case "issue-account":
|
||||||
|
// The vault is the one issuer once installed (issue 774): the controller forwards, and keeps nothing.
|
||||||
|
if v.refuse != nil {
|
||||||
|
return nil, v.refuse
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]any{"minted": true, "account": map[string]any{"user": args["user"], "generation": 1}})
|
||||||
case "secret-ask":
|
case "secret-ask":
|
||||||
if v.refuse != nil {
|
if v.refuse != nil {
|
||||||
return nil, v.refuse
|
return nil, v.refuse
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
|
||||||
github.com/jackc/pgx/v5 v5.10.0
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYg
|
|||||||
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE=
|
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031083427-de8e82c06f54 h1:xITIabv8ujmwpaEKBiQpkOx6U/ICLTTzX9WWC3DGOPQ=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031083427-de8e82c06f54/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself.
|
|||||||
|
|
||||||
mesh-catalog f3a57dcc56fb6c198095cafd11d0decff9f720c4 modules/*/module.json, modules/*/files, modules/*/templates, predefined.json
|
mesh-catalog f3a57dcc56fb6c198095cafd11d0decff9f720c4 modules/*/module.json, modules/*/files, modules/*/templates, predefined.json
|
||||||
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
|
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
|
||||||
mesh-sdk 2da0de75c3f0087cff9c2f5ef789ca6dc3c59045 conformance/events/module-event.json
|
mesh-sdk acbf80ad910a3492dd9a970ac17f9b29579c4e56 conformance/events/module-event.json
|
||||||
|
|
||||||
To move them, from this repository's root, with the two repositories checked out beside it:
|
To move them, from this repository's root, with the two repositories checked out beside it:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user