Never make a credential only a person can give, and give it once per provider (hq issue 164, ADR 0337)
mesh/merge-gate head cfe63a4738ae merged into trunk 404b4409e70e — fail: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; head c…
mesh/repo-check head cfe63a4738ae merged into trunk 404b4409e70e — fail: head cfe63a4738ae does not merge cleanly into trunk 404b4409e70e: in conflict test…
mesh/delivery superseded: a newer head of the same pull request

A Servarr app's API key, an indexer's key and a downloader's password are values the provider's
software holds; the pair path made a random value for every consumer of them, sealed it to both ends
and reported success, and the provider refused it. An offer may now say `"credential": {"issued-by":
"outside"}`: SecretFor never makes a value for it, the plan reads such a pair as given or not given
and sends the consumer no credential file until a person gives it, without refusing its machine;
remakeShared and SharedSecretFor never make a provider's own secret issued outside either.
`secret accept <provider's node> <provider module> <provision>` seals one value to every consumer that
binds the provision from that provider, the provider and the operator, under one generation; the
per-pair form is refused for such a provision, and rotate names the accept. A consumer may wait for a
provision's credential: the controller excuses the wait only when the store bears it out, and its
needs-operator condition names the one accept line.

Adds the replay of issue 164 (TestReplay164), written with only what the controller had before.
This commit is contained in:
jochen
2026-10-13 11:10:17 +02:00
parent 1826b68a22
commit cfe63a4738
18 changed files with 1207 additions and 26 deletions
+145
View File
@@ -0,0 +1,145 @@
package main
import (
"context"
"fmt"
"sort"
"strings"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// A provision whose credential only a person outside the mesh can give (novox/hq ADR 0337, issue 164, issue 165):
// given once for its provider, at the controller's terminal, and sealed in one act to every consumer that binds the
// provision from that provider now. The value is never read back, printed or kept.
// outsideOffer is the provider module's own name when `secret accept <node> <module> <name>` names a provision that
// module offers with a credential issued outside the mesh, and "" when it names anything else (an own secret, or a
// pair with --provider).
func outsideOffer(ctx context.Context, module, name, provider string) (string, error) {
if provider != "" {
return "", nil
}
open, err := openStores(ctx)
if err != nil {
return "", err
}
defer open.Close()
shelf, err := open.inventory.Catalogue(ctx)
if err != nil {
return "", err
}
if m, known := shelf[module]; known && m.IssuedOutsideOf(name) {
return module, nil
}
return "", nil
}
// offeredOutsideBy is a module that offers the provision with a credential issued outside the mesh, or "".
func offeredOutsideBy(ctx context.Context, provision string) (string, error) {
open, err := openStores(ctx)
if err != nil {
return "", err
}
defer open.Close()
shelf, err := open.inventory.Catalogue(ctx)
if err != nil {
return "", err
}
module, _ := catalogue.OffersIssuedOutside(shelf, provision)
return module, nil
}
// consumersFrom is every consumer that binds a provision from one provider now, found by resolving every machine as
// its next send would, and the machines that could not be resolved, with why.
func consumersFrom(ctx context.Context, open *stores, provider, providerModule, provision string) (
[]inventory.PairEnd, []string, error) {
nodes, err := open.inventory.Nodes(ctx)
if err != nil {
return nil, nil, err
}
var out []inventory.PairEnd
var unresolved []string
for _, n := range nodes {
plan, _, err := planFor(ctx, open, n.Name)
if err != nil {
unresolved = append(unresolved, fmt.Sprintf("%s: %s", n.Name, oneLine(err.Error())))
continue
}
out = append(out, bindsFrom(plan, n.Name, provider, providerModule, provision)...)
}
return out, unresolved, nil
}
// bindsFrom is every consumer on one machine whose resolution answers the provision from that provider. Pure.
func bindsFrom(plan catalogue.Resolution, machine, provider, providerModule, provision string) []inventory.PairEnd {
var out []inventory.PairEnd
for _, need := range plan.Needs {
if need.ByRecord || need.Name != provision || need.From != provider {
continue
}
if need.Module != "" && need.Module != providerModule {
continue
}
out = append(out, inventory.PairEnd{Consumer: machine, ConsumerModule: need.For, Local: need.Local})
}
sort.Slice(out, func(i, j int) bool {
return out[i].ConsumerModule+"\x00"+out[i].Local < out[j].ConsumerModule+"\x00"+out[j].Local
})
return out
}
// acceptForProvision gives a provision's credential issued outside the mesh, once for its provider (ADR 0337 §3).
func acceptForProvision(ctx context.Context, provider, providerModule, provision, value string) error {
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
consumers, unresolved, err := consumersFrom(ctx, open, provider, providerModule, provision)
if err != nil {
return err
}
accepted, err := open.inventory.AcceptSecretForProvision(ctx, provision, provider, providerModule, value, consumers)
value = ""
if err != nil {
return err
}
fmt.Print(acceptedSaid(provider, providerModule, provision, accepted, unresolved))
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 "))
}
fmt.Printf(" sent %s; each consumer starts again on it\n", strings.Join(accepted.Machines, ", "))
return nil
}
// acceptedSaid is what one accept for a provider did, in words that never carry the value. Pure.
func acceptedSaid(provider, providerModule, provision string, a inventory.ProvisionAccepted, unresolved []string) string {
var b strings.Builder
if len(a.Sealed) == 0 {
fmt.Fprintf(&b, "%s from %s on %s: no consumer binds it now, so nothing was sealed; a consumer that binds "+
"later waits for it, and this line given again seals it to every consumer then\n", provision,
providerModule, provider)
} else {
var names []string
for _, c := range a.Sealed {
names = append(names, fmt.Sprintf("%s on %s%s", c.ConsumerModule, c.Consumer, asLocal(c.Local)))
}
fmt.Fprintf(&b, "%s from %s on %s is given, sealed to %s and to %s\n", provision, providerModule, provider,
strings.Join(names, ", "), provider)
fmt.Fprintf(&b, " the mesh cannot read it back, never makes one of its own for it, and will not rotate it\n")
}
for _, c := range a.Unsealable {
fmt.Fprintf(&b, " not sealed to %s on %s: no sealing key on that machine; give it again once it has one\n",
c.ConsumerModule, c.Consumer)
}
for _, u := range unresolved {
fmt.Fprintf(&b, " not known whether a consumer on %s binds it, which did not resolve; give it again once it does\n", u)
}
return b.String()
}
+1 -1
View File
@@ -75,7 +75,7 @@ func stateHealth(ctx context.Context, inv *inventory.Inventory, k *conditions.Ke
State: r.State, Reason: r.Reason, Since: r.Since, Streak: r.Streak, Restarts: r.Restarts,
Check: r.Check, Needs: r.Needs, Account: r.Account, Root: r.Root}
for _, w := range r.Waits {
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Secret: w.Secret, Setting: w.Setting, What: w.What})
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Secret: w.Secret, Setting: w.Setting, Provision: w.Provision, What: w.What})
}
resources = append(resources, kept)
}
+139 -14
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"time"
@@ -44,6 +45,28 @@ func needsOperatorKey(module, node string) string {
type operatorWaitFacts struct {
manifests map[string]catalogue.Manifest
given map[string]map[string]time.Time
// outside is, per provision some module offers with a credential issued outside the mesh, that provider module
// and the machines it is assigned to (novox/hq ADR 0337); pairsGiven, per "<module>@<machine>", the provisions
// it holds a value a person gave for, sealed to the keys both ends have now. Absent is not known: no wait for a
// provision of it is excused.
outside map[string]outsideProvider
pairsGiven map[string]map[string]bool
}
// outsideProvider is the module offering a provision whose credential only a person gives, and where it runs.
type outsideProvider struct {
Module string
Machines []string
}
// acceptLine is the one line that gives an outside provision's credential for every consumer of its provider: on
// the provider's machine when it runs on one, named as <machine> when on several or none.
func (p outsideProvider) acceptLine(provision string) string {
machine := "<its machine>"
if len(p.Machines) == 1 {
machine = p.Machines[0]
}
return inventory.AcceptLine(machine, p.Module, provision)
}
// checkWait is nil when a wait is excused, and otherwise why not, in words. Pure.
@@ -52,9 +75,18 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er
if !known {
return fmt.Errorf("says it waits for %s, and the mesh holds no manifest of %s to check it against", waitNames(w), module)
}
named := 0
for _, n := range []string{w.Secret, w.Setting, w.Provision} {
if n != "" {
named++
}
}
switch {
case w.Secret != "" && w.Setting != "", w.Secret == "" && w.Setting == "":
return fmt.Errorf("says it waits, naming %s, where a wait names one secret or one setting", waitNames(w))
case named != 1:
return fmt.Errorf("says it waits, naming %s, where a wait names one secret, one setting or one provision",
waitNames(w))
case w.Provision != "":
return checkProvisionWait(module, machine, w, m, f)
case w.Setting != "":
if _, declared := m.Settings[w.Setting]; !declared {
return fmt.Errorf("says it waits for the setting %s, which %s does not declare", w.Setting, module)
@@ -80,20 +112,46 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er
return nil
}
// checkProvisionWait excuses a wait for a provision's credential only when the store bears it out (novox/hq ADR 0337
// §6): the module requires the provision, a provider offers it with a credential issued outside the mesh, and the
// module on this machine holds no value a person gave for it under the keys both ends have now.
func checkProvisionWait(module, machine string, w inventory.Wait, m catalogue.Manifest, f operatorWaitFacts) error {
if !slices.Contains(m.Requires, w.Provision) {
return fmt.Errorf("says it waits for the provision %s, which %s does not require", w.Provision, module)
}
if _, outside := f.outside[w.Provision]; !outside {
return fmt.Errorf("says it waits for the provision %s, whose credential the mesh gives itself: only a "+
"credential issued outside the mesh waits for the operator", w.Provision)
}
given, readable := f.pairsGiven[module+"@"+machine]
if !readable {
return fmt.Errorf("says it waits for the provision %s, and what was given on %s could not be read", w.Provision, machine)
}
if given[w.Provision] {
return fmt.Errorf("says it waits for the provision %s, whose credential was given", w.Provision)
}
return nil
}
// waitRefusedPrefix opens every reason checkWait gives, so the words of a refused wait are told from a check's own.
const waitRefusedPrefix = "says it waits"
// waitNames is what a wait names, as "the secret x" or "the setting y".
func waitNames(w inventory.Wait) string {
switch {
case w.Secret != "" && w.Setting != "":
return "the secret " + w.Secret + " and the setting " + w.Setting
case w.Secret != "":
return "the secret " + w.Secret
case w.Setting != "":
return "the setting " + w.Setting
var names []string
if w.Secret != "" {
names = append(names, "the secret "+w.Secret)
}
return "nothing"
if w.Setting != "" {
names = append(names, "the setting "+w.Setting)
}
if w.Provision != "" {
names = append(names, "the provision "+w.Provision)
}
if len(names) == 0 {
return "nothing"
}
return strings.Join(names, " and ")
}
// checkWaiting reads one machine's resources against the facts: every waiting resource whose waits all check out is
@@ -107,11 +165,17 @@ func checkWaiting(machine string, rs []inventory.ResourceHealth, f operatorWaitF
if len(r.Waits) == 0 {
why = fmt.Errorf("says it waits, and names nothing it waits for")
}
waits := make([]inventory.Wait, 0, len(r.Waits))
for _, w := range r.Waits {
if why == nil {
why = checkWait(r.Module, machine, w, f)
}
if p, outside := f.outside[w.Provision]; outside && w.Provision != "" {
w.Act = p.acceptLine(w.Provision)
}
waits = append(waits, w)
}
r.Waits = waits
if why != nil {
r.State, r.Reason = link.StateUnhealthy, why.Error()
}
@@ -150,6 +214,10 @@ func operatorWaitSaid(module, machine string, waits []inventory.Wait) string {
if w.Secret != "" {
part += fmt.Sprintf(", given with `nox secret ask %s %s %s`", machine, module, w.Secret)
}
if w.Provision != "" && w.Act != "" {
// One line for the provider, which seals it to every consumer of it (ADR 0337 §3, §7).
part += fmt.Sprintf(", given for every consumer with `%s`", w.Act)
}
parts = append(parts, part+")")
}
return fmt.Sprintf("%s on %s waits for the operator: %s", module, machine, strings.Join(parts, "; "))
@@ -172,7 +240,7 @@ func needsOperatorObservation(module, node string, waits []inventory.Wait, rs []
func needsOperatorWords(module, node string, waits []inventory.Wait) words {
var acts []string
seen := map[string]bool{}
secret := false
secret, provision := false, false
for _, w := range waits {
var act string
switch {
@@ -180,6 +248,9 @@ func needsOperatorWords(module, node string, waits []inventory.Wait) words {
act, secret = fmt.Sprintf("type %s at %s's desk prompt", w.What, node), true
case w.Setting != "":
act = fmt.Sprintf("approve %s of %s on %s when it is proposed to you", w.Setting, module, node)
case w.Provision != "":
// ADR 0337 §7: "give <what> of <provider> on <machine>"; the line is in the summary.
act, provision = "give "+w.What, true
}
if act != "" && !seen[act] {
seen[act] = true
@@ -197,6 +268,10 @@ func needsOperatorWords(module, node string, waits []inventory.Wait) words {
explanation += " A hidden prompt opens at the desk when the secret is asked for, and what you type there " +
"is sealed to the machine."
}
if provision {
explanation += " A key only you can give is given once for the app that issued it, and reaches every " +
"module that uses it."
}
explanation += " Its update is in place and nothing was undone; it carries on by itself once it is given."
return words{
Headline: fmt.Sprintf("%s waits for you on %s", module, node),
@@ -217,6 +292,12 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
if f.given == nil {
f.given = map[string]map[string]time.Time{}
}
if f.outside == nil {
f.outside = map[string]outsideProvider{}
}
if f.pairsGiven == nil {
f.pairsGiven = map[string]map[string]bool{}
}
if inv == nil {
return
}
@@ -238,15 +319,59 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
}
}
}
if _, read := f.given[module+"@"+machine]; read {
if _, read := f.given[module+"@"+machine]; !read {
if given, err := inv.GivenOwnSecrets(ctx, machine, module); err == nil {
f.given[module+"@"+machine] = given
}
}
if _, read := f.pairsGiven[module+"@"+machine]; !read {
if given, err := inv.GivenPairsOf(ctx, machine, module); err == nil {
f.pairsGiven[module+"@"+machine] = given
}
}
// The provisions it requires whose credential only a person gives, and where their provider runs (ADR 0337).
m, known := f.manifests[module]
if !known {
continue
}
if given, err := inv.GivenOwnSecrets(ctx, machine, module); err == nil {
f.given[module+"@"+machine] = given
for _, provision := range m.Requires {
if _, read := f.outside[provision]; read {
continue
}
if shelf == nil {
var err error
if shelf, err = inv.Catalogue(ctx); err != nil {
shelf = map[string]catalogue.Manifest{}
}
}
provider, outside := catalogue.OffersIssuedOutside(shelf, provision)
if !outside {
continue
}
f.outside[provision] = outsideProvider{Module: provider, Machines: machinesRunning(ctx, inv, provider)}
}
}
}
// machinesRunning is every machine a module is assigned to, by name; a read that fails is left out.
func machinesRunning(ctx context.Context, inv *inventory.Inventory, module string) []string {
nodes, err := inv.Nodes(ctx)
if err != nil {
// Only the summary's accept line reads it, which then names the provider's machine "<its machine>".
// empty-on-error: no wait is excused or refused by it
return nil
}
var out []string
for _, n := range nodes {
assigned, err := inv.Assigned(ctx, n.Name)
if err == nil && slices.Contains(assigned, module) {
out = append(out, n.Name)
}
}
sort.Strings(out)
return out
}
// waitingModules is every module with a waiting resource in a statement.
func waitingModules(rs []inventory.ResourceHealth) []string {
seen := map[string]bool{}
+2 -2
View File
@@ -55,8 +55,8 @@ func TestAWaitIsExcusedOnlyWhenItChecksOut(t *testing.T) {
{"a secret the mesh makes", inventory.Wait{Part: "p", Secret: "made", What: "w"}, factsGiven(nil), "mesh makes itself"},
{"the bus account", inventory.Wait{Part: "p", Secret: "broker", What: "w"}, factsGiven(nil), "mesh makes itself"},
{"a setting not declared", inventory.Wait{Part: "p", Setting: "logins", What: "w"}, factsGiven(nil), "does not declare"},
{"both", inventory.Wait{Part: "p", Secret: "licence", Setting: "smb-users", What: "w"}, factsGiven(nil), "one secret or one setting"},
{"neither", inventory.Wait{Part: "p", What: "w"}, factsGiven(nil), "one secret or one setting"},
{"both", inventory.Wait{Part: "p", Secret: "licence", Setting: "smb-users", What: "w"}, factsGiven(nil), "one secret, one setting or one provision"},
{"neither", inventory.Wait{Part: "p", What: "w"}, factsGiven(nil), "one secret, one setting or one provision"},
{"no manifest known", passwordWait, operatorWaitFacts{}, "no manifest"},
{"what was given cannot be read", passwordWait,
operatorWaitFacts{manifests: map[string]catalogue.Manifest{"mounts": mountsManifest}}, "could not be read"},
@@ -0,0 +1,83 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// One accept for a provider reaches every consumer that binds the provision from it, and each consumer's send
// places it; the per-pair form naming a consumer is refused for such a provision, naming the provider's form; a
// value the mesh made before the rule is not placed (novox/hq ADR 0337 §2, §3, §4, issue 164, issue 165). Needs the
// store (MESH_TEST_POSTGRES), and is named in testdata/store-tests.
func TestOneAcceptForAProviderIsPlacedForEveryConsumer(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
// Before the rule: tv's offer said nothing of its credential, and the send made one per pair.
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh}}})
for _, m := range []string{"requests", "indexer"} {
register(t, open, catalogue.Manifest{Module: m, Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": "/var/lib/mesh/" + m + "/tv-api.secret"}})
}
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}, {"laptop", "indexer"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
placed := func(module string) bool {
t.Helper()
plan, settings, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatalf("laptop was refused: %v", err)
}
declared, err := declarationFor(ctx, open, "laptop", plan, settings)
if err != nil {
t.Fatalf("laptop did not compose: %v", err)
}
for _, r := range declared.Resources {
if r["path"] == "/var/lib/mesh/"+module+"/tv-api.secret" && r["sealed"] != nil {
return true
}
}
return false
}
if !placed("requests") || !placed("indexer") {
t.Fatal("before the rule, the send made and placed a credential per pair")
}
// The offer now says only a person gives it: the values the mesh made are not placed.
register(t, open, catalogue.Manifest{Module: "tv", Version: "2",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}})
if placed("requests") || placed("indexer") {
t.Fatal("a credential nobody gave was placed")
}
// The per-pair form is refused, naming the provider's form, and reads nothing.
file := filepath.Join(t.TempDir(), "key")
if err := os.WriteFile(file, []byte("the app's key\n"), 0o600); err != nil {
t.Fatal(err)
}
err := secretCommand(ctx, []string{"accept", "laptop", "requests", "tv-api", "--provider", "anchor", "--from", file})
if err == nil || !strings.Contains(err.Error(), "secret accept anchor tv tv-api") {
t.Fatalf("the per-pair form was not refused naming the provider's: %v", err)
}
consumers, unresolved, err := consumersFrom(ctx, open, "anchor", "tv", "tv-api")
if err != nil || len(unresolved) != 0 {
t.Fatalf("the consumers of tv on anchor could not be found: %v %v", err, unresolved)
}
if len(consumers) != 2 {
t.Fatalf("the consumers of tv on anchor are %+v; want requests and indexer on laptop", consumers)
}
if _, err := open.inventory.AcceptSecretForProvision(ctx, "tv-api", "anchor", "tv", "the app's key", consumers); err != nil {
t.Fatal(err)
}
if !placed("requests") || !placed("indexer") {
t.Fatal("the value given once for the provider was not placed for every consumer")
}
}
+113
View File
@@ -0,0 +1,113 @@
package main
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// A consumer may wait for a provision's credential only a person gives (novox/hq ADR 0337 §5–§7, issue 164).
var requestsManifest = catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api", "db"},
Secrets: map[string]string{"tv-api": "/s/tv-api", "db": "/s/db"}}
var tvKeyWait = inventory.Wait{Part: "the tv library", Provision: "tv-api", What: "the API key of tv"}
func outsideFacts(given map[string]bool) operatorWaitFacts {
return operatorWaitFacts{manifests: map[string]catalogue.Manifest{"requests": requestsManifest},
outside: map[string]outsideProvider{"tv-api": {Module: "tv", Machines: []string{"media"}}},
pairsGiven: map[string]map[string]bool{"requests@laptop": given}}
}
// §6: excused only when the module requires the provision, its provider says it is issued outside, and no value a
// person gave is held for the pair under the keys both ends have now.
func TestAWaitForAProvisionIsExcusedOnlyWhenTheStoreBearsItOut(t *testing.T) {
for _, c := range []struct {
name string
w inventory.Wait
f operatorWaitFacts
says string
}{
{"required, issued outside, not given", tvKeyWait, outsideFacts(nil), ""},
{"given", tvKeyWait, outsideFacts(map[string]bool{"tv-api": true}), "was given"},
{"not required", inventory.Wait{Part: "p", Provision: "music-api", What: "w"}, outsideFacts(nil), "does not require"},
{"a credential the mesh gives itself", inventory.Wait{Part: "p", Provision: "db", What: "w"}, outsideFacts(nil),
"mesh gives itself"},
{"what was given cannot be read", tvKeyWait, operatorWaitFacts{
manifests: map[string]catalogue.Manifest{"requests": requestsManifest},
outside: map[string]outsideProvider{"tv-api": {Module: "tv"}}}, "could not be read"},
{"a provision and a secret", inventory.Wait{Part: "p", Provision: "tv-api", Secret: "x", What: "w"},
outsideFacts(nil), "one secret, one setting or one provision"},
} {
err := checkWait("requests", "laptop", c.w, c.f)
switch {
case c.says == "" && err != nil:
t.Errorf("%s: refused: %v", c.name, err)
case c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)):
t.Errorf("%s: %v; want a refusal saying %q", c.name, err, c.says)
}
}
}
// §7: an excused wait is the consumer's needs-operator condition, in plain words, its summary naming the one line
// that gives the credential for every consumer of the provider; the wait as kept is not changed.
func TestAWaitForAProvisionNamesTheOneAcceptForItsProvider(t *testing.T) {
r := inventory.ResourceHealth{Module: "requests", Resource: "requests.app", Kind: "container", Target: "requests",
State: link.StateWaiting, Check: "tool", Waits: []inventory.Wait{tvKeyWait}}
kept := []inventory.ResourceHealth{r}
got := checkWaiting("laptop", kept, outsideFacts(nil))
if got[0].State != link.StateWaiting {
t.Fatalf("an excused wait for a provision was judged: %+v", got[0])
}
if kept[0].Waits[0].Act != "" {
t.Fatal("checking the wait changed the statement as kept")
}
waits, waiting := operatorWait("requests", got)
if !waiting {
t.Fatal("the module does not read as waiting for the operator")
}
o := needsOperatorObservation("requests", "laptop", waits, got)
if !strings.Contains(o.Summary, "secret accept media tv tv-api") {
t.Fatalf("the summary does not name the accept for the provider: %q", o.Summary)
}
if !strings.HasPrefix(o.Needs, "give the API key of tv") {
t.Fatalf("the needs do not name the act: %q", o.Needs)
}
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions}, "laptop"); !ok {
t.Fatalf("the condition is not in plain words: %s", why)
}
// A provider on several machines, or none: the line names its machine as a place to fill.
if line := (outsideProvider{Module: "tv", Machines: []string{"a", "b"}}).acceptLine("tv-api"); line !=
"secret accept <its machine> tv tv-api" {
t.Fatalf("a provider on two machines reads %q", line)
}
}
// §3: the consumers one accept reaches are those whose resolution answers the provision from that provider.
func TestTheConsumersOfAProviderAreThoseItAnswers(t *testing.T) {
plan := catalogue.Resolution{Needs: []catalogue.Needed{
{Name: "tv-api", From: "media", Module: "tv", For: "requests"},
{Name: "tv-api", From: "media", Module: "tv", For: "indexer", Local: "main"},
{Name: "tv-api", From: "other", Module: "tv", For: "elsewhere"},
{Name: "tv-api", From: "media", Module: "another-tv", For: "not-this-module"},
{Name: "db", From: "media", Module: "tv", For: "requests"},
{Name: "tv-api", ByRecord: true, For: "by-record"},
}}
got := bindsFrom(plan, "laptop", "media", "tv", "tv-api")
if len(got) != 2 || got[0].ConsumerModule != "indexer" || got[0].Local != "main" || got[1].ConsumerModule != "requests" {
t.Fatalf("the consumers from tv on media are %+v", got)
}
said := acceptedSaid("media", "tv", "tv-api", inventory.ProvisionAccepted{
Sealed: []inventory.PairEnd{{Consumer: "laptop", ConsumerModule: "requests"}},
Unsealable: []inventory.PairEnd{{Consumer: "new", ConsumerModule: "indexer"}}}, []string{"down: refused"})
for _, want := range []string{"sealed to requests on laptop", "no sealing key", "down: refused"} {
if !strings.Contains(said, want) {
t.Errorf("what the accept says lacks %q:\n%s", want, said)
}
}
}
+21 -1
View File
@@ -182,12 +182,21 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
}
var secret inventory.Secret
var err error
if n.SharedOwn != "" {
if outsideModule, outside := issuedOutsideBy(shelf, n); outside {
// Only a person can give it (novox/hq ADR 0337, issue 164): read as given, never made.
secret, err = inv.GivenPairSecret(ctx, n.Name, nodeName, n.For, n.From, outsideModule, n.Local)
} else if n.SharedOwn != "" {
// The provider's one credential, sealed to this consumer too (novox/hq ADR 0158).
secret, err = inv.SharedSecretFor(ctx, n.Name, nodeName, n.For, n.From, providerModuleOf(resolved, open, ctx, n), n.Local, n.SharedOwn)
} else {
secret, err = inv.SecretFor(ctx, n.Name, nodeName, n.For, n.From, n.Local)
}
if inventory.IsNotGiven(err) {
// **Not given, and the rest of the consumer runs** (ADR 0337 §4): no credential file, so the module can
// tell it was not given and says it waits for it; the machine is not refused for what only a person can
// give, and nothing the mesh made stands in for it.
continue
}
if err != nil {
// Said rather than skipped. A machine that resolves cleanly and receives no
// credential is one that will fail to authenticate at some later, less obvious
@@ -1742,6 +1751,17 @@ func busUserList(ctx context.Context, inv *inventory.Inventory,
// providerModuleOf is which module answers a need on the providing node: the one in this node's
// own set when the provider is here, else the one the catalogue says offers it.
// issuedOutsideBy is whether a need's provision is offered by its provider with a credential issued outside the
// mesh (novox/hq ADR 0337), and that provider module: the one answering it when known, else any that offers it so.
func issuedOutsideBy(shelf map[string]catalogue.Manifest, n catalogue.Needed) (string, bool) {
if n.Module != "" {
if m, known := shelf[n.Module]; known {
return n.Module, m.IssuedOutsideOf(n.Name)
}
}
return catalogue.OffersIssuedOutside(shelf, n.Name)
}
func providerModuleOf(resolved catalogue.Resolution, open *stores, ctx context.Context, n catalogue.Needed) string {
for _, m := range resolved.Modules {
if _, shared := m.SharedCredentialOf(n.Name); shared {
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// TestReplay164 replays novox/hq issue 164 (found provisioning the media machine's modules, re-checked
// 2026-10-08): a Servarr app's API key, an indexer's key and a downloader's password are values the provider's
// software holds and only a person can give, and for a consumer of such a provision the send made a random pair
// credential, sealed it to both ends and placed it; the provider refused it, and a consumer retrying it could be
// banned by the provider. The outcome asserted (ADR 0337): for a provision whose offer says its credential is
// issued outside the mesh, the send makes no value and places none, the consumer's machine still composes, and a
// value a person gave for the pair is placed. Written with only what the controller had before its fix —
// register, assign, planFor, declarationFor, AcceptSecretForPair and the inventory's secret reads — so it is laid
// over the commit before. Needs the store (MESH_TEST_POSTGRES), and is named in testdata/store-tests.
func TestReplay164(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
const secretFile = "/var/lib/mesh/requests/tv-api.secret"
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}},
Serves: map[string]map[string]any{"tv-api": {"port": 8989}}})
register(t, open, catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": secretFile}})
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
sent := func() []map[string]any {
t.Helper()
plan, settings, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err)
}
declared, err := declarationFor(ctx, open, "laptop", plan, settings)
if err != nil {
t.Fatalf("the send to laptop did not compose: %v", err)
}
return declared.Resources
}
placed := func(resources []map[string]any) map[string]any {
for _, r := range resources {
if r["path"] == secretFile && r["sealed"] != nil {
return r
}
}
return nil
}
if got := placed(sent()); got != nil {
t.Errorf("the send placed a credential for tv-api (%v), which only a person can give and nobody gave", got["id"])
}
held, err := open.inventory.SecretsOf(ctx, "laptop", "requests")
if err != nil {
t.Fatal(err)
}
for _, s := range held {
if s.Name == "tv-api" {
t.Fatalf("the mesh holds a value for tv-api that nobody gave (origin %q)", s.Origin)
}
}
// Given by a person for the pair, it is placed, as given.
if err := open.inventory.AcceptSecretForPair(ctx, "tv-api", "laptop", "requests", "anchor", "", "the app's key"); err != nil {
t.Fatal(err)
}
if placed(sent()) == nil {
t.Fatal("the value a person gave for tv-api was not placed")
}
}
+32
View File
@@ -79,6 +79,37 @@ func secretCommand(ctx context.Context, args []string) error {
return fmt.Errorf("a secret's value is given at the controller's terminal or at the desk (`give`), never "+
"through a verb (this line came through %q): nothing was read or sealed", verb)
}
// **A provision's credential issued outside the mesh is given once, for its provider** (novox/hq ADR 0337 §3,
// issue 165): `secret accept <provider's machine> <provider module> <provision>`, sealed to every consumer of
// that provider; the per-pair form naming a consumer is refused for it, naming this one.
outsideAt, err := outsideOffer(ctx, module, name, *provider)
if err != nil {
return err
}
if outsideAt == "" && *provider != "" {
if offered, err := offeredOutsideBy(ctx, name); err != nil {
return err
} else if offered != "" {
return fmt.Errorf("%s is issued outside the mesh and given once for its provider, which seals it to "+
"every consumer of it (ADR 0337): `secret accept %s %s %s`; nothing was read or sealed",
name, *provider, offered, name)
}
}
if outsideAt != "" {
if *desk != "" || *local != "" {
return errors.New("a provision's credential issued outside the mesh is given at the controller's " +
"terminal, for every consumer of its provider; it takes neither --at-desk nor --local")
}
value, err := valueFor(node, module, name, *from)
if err != nil {
return err
}
value = asSupplied(value)
if value == "" {
return errors.New("there is nothing to seal")
}
return acceptForProvision(ctx, node, module, name, value)
}
if *desk != "" {
if *from != "" || *provider != "" {
return errors.New("--at-desk gives a module's own secret, and takes neither --from nor --provider")
@@ -173,6 +204,7 @@ const secretUsage = "secret rotate <node> <module> <name> [--why <text> [--cause
"secret ask <node> <module> <name> [--at <machine>]\n" +
"secret issued <provision>\n" +
"secret accept <node> <module> <name> [--from <file> | --at-desk <machine>] [--provider <node> [--local <name>]]\n" +
"secret accept <provider's node> <provider module> <provision> [--from <file>] (a credential issued outside the mesh, for every consumer)\n" +
"secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>] [--provider <node>]\n" +
"secret export [--out <file>]"
+2 -2
View File
@@ -25,13 +25,13 @@ func TestAnOfferMaySayItsSoftwareIssuesTheCredential(t *testing.T) {
}
for want, raw := range map[string]string{
"the one issuer an offer may name": `{"module":"d","version":"1",
"the issuers an offer may name": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"vendor","tool":"d_token"}}]}`,
"names no tool that answers it": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"software"}}]}`,
"names its own secret": `{"module":"d","version":"1","own-secrets":{"token":{"path":"/p","taken":"at-start"}},
"provides":[{"name":"d-api","credential":{"issued-by":"software","tool":"d_token","own":"token"}}]}`,
"the one issuer an offer may name ": `{"module":"d","version":"1",
"the issuers an offer may name ": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"tool":"d_token"}}]}`,
} {
want = strings.TrimSpace(want)
+46
View File
@@ -0,0 +1,46 @@
package catalogue
import (
"strings"
"testing"
)
// A provision's credential may be one only a person outside the mesh can give (novox/hq ADR 0337 §1, issue 164):
// the offer says `issued-by: outside` and names neither an own secret nor a tool, and the parser refuses it beside
// either. An offer that says `software` keeps ADR 0305's shape.
func TestAnOfferMaySayAPersonOutsideTheMeshIssuesItsCredential(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"tv","version":"1",
"provides":[{"name":"tv-api","scope":"mesh","credential":{"issued-by":"outside"}}],
"serves":{"tv-api":{"port":8989}}}`))
if err != nil {
t.Fatal(err)
}
if !m.IssuedOutsideOf("tv-api") {
t.Fatal("the offer's credential issued outside was not read")
}
if m.IssuedOutsideOf("other") {
t.Fatal("a provision the module does not offer reads as issued outside")
}
if _, issued := m.IssuedCredentialOf("tv-api"); issued {
t.Fatal("a credential issued outside was read as issued by the software")
}
if _, shared := m.SharedCredentialOf("tv-api"); shared {
t.Fatal("a credential issued outside was read as an own secret shared")
}
if who, offered := OffersIssuedOutside(map[string]Manifest{"tv": m}, "tv-api"); !offered || who != "tv" {
t.Fatalf("the catalogue does not say tv offers tv-api issued outside: %q %v", who, offered)
}
for want, raw := range map[string]string{
"names its own secret": `{"module":"d","version":"1","own-secrets":{"key":{"path":"/p","taken":"at-start"}},
"provides":[{"name":"d-api","credential":{"issued-by":"outside","own":"key"}}]}`,
"names the tool": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"outside","tool":"d_key"}}]}`,
"the issuers an offer may name": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"a-person"}}]}`,
} {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("expected a refusal saying %q, got %v", want, err)
}
}
}
+47 -4
View File
@@ -269,7 +269,8 @@ func KeepsConsumerData(catalogue map[string]Manifest, provision string) bool {
func (o Offer) MachineReach() bool { return o.Reach == ReachMachine }
// OfferCredential names which of the provider's own secrets a provision's consumers receive — or, with
// IssuedBy, that the provider's software issues the provision's one credential itself (novox/hq ADR 0305,
// IssuedBy "outside", that only a person can give the provision's credential (novox/hq ADR 0337, issue 164) — or,
// with IssuedBy "software", that the provider's software issues the provision's one credential itself (novox/hq ADR 0305,
// issue 526): a media server's sign-in token from its vendor, which the mesh can neither make nor rotate.
// Tool is then the provider's own tool that answers that value, sealed to a key made for the controller's
// one call, and to the controller alone.
@@ -293,6 +294,34 @@ func (m Manifest) IssuedCredentialOf(provision string) (string, bool) {
return "", false
}
// IssuedOutsideOf is whether this module's offer of a provision says only a person outside the mesh can give its
// credential (novox/hq ADR 0337): the mesh never makes a value for it, and a consumer holds one only once a person
// gave it with `secret accept`.
func (m Manifest) IssuedOutsideOf(provision string) bool {
for _, o := range m.Provides {
if o.Name == provision && o.Credential != nil && o.Credential.IssuedBy == IssuedOutside {
return true
}
}
return false
}
// OffersIssuedOutside is whether any module of a catalogue offers the provision with a credential issued outside
// the mesh (novox/hq ADR 0337), and the first such module by name.
func OffersIssuedOutside(shelf map[string]Manifest, provision string) (string, bool) {
var names []string
for name, m := range shelf {
if m.IssuedOutsideOf(provision) {
names = append(names, name)
}
}
if len(names) == 0 {
return "", false
}
sort.Strings(names)
return names[0], true
}
// SharedCredentialOf is the own secret an offer of this module names as the provision's credential,
// and whether it names one.
func (m Manifest) SharedCredentialOf(provision string) (string, bool) {
@@ -1647,14 +1676,28 @@ func ParseManifest(raw []byte) (Manifest, error) {
"makes is %d", m.Module, p, id.Max, shortest))
}
}
if c := offer.Credential; c != nil && (c.IssuedBy != "" || c.Tool != "") {
if c := offer.Credential; c != nil && c.IssuedBy == IssuedOutside {
// Only a person can give it (novox/hq ADR 0337 §1, issue 164): the software holds a value the mesh can
// neither make nor fetch, so the offer names nothing that would answer it — no own secret, no tool.
switch {
case c.Own != "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential issued outside the mesh and names its own secret %q too; a "+
"person gives the value with `secret accept`, so the offer names no own secret (ADR 0337)",
m.Module, p, c.Own))
case c.Tool != "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential issued outside the mesh and names the tool %q; only a "+
"person gives it, so the offer names no tool (ADR 0337)", m.Module, p, c.Tool))
}
} else if c := offer.Credential; c != nil && (c.IssuedBy != "" || c.Tool != "") {
// The software issues it (novox/hq ADR 0305): named by the tool that answers it, never by an own
// secret, which would be a second copy for the mesh to keep in step with the software's.
switch {
case c.IssuedBy != IssuedBySoftware:
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential issued by %q; the one issuer an offer may name is %q (ADR 0305)",
m.Module, p, c.IssuedBy, IssuedBySoftware))
"%s provides %q with a credential issued by %q; the issuers an offer may name are %q and %q "+
"(ADR 0305, ADR 0337)", m.Module, p, c.IssuedBy, IssuedBySoftware, IssuedOutside))
case c.Own != "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential its software issues and names its own secret %q too; the "+
+7 -1
View File
@@ -44,7 +44,13 @@ type Wait struct {
Part string `json:"part"`
Secret string `json:"secret,omitempty"`
Setting string `json:"setting,omitempty"`
What string `json:"what"`
// Provision is a provision the module requires whose credential only a person can give and nobody gave yet
// (novox/hq ADR 0337 §5): a wait names exactly one of Secret, Setting or Provision.
Provision string `json:"provision,omitempty"`
What string `json:"what"`
// Act is the line that gives a provision's credential, filled by the controller when it excuses the wait
// (ADR 0337 §7); never said by a machine.
Act string `json:"act,omitempty"`
}
// NodeHealth is a machine's newest statement, as kept.
+252
View File
@@ -0,0 +1,252 @@
package inventory
import (
"context"
"errors"
"fmt"
"sort"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/secrets"
)
// A provision whose credential only a person outside the mesh can give (novox/hq ADR 0337, issue 164): a Servarr
// app's API key, an indexer's key, a downloader's password. The provider's software holds the value; the mesh can
// neither make one that would work nor fetch it. So the mesh never makes one (§2), a person gives it once per
// provider and it is sealed to every consumer of that provider (§3), and until then a consumer holds no credential
// for it and is told so (§4), and the rest of it runs.
// NotGivenError is a pair credential only a person can give, which no person gave for this pair under the keys its
// two ends have now: the consumer receives no credential for it, and waits (ADR 0337 §4, §8). It is not a fault of
// the machine, so the plan does not refuse the machine for it.
type NotGivenError struct {
Provision, Consumer, ConsumerModule, Provider, ProviderModule, Local string
// Own is set when the credential is the provider's own secret issued outside (ADR 0158, ADR 0228), and names it.
Own string
// Why is why no value is held, in words that never carry one.
Why string
}
func (e *NotGivenError) Error() string {
if e.Own != "" {
return fmt.Sprintf("%s on %s holds no %s from %s: %s; give it with `secret accept %s %s %s`",
e.ConsumerModule, e.Consumer, e.Provision, e.Provider, e.Why, e.Provider, e.ProviderModule, e.Own)
}
return fmt.Sprintf("%s on %s holds no %s from %s: %s; it is issued outside the mesh and given with `%s`",
e.ConsumerModule, e.Consumer, e.Provision, e.Provider, e.Why, acceptLine(e.Provider, e.ProviderModule, e.Provision))
}
// IsNotGiven is whether an error is a credential only a person gives, not given yet.
func IsNotGiven(err error) bool {
var ng *NotGivenError
return errors.As(err, &ng)
}
// acceptLine is the one line that gives a provision's credential issued outside, for every consumer of a provider.
func acceptLine(provider, providerModule, provision string) string {
if providerModule == "" {
providerModule = "<provider module>"
}
return fmt.Sprintf("secret accept %s %s %s", provider, providerModule, provision)
}
// AcceptLine is acceptLine, for the controller's words.
func AcceptLine(provider, providerModule, provision string) string {
return acceptLine(provider, providerModule, provision)
}
// issuedOutside is whether a module in the catalogue offers the provision with a credential issued outside the
// mesh, and which.
func (i *Inventory) issuedOutside(ctx context.Context, provision string) (string, bool, error) {
shelf, err := i.Catalogue(ctx)
if err != nil {
return "", false, err
}
module, outside := catalogue.OffersIssuedOutside(shelf, provision)
return module, outside, nil
}
// ownIssuedOutside is whether a provider's own secret says only an outside party issues it (ADR 0228).
func (i *Inventory) ownIssuedOutside(ctx context.Context, providerModule, own string) (string, bool, error) {
if providerModule == "" {
return own, false, nil
}
m, err := i.declared(ctx, providerModule)
if err != nil {
return own, false, err
}
return own, m.OwnSecrets[own].IssuedBy == catalogue.IssuedOutside, nil
}
// GivenPairSecret is the credential of a pair whose provision is issued outside the mesh, as a person gave it, and
// never one the mesh makes (ADR 0337 §2, §4). It writes nothing. A value is answered only when a person gave it
// (`accepted`) and it is sealed to the keys both ends have now; otherwise a NotGivenError says why — never given, or
// given under a key one end no longer has (§8), or a value the mesh made before the rule, which is not given and
// never delivered again (§2).
func (i *Inventory) GivenPairSecret(ctx context.Context, name, consumer, consumerModule, provider, providerModule,
local string) (Secret, error) {
consumerKey, err := i.SealingKeyOf(ctx, consumer)
if err != nil {
return Secret{}, err
}
providerKey, err := i.SealingKeyOf(ctx, provider)
if err != nil {
return Secret{}, err
}
consumerNode, err := i.NodeByName(ctx, consumer)
if err != nil {
return Secret{}, err
}
providerNode, err := i.NodeByName(ctx, provider)
if err != nil {
return Secret{}, err
}
notGiven := func(why string) (Secret, error) {
return Secret{}, &NotGivenError{Provision: name, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider, ProviderModule: providerModule, Local: local, Why: why}
}
var held Secret
err = i.store.Pool().QueryRow(ctx,
`select for_consumer, for_provider, consumer_key, provider_key, origin from secret
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local).
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
switch {
case errors.Is(err, pgx.ErrNoRows):
return notGiven("nobody gave it")
case err != nil:
return Secret{}, err
case held.Origin != OriginAccepted:
return notGiven("the value held was made by the mesh, which " + providerModule + "'s software would refuse")
case consumerKey == "" || providerKey == "" || held.ConsumerKey != consumerKey || held.ProviderKey != providerKey:
return notGiven("it was given under a sealing key one end no longer has")
}
held.Name, held.Consumer, held.Provider = name, consumer, provider
held.ConsumerModule, held.Local = consumerModule, local
return held, nil
}
// PairEnd is one consumer of a provision from one provider: the machine, the module there and its local name.
type PairEnd struct {
Consumer, ConsumerModule, Local string
}
// ProvisionAccepted is what one accept for a provider sealed (ADR 0337 §3): the consumers it reached, those it could
// not (no sealing key on their machine), and the machines to send.
type ProvisionAccepted struct {
Sealed, Unsealable []PairEnd
Machines []string
}
// AcceptSecretForProvision seals one value a person gave for a provision issued outside the mesh to the provider's
// machine, to every consumer named, and to the operator, in one act under one generation, each row `accepted`
// (ADR 0337 §3, issue 165); the plaintext is not kept. The consumers are every one that binds the provision from
// this provider now, which the caller finds by resolving every machine. A consumer whose machine has no sealing key
// is not sealed to, and said.
func (i *Inventory) AcceptSecretForProvision(ctx context.Context, provision, provider, providerModule, value string,
consumers []PairEnd) (ProvisionAccepted, error) {
m, err := i.declared(ctx, providerModule)
if err != nil {
return ProvisionAccepted{}, err
}
if !m.IssuedOutsideOf(provision) {
return ProvisionAccepted{}, fmt.Errorf("%s does not offer %q with a credential issued outside the mesh "+
"(\"credential\": {\"issued-by\": \"outside\"}, ADR 0337), so its consumers' credentials are not given "+
"once for the provider", providerModule, provision)
}
if value == "" {
return ProvisionAccepted{}, errors.New("there is nothing to seal")
}
providerNode, err := i.NodeByName(ctx, provider)
if err != nil {
return ProvisionAccepted{}, err
}
providerKey, err := i.SealingKeyOf(ctx, provider)
if err != nil {
return ProvisionAccepted{}, err
}
if providerKey == "" {
return ProvisionAccepted{}, fmt.Errorf("%s has no sealing key, so nothing can be sealed to it; a node joins to get one", provider)
}
forOperator, operatorKey, err := i.operatorSeal(ctx, value)
if err != nil {
return ProvisionAccepted{}, err
}
generation := secrets.Stamp()
var out ProvisionAccepted
machines := map[string]bool{provider: true}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return ProvisionAccepted{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
for _, c := range consumers {
consumerNode, err := i.NodeByName(ctx, c.Consumer)
if err != nil {
return ProvisionAccepted{}, err
}
consumerKey, err := i.SealingKeyOf(ctx, c.Consumer)
if err != nil {
return ProvisionAccepted{}, err
}
if consumerKey == "" {
out.Unsealable = append(out.Unsealable, c)
continue
}
sealed, err := secrets.Accept(value, consumerKey, providerKey)
if err != nil {
return ProvisionAccepted{}, err
}
if _, err := tx.Exec(ctx,
`insert into secret (name, consumer, consumer_module, provider, for_consumer, for_provider,
consumer_key, provider_key, operator_sealed, operator_key, origin, local, generation)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
on conflict (name, local, consumer, consumer_module, provider) do update set
for_consumer = excluded.for_consumer, for_provider = excluded.for_provider,
consumer_key = excluded.consumer_key, provider_key = excluded.provider_key,
created_at = now(), origin = excluded.origin, generation = excluded.generation,
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
provision, consumerNode.ID, c.ConsumerModule, providerNode.ID,
sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey,
forOperator, operatorKey, OriginAccepted, c.Local, generation); err != nil {
return ProvisionAccepted{}, err
}
out.Sealed = append(out.Sealed, c)
machines[c.Consumer] = true
}
if err := tx.Commit(ctx); err != nil {
return ProvisionAccepted{}, err
}
for n := range machines {
out.Machines = append(out.Machines, n)
}
sort.Strings(out.Machines)
return out, nil
}
// GivenPairsOf is every provision a module on a machine holds a value a person gave for, sealed to the keys both
// ends have now: what a wait for a provision's credential is checked against (ADR 0337 §6).
func (i *Inventory) GivenPairsOf(ctx context.Context, machine, module string) (map[string]bool, error) {
rows, err := i.store.Pool().Query(ctx,
`select s.name
from secret s
join node c on c.id = s.consumer
join node p on p.id = s.provider
where c.name = $1 and s.consumer_module = $2 and s.origin = 'accepted'
and s.consumer_key = c.sealing_key and s.provider_key = p.sealing_key`, machine, module)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
out[name] = true
}
return out, rows.Err()
}
+195
View File
@@ -0,0 +1,195 @@
package inventory
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// outsideProvision registers a provider offering tv-api with a credential issued outside the mesh (novox/hq ADR
// 0337 §1) and two consumers requiring it, on twoNodesWithKeys' machines.
func outsideProvision(t *testing.T, inv *Inventory) {
t.Helper()
ctx := t.Context()
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}, Source{}); err != nil {
t.Fatal(err)
}
for _, m := range []string{"requests", "indexer"} {
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": "/run/tv-api.secret"}}, Source{}); err != nil {
t.Fatal(err)
}
}
}
// pairRows is how many pair rows the store holds for tv-api.
func pairRows(t *testing.T, inv *Inventory) int {
t.Helper()
var n int
if err := inv.store.Pool().QueryRow(t.Context(), `select count(*) from secret where name = 'tv-api'`).Scan(&n); err != nil {
t.Fatal(err)
}
return n
}
// A credential only a person can give is never made (ADR 0337 §2, issue 164): not for a pair with no row, which is
// what a newly bound consumer has, and not after a key change; a value the mesh made before the rule is not given.
func TestACredentialIssuedOutsideIsNeverMade(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
outsideProvision(t, inv)
_, err := inv.SecretFor(ctx, "tv-api", "consumer", "requests", "provider", "")
if !IsNotGiven(err) {
t.Fatalf("SecretFor made or refused a credential only a person gives, rather than saying it was not given: %v", err)
}
if !strings.Contains(err.Error(), "secret accept provider tv tv-api") {
t.Fatalf("not given does not name the one line that gives it: %v", err)
}
if n := pairRows(t, inv); n != 0 {
t.Fatalf("the store holds %d rows for tv-api, which nobody gave", n)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); !IsNotGiven(err) {
t.Fatalf("a pair nobody gave read as given: %v", err)
}
if n := pairRows(t, inv); n != 0 {
t.Fatalf("reading the pair wrote %d rows", n)
}
// A value the mesh made before the rule (the minted value of issue 164) is not given.
if _, err := inv.store.Pool().Exec(ctx, `insert into secret (name, consumer, consumer_module, provider,
for_consumer, for_provider, consumer_key, provider_key, local)
select 'tv-api', c.id, 'indexer', p.id, 'x', 'y', c.sealing_key, p.sealing_key, ''
from node c, node p where c.name = 'consumer' and p.name = 'provider'`); err != nil {
t.Fatal(err)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "indexer", "provider", "tv", ""); !IsNotGiven(err) ||
!strings.Contains(err.Error(), "made by the mesh") {
t.Fatalf("a value the mesh made before the rule read as given: %v", err)
}
// Given, then a key changes: not given, and nothing is made in its place (ADR 0337 §8).
if _, err := inv.AcceptSecretForProvision(ctx, "tv-api", "provider", "tv", "the app's key",
[]PairEnd{{Consumer: "consumer", ConsumerModule: "requests"}}); err != nil {
t.Fatal(err)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); err != nil {
t.Fatalf("the value given does not read as given: %v", err)
}
cn, _ := inv.NodeByName(ctx, "consumer")
key, _ := aSealingKey(t)
if err := inv.RecordSealingKey(ctx, cn.ID, key); err != nil {
t.Fatal(err)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); !IsNotGiven(err) ||
!strings.Contains(err.Error(), "sealing key") {
t.Fatalf("a value given under a key the consumer no longer has read as given: %v", err)
}
var origin string
if err := inv.store.Pool().QueryRow(ctx, `select origin from secret where name = 'tv-api' and consumer_module = 'requests'`).
Scan(&origin); err != nil || origin != OriginAccepted {
t.Fatalf("the given row was replaced (origin %q, %v)", origin, err)
}
// Rotate refuses it, naming the accept (ADR 0337 §9).
err = inv.RotateSecret(ctx, "tv-api", "consumer", "requests", "provider", "")
if err == nil || !strings.Contains(err.Error(), "secret accept provider tv tv-api") {
t.Fatalf("rotate of a credential issued outside was not refused naming the accept: %v", err)
}
}
// One accept for a provider is sealed to every consumer named, to the provider and to the operator, under one
// generation, each row accepted (ADR 0337 §3, issue 165); a consumer with no sealing key is said, not sealed to.
func TestOneAcceptForAProviderIsSealedToEveryConsumer(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
outsideProvision(t, inv)
operatorKey, operatorOpens := aSealingKey(t)
if _, err := inv.SetOperatorKey(ctx, operatorKey); err != nil {
t.Fatal(err)
}
consumerKey, consumerOpens := aSealingKey(t)
cn, _ := inv.NodeByName(ctx, "consumer")
if err := inv.RecordSealingKey(ctx, cn.ID, consumerKey); err != nil {
t.Fatal(err)
}
if _, err := inv.AddNode(ctx, "keyless"); err != nil {
t.Fatal(err)
}
accepted, err := inv.AcceptSecretForProvision(ctx, "tv-api", "provider", "tv", "the app's key", []PairEnd{
{Consumer: "consumer", ConsumerModule: "requests"}, {Consumer: "consumer", ConsumerModule: "indexer"},
{Consumer: "keyless", ConsumerModule: "requests"}})
if err != nil {
t.Fatal(err)
}
if len(accepted.Sealed) != 2 || len(accepted.Unsealable) != 1 || accepted.Unsealable[0].Consumer != "keyless" {
t.Fatalf("the accept did not seal to both consumers with keys and say the keyless one: %+v", accepted)
}
if strings.Join(accepted.Machines, ",") != "consumer,provider" {
t.Fatalf("the machines to send are the provider's and every consumer's sealed to: %v", accepted.Machines)
}
rows, err := inv.store.Pool().Query(ctx, `select consumer_module, origin, coalesce(generation, ''), for_consumer,
coalesce(operator_sealed, '') from secret where name = 'tv-api'`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
generations := map[string]bool{}
seen := 0
for rows.Next() {
var module, origin, generation, forConsumer, forOperator string
if err := rows.Scan(&module, &origin, &generation, &forConsumer, &forOperator); err != nil {
t.Fatal(err)
}
seen++
if origin != OriginAccepted || generation == "" {
t.Fatalf("%s's row is not accepted under a generation (origin %q)", module, origin)
}
generations[generation] = true
if opened, ok := consumerOpens(forConsumer); !ok || string(opened) != "the app's key" {
t.Fatalf("%s's copy does not open to the value given", module)
}
if opened, ok := operatorOpens(forOperator); !ok || string(opened) != "the app's key" {
t.Fatalf("%s's operator copy does not open to the value given", module)
}
}
if seen != 2 || len(generations) != 1 {
t.Fatalf("%d rows under %d generations; want 2 under one", seen, len(generations))
}
given, err := inv.GivenPairsOf(ctx, "consumer", "indexer")
if err != nil || !given["tv-api"] {
t.Fatalf("the provision given does not read as given for indexer (%v)", err)
}
// Only an offer that says so is given this way.
if _, err := inv.AcceptSecretForProvision(ctx, "secret", "provider", "gitea", "x", nil); err == nil {
t.Fatal("a provision not issued outside was given once for its provider")
}
}
// The provider's own secret issued outside the mesh, named as a provision's shared credential (ADR 0158, ADR 0228),
// is never made for a consumer that binds: the consumer is told it was not given (ADR 0337 §2, §4).
func TestASharedOwnSecretIssuedOutsideIsNeverMade(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "downloader", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"password": {Path: "/run/password", Taken: catalogue.TakenAtStart,
IssuedBy: catalogue.IssuedOutside}},
Provides: []catalogue.Offer{{Name: "downloader-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{Own: "password"}}}}, Source{}); err != nil {
t.Fatal(err)
}
_, err := inv.SharedSecretFor(ctx, "downloader-api", "consumer", "gitea", "provider", "downloader", "", "password")
if !IsNotGiven(err) {
t.Fatalf("a shared own secret issued outside was made or refused rather than not given: %v", err)
}
var n int
if err := inv.store.Pool().QueryRow(ctx, `select count(*) from module_secret where module = 'downloader'`).Scan(&n); err != nil || n != 0 {
t.Fatalf("the mesh made %d values of an own secret issued outside (%v)", n, err)
}
if err := inv.remakeShared(ctx, nil, "", "downloader", "password", "", nil, "", "", ""); err == nil ||
!strings.Contains(err.Error(), "issued outside") {
t.Fatalf("remakeShared made a value of an own secret issued outside: %v", err)
}
}
+39
View File
@@ -104,6 +104,21 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
consumerModule, name, provider, consumer, consumerModule, name, provider, localFlag(local))
}
// **Never made where only a person can give it** (novox/hq ADR 0337 §2, issue 164): a provision whose offer
// says `issued-by: outside` gets no value of the mesh's, which its provider's software would refuse. Asked
// here, on the one path that makes a value, so no caller can have one made by not knowing; the plan reads
// such a pair with GivenPairSecret, which never makes one and delivers only a value a person gave.
why := "nobody gave it"
if err == nil {
why = "the value held was sealed to a key one end no longer has"
}
if providerModule, outside, oerr := i.issuedOutside(ctx, name); oerr != nil {
return Secret{}, oerr
} else if outside {
return Secret{}, &NotGivenError{Provision: name, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider, ProviderModule: providerModule, Local: local, Why: why}
}
// And to the operator, when the mesh has one (novox/hq ADR 0085, amended): the third copy that
// makes a vault-provided secret recoverable, and nothing the mesh can open.
operator, err := i.OperatorKey(ctx)
@@ -239,6 +254,16 @@ func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerMo
`select origin from secret where name = $1 and consumer = $2 and consumer_module = $3
and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local).Scan(&origin)
if providerModule, outside, oerr := i.issuedOutside(ctx, name); oerr != nil {
return oerr
} else if outside {
// Only a person can give it, so only a person can give its next value (novox/hq ADR 0337 §9, ADR 0092 §7):
// deleting the row would leave the consumer waiting for nothing new.
return fmt.Errorf(
"%s is issued outside the mesh: %s's software holds it and the mesh cannot make its replacement. "+
"Give the new value with `%s`, which seals it to every consumer of %s on %s",
name, providerModule, acceptLine(provider, providerModule, name), providerModule, provider)
}
if err == nil && origin == OriginAccepted {
return fmt.Errorf(
"%s's %q credential from %s was accepted from a person, and the mesh cannot make "+
@@ -886,6 +911,15 @@ func (i *Inventory) SharedSecretFor(ctx context.Context, provision, consumer, co
held.ConsumerModule, held.Local = consumerModule, local
return held, nil
}
if own, outside, err := i.ownIssuedOutside(ctx, providerModule, own); err != nil {
return Secret{}, err
} else if outside && (ownOrigin == nil || *ownOrigin != OriginAccepted) {
// The provider's own secret only an outside party issues, which nobody gave (ADR 0228, issue 401): the
// mesh makes no value for it here either, and the consumer is told it was not given (ADR 0337 §4).
return Secret{}, &NotGivenError{Provision: provision, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider, ProviderModule: providerModule, Local: local, Own: own,
Why: "nobody gave " + providerModule + "'s own secret " + own + ", which is issued outside the mesh"}
}
if ownOrigin != nil && *ownOrigin == OriginAccepted {
return Secret{}, fmt.Errorf(
"%s on %s needs %s from %s, whose credential is %s's own secret %q — a value given to the "+
@@ -912,6 +946,11 @@ func (i *Inventory) remakeShared(ctx context.Context, providerID any, providerKe
if len(provisions) == 0 {
return fmt.Errorf("%s names no provision whose credential is its own secret %q", providerModule, own)
}
if m.OwnSecrets[own].IssuedBy == catalogue.IssuedOutside {
// Never made (novox/hq ADR 0228, ADR 0337 §2): only a person can give it, with `secret accept`.
return fmt.Errorf("%s's own secret %q is issued outside the mesh, so the mesh makes no value for it; a "+
"person gives it with `secret accept <machine> %s %s`", providerModule, own, providerModule, own)
}
operator, err := i.OperatorKey(ctx)
if err != nil {
return err
+4 -1
View File
@@ -577,7 +577,10 @@ type Wait struct {
Part string `json:"part"`
Secret string `json:"secret,omitempty"`
Setting string `json:"setting,omitempty"`
What string `json:"what"`
// Provision is a provision the module requires whose credential only a person can give and nobody gave yet
// (novox/hq ADR 0337 §5): a wait names exactly one of Secret, Setting or Provision.
Provision string `json:"provision,omitempty"`
What string `json:"what"`
}
// ResourceHealth is one long-running resource's state.
+5
View File
@@ -45,3 +45,8 @@
./cmd/mesh-controller TestReplay348AFaultFromBeforeTheSendFailsNoGate
./cmd/mesh-controller TestReplay239AModuleNameIsNotTakenOverByAnotherRepository
./cmd/mesh-controller TestAWalkThatMovedNothingSaysWhichModulesMatchedWhatRuns
./internal/inventory TestACredentialIssuedOutsideIsNeverMade
./internal/inventory TestOneAcceptForAProviderIsSealedToEveryConsumer
./internal/inventory TestASharedOwnSecretIssuedOutsideIsNeverMade
./cmd/mesh-controller TestOneAcceptForAProviderIsPlacedForEveryConsumer
./cmd/mesh-controller TestReplay164