mesh/delivery merged: source.main moved to novox/mesh-sdk@d6c87ee1a7ae
mesh/delivery-group group fix/1126-provision-credentials merged: every member is merged
mesh/check pass c459f757308a on 036266daff85 | unit 6028 run, 142 kept, 16 skipped | integration 6091 run, 174 kept, 25 skipped | over bound: unit, in…
After the controller, nothing makes a pair row for a new consumer, so a need whose provider offers no credential (form none) was composed with a stand-in on both ends and the node-engine held the consumer (issue 1126: mesh-cli on ace, "stands in for the grant of mcp-endpoint"). The vault never mints for form none, so such a need now composes with no credential: the consumer's need carries none, the provider's grant carries the consumer and nothing sealed, neither is named foreseen, CredentialsNeeded asks the vault for neither, and no empty grant file is written. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
550 lines
30 KiB
Go
550 lines
30 KiB
Go
package compose
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// secretValue is a value the Input carries that must never appear in anything the composition says.
|
|
const secretValue = "SECRET-VALUE-THAT-MUST-NOT-LEAK"
|
|
|
|
func rendered(t *testing.T, in Input, node string, mode Mode) Rendered {
|
|
t.Helper()
|
|
got, err := Render(in, node, mode)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
// anAssistant is mesh-controller's outside_not_given_test.go anAssistant (cmd/mesh-controller at 6c2a8d62): a module with a secret
|
|
// the mesh makes and one only an outside party issues.
|
|
func anAssistant() catalogue.Manifest {
|
|
return catalogue.Manifest{Module: "assistant", Version: "1",
|
|
OwnSecrets: catalogue.OwnSecrets{
|
|
"password": {Path: "/var/lib/mesh/assistant/password", Taken: catalogue.TakenAtStart},
|
|
"vendor-key": {Path: "/var/lib/mesh/assistant/vendor-key", Taken: catalogue.TakenAtStart, IssuedBy: catalogue.IssuedOutside},
|
|
},
|
|
Resources: []map[string]any{{"id": "state", "type": "directory", "path": "/var/lib/mesh/assistant", "mode": "0700"}}}
|
|
}
|
|
|
|
func withAssistant(t *testing.T) Input {
|
|
t.Helper()
|
|
in := aMesh(t)
|
|
in.Catalogue["assistant"] = anAssistant()
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
laptop.Assigned = append(laptop.Assigned, "assistant")
|
|
laptop.SealingKey = keyOf(t, "laptop")
|
|
return in
|
|
}
|
|
|
|
// Own secrets (plan.go renderingFor, novox/hq issue 275 and 401): one held and sealed to the machine's key is carried; one not held is
|
|
// foreseen in the Foreseeing mode and left out in the Reading mode, and nothing is made.
|
|
func TestAnOwnSecretHeldIsCarriedAndOneNotHeldIsForeseenOrLeftOut(t *testing.T) {
|
|
mk := func() Input {
|
|
in := withAssistant(t)
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made",
|
|
SealedTo: nodeOf(t, &in, "laptop").SealingKey, Sealed: secretValue}}
|
|
return in
|
|
}
|
|
in := mk()
|
|
got := rendered(t, in, "laptop", Foreseeing)
|
|
if got.Rendering.Needed["assistant"]["password"] != secretValue || len(got.Rendering.Foreseen) != 0 {
|
|
t.Errorf("a held secret: needed %v foreseen %v", got.Rendering.Needed, got.Rendering.Foreseen)
|
|
}
|
|
// Not held: foreseen with a stand-in (Foreseeing), left out (Reading).
|
|
in.OwnSecrets = nil
|
|
got = rendered(t, in, "laptop", Foreseeing)
|
|
if !got.Rendering.Foreseen["assistant"]["password"] || got.Rendering.Needed["assistant"]["password"] != "" ||
|
|
len(got.Foreseen) != 1 || got.Foreseen[0] != "assistant/password" {
|
|
t.Errorf("a secret the send makes: foreseen %v / %v needed %v", got.Rendering.Foreseen, got.Foreseen, got.Rendering.Needed)
|
|
}
|
|
got = rendered(t, in, "laptop", Reading)
|
|
if len(got.Rendering.Needed["assistant"]) != 0 || len(got.Foreseen) != 0 || got.Rendering.Foreseen != nil {
|
|
t.Errorf("Reading composed a secret that was never issued: %v %v", got.Rendering.Needed, got.Foreseen)
|
|
}
|
|
// Held but sealed to a key the machine no longer has is not held.
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: "an-old-key", Sealed: secretValue}}
|
|
if got := rendered(t, in, "laptop", Foreseeing); !got.Rendering.Foreseen["assistant"]["password"] {
|
|
t.Errorf("a secret sealed to an old key is held: %v", got.Rendering.Needed)
|
|
}
|
|
// Held with no value in the Input: a stand-in, named.
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: nodeOf(t, &in, "laptop").SealingKey}}
|
|
if got := rendered(t, in, "laptop", Reading); !got.Rendering.Foreseen["assistant"]["password"] {
|
|
t.Errorf("a held secret with no value is not named: %v", got.Rendering.Foreseen)
|
|
}
|
|
unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) })
|
|
|
|
// 1126 (ADR 0547 §2): a node applying what it composed itself never applies a stand-in: a held secret with no value in the
|
|
// Input is refused, naming it; held with its value, it composes.
|
|
fps := map[string]string{"assistant": "src2:" + strings.Repeat("a", 64), overlay.Name: "src2:" + strings.Repeat("b", 64)}
|
|
if _, _, err := CompositionsApplied(in, "laptop", fps); !errors.Is(err, ErrStoodIn) || !strings.Contains(err.Error(), "assistant/password") {
|
|
t.Errorf("a stand-in was not refused for a node's own apply: %v", err)
|
|
}
|
|
if out, _, err := CompositionsApplied(mk(), "laptop", fps); err != nil || len(out) == 0 {
|
|
t.Errorf("a held secret with its value is refused for a node's own apply: %v", err)
|
|
}
|
|
}
|
|
|
|
// An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228): nothing is composed or
|
|
// foreseen for it; a value the mesh made for it is not one a person gave; a value given is placed once it is sealed to the key held now.
|
|
func TestAnOutsideSecretIsPlacedOnlyOnceAPersonGaveIt(t *testing.T) {
|
|
in := withAssistant(t)
|
|
key := nodeOf(t, &in, "laptop").SealingKey
|
|
for _, mode := range []Mode{Reading, Foreseeing} {
|
|
got := rendered(t, in, "laptop", mode)
|
|
if _, there := got.Rendering.Needed["assistant"]["vendor-key"]; there || got.Rendering.Foreseen["assistant"]["vendor-key"] {
|
|
t.Fatalf("%v: a secret nobody gave is composed: %v %v", mode, got.Rendering.Needed, got.Rendering.Foreseen)
|
|
}
|
|
}
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "vendor-key", Origin: "made", SealedTo: key, Sealed: secretValue}}
|
|
if _, there := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; there {
|
|
t.Error("a value the mesh made for a secret issued outside was placed")
|
|
}
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "vendor-key", Origin: "accepted", SealedTo: key, Sealed: "the vendor's key, sealed"}}
|
|
if got := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; got != "the vendor's key, sealed" {
|
|
t.Errorf("a value given is placed as %q", got)
|
|
}
|
|
in.OwnSecrets[0].SealedTo = "an-old-key"
|
|
if _, there := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; there {
|
|
t.Error("a value given under an old key was placed")
|
|
}
|
|
}
|
|
|
|
// 1364/AC3 (the send's refusals, novox/hq issue 275): in the Foreseeing mode a given secret under an old key is refused in the
|
|
// controller's words. No secret value is in an error. A bus account not yet issued is foreseen, issued or not (issue 774: the vault
|
|
// mints it once the placement is written on the Approve, ADR 0561 §1, ADR 0567 §5; mesh-self-check's proposal on ace was refused
|
|
// "has no bus credential: nothing was issued for ace.mesh-self-check").
|
|
func TestTheSendsRefusalsAreRefusedInTheSendsWords(t *testing.T) {
|
|
broker := catalogue.Manifest{Module: "mesh-self-check", Version: "1", RunsAs: "mesh-self-check",
|
|
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/mesh-self-check/broker", Taken: catalogue.TakenAtStart}}}
|
|
in := withAssistant(t)
|
|
in.Catalogue["mesh-self-check"] = broker
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
laptop.Assigned = append(laptop.Assigned, "mesh-self-check")
|
|
for _, minted := range [][]string{nil, {"laptop.mesh-self-check"}} {
|
|
in.BusMinted = minted
|
|
got, err := Render(in, "laptop", Foreseeing)
|
|
if err != nil || !got.Rendering.Foreseen["mesh-self-check"]["broker"] {
|
|
t.Fatalf("minted %v: the bus account is not foreseen: %v", minted, err)
|
|
}
|
|
// Reading refuses nothing for it: the machine is not running it.
|
|
if _, err := Render(in, "laptop", Reading); err != nil {
|
|
t.Fatalf("Reading refused a secret the machine is not running: %v", err)
|
|
}
|
|
}
|
|
// A given secret under an old key.
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "accepted", SealedTo: "an-old-key", Sealed: secretValue},
|
|
{Node: "laptop", Module: "mesh-self-check", Name: "broker", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}}
|
|
_, err := Render(in, "laptop", Foreseeing)
|
|
wantOld := "assistant on laptop holds \"password\", which was given to the mesh rather than made by it, and laptop has since generated a new " +
|
|
"sealing key. The mesh cannot make another; issue it again"
|
|
if err == nil || err.Error() != wantOld {
|
|
t.Fatalf("got %v, want %q", err, wantOld)
|
|
}
|
|
if strings.Contains(err.Error(), secretValue) {
|
|
t.Error("a secret value is in an error")
|
|
}
|
|
// A machine with no sealing key cannot be sealed to.
|
|
laptop.SealingKey = ""
|
|
in.OwnSecrets = nil
|
|
if _, err := Render(in, "laptop", Foreseeing); err == nil || !strings.Contains(err.Error(), "laptop has no sealing key, so nothing can be sealed to it") {
|
|
t.Fatalf("no sealing key: %v", err)
|
|
}
|
|
}
|
|
|
|
// aGrantMesh is the objects provider on anchor and its consumer files on laptop, with the pair credential made.
|
|
func aGrantMesh(t *testing.T) Input {
|
|
t.Helper()
|
|
in := theObjectsMesh(t, objects())
|
|
in.Issued = []Issued{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", ForProvider: "sealed-to-anchor"}}
|
|
return in
|
|
}
|
|
|
|
// A credential made for a consumer is a grant on its provider's machine, carrying the consumer's values and the sealed copy; without
|
|
// the sealed copy a stand-in is composed and named (novox/hq plan.go grantsFor).
|
|
func TestAGrantIsCreatedForEachCredentialMadeForAConsumer(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
// 1126 (ADR 0550 §5): the credentials each node's composition reads, by their ends: laptop's files consumes anchor's
|
|
// s3-bucket, and anchor grants it; the node composing its own asks the vault for exactly these.
|
|
pair := CredentialKey{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor"}
|
|
if consumer, provider, err := CredentialsNeeded(in, "laptop"); err != nil || len(provider) != 0 || len(consumer) != 1 || consumer[0] != pair {
|
|
t.Errorf("laptop needs %v as consumer and %v as provider (%v), want %v as consumer", consumer, provider, err, pair)
|
|
}
|
|
if consumer, provider, err := CredentialsNeeded(in, "anchor"); err != nil || len(consumer) != 0 || len(provider) != 1 || provider[0] != pair {
|
|
t.Errorf("anchor needs %v as consumer and %v as provider (%v), want %v as provider", consumer, provider, err, pair)
|
|
}
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if len(got.Rendering.Grants) != 1 {
|
|
t.Fatalf("grants are %+v", got.Rendering.Grants)
|
|
}
|
|
g := got.Rendering.Grants[0]
|
|
if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.At != "laptop.internal" || g.Sealed != "sealed-to-anchor" {
|
|
t.Errorf("the grant is %+v", g)
|
|
}
|
|
if len(got.Foreseen) != 0 || len(got.Withheld) != 0 || len(got.Unbound) != 0 {
|
|
t.Errorf("foreseen %v withheld %v unbound %v", got.Foreseen, got.Withheld, got.Unbound)
|
|
}
|
|
in.Issued[0].ForProvider = ""
|
|
got = rendered(t, in, "anchor", Foreseeing)
|
|
if got.Rendering.Grants[0].Sealed != catalogue.ForeseenSealed || len(got.Foreseen) != 1 {
|
|
t.Errorf("a grant with no sealed copy: %+v foreseen %v", got.Rendering.Grants, got.Foreseen)
|
|
}
|
|
// A credential made for a consumer on another provider is not this node's grant.
|
|
if len(rendered(t, in, "laptop", Foreseeing).Rendering.Grants) != 0 {
|
|
t.Error("the consumer is granted its own credential")
|
|
}
|
|
// A consumer whose set does not resolve is skipped, not fatal.
|
|
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files")
|
|
in.Catalogue["files"] = catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"},
|
|
Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}}
|
|
in.Catalogue["twin"] = catalogue.Manifest{Module: "twin", Version: "1", Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}}
|
|
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "twin")
|
|
if got, err := Render(in, "anchor", Foreseeing); err != nil || len(got.Rendering.Grants) != 0 {
|
|
t.Errorf("a consumer that does not resolve: %v %+v", err, got.Rendering.Grants)
|
|
}
|
|
}
|
|
|
|
// 1364/AC2: a consumer the grants leave out because its identity overflows the provision's bound is returned beside them, naming the
|
|
// consumer and the provision (novox/hq ADR 0225), and is not granted.
|
|
func TestAConsumerWhoseIdentityOverflowsIsLeftOutOfTheGrantsAndNamed(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
in.Nodes[1].Name = "a-machine-with-a-very-long-name"
|
|
in.Issued[0].Consumer = "a-machine-with-a-very-long-name"
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if len(got.Rendering.Grants) != 0 || len(got.Withheld) != 1 {
|
|
t.Fatalf("grants %+v withheld %+v", got.Rendering.Grants, got.Withheld)
|
|
}
|
|
w := got.Withheld[0]
|
|
if w.Consumer != "a-machine-with-a-very-long-name" || w.Provision != "s3-bucket" || w.Provider != "anchor" || w.Module != "files" ||
|
|
!strings.Contains(w.String(), "a-machine-with-a-very-long-name") || !strings.Contains(w.String(), "s3-bucket") {
|
|
t.Errorf("the consumer left out is %+v / %s", w, w.String())
|
|
}
|
|
if len(got.Rendering.Withheld) != 1 {
|
|
t.Errorf("the Rendering does not carry it: %+v", got.Rendering.Withheld)
|
|
}
|
|
}
|
|
|
|
// 1364/AC3: a credential on record for a consumer that its own resolution binds to another provider is withdrawn, named with where it is
|
|
// bound (novox/hq issue 274): the grant stays, with no consumer module asking.
|
|
func TestACredentialBoundToAnotherProviderIsWithdrawnAndSaidWhereItIsBound(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
second := objects()
|
|
second.Module = "objects2"
|
|
in.Catalogue["objects2"] = second
|
|
in.Nodes = append(in.Nodes, Node{Name: "ace", Place: Place{Site: "there", Address: "10.77.0.3"},
|
|
Assigned: []string{overlay.Name, "objects2"}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}})
|
|
nodeOf(t, &in, "laptop").Pins = map[string]catalogue.Chosen{"s3-bucket": {Node: "ace", Module: "objects2"}}
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if len(got.Unbound) != 1 {
|
|
t.Fatalf("unbound %+v grants %+v", got.Unbound, got.Rendering.Grants)
|
|
}
|
|
u := got.Unbound[0]
|
|
if u.Provision != "s3-bucket" || u.Provider != "anchor" || u.Consumer != "laptop" || u.Module != "files" ||
|
|
len(u.BoundTo) != 1 || u.BoundTo[0] != "ace" || !strings.Contains(u.String(), "ace") {
|
|
t.Errorf("unbound is %+v / %s", u, u.String())
|
|
}
|
|
if len(got.Rendering.Grants) != 1 || got.Rendering.Grants[0].From != "" {
|
|
t.Errorf("the withdrawn credential is still asked for: %+v", got.Rendering.Grants)
|
|
}
|
|
}
|
|
|
|
func aCertificateMesh(t *testing.T) Input {
|
|
t.Helper()
|
|
in := aMesh(t)
|
|
in.Catalogue["secure"] = catalogue.Manifest{Module: "secure", Version: "1", Certificate: &catalogue.Certificate{Into: "/etc/secure/tls.pem", Authority: "/etc/secure/ca.pem"}}
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
laptop.Assigned = append(laptop.Assigned, "secure")
|
|
return in
|
|
}
|
|
|
|
// 1364/AC4: with no certificate and authority in the Input a stand-in is composed and named; given them, the composition carries them as
|
|
// given. A machine that never reported a serving key, and one not on the private network, are refused in the controller's words.
|
|
func TestACertificateIsCarriedAsGivenOrStoodInForAndNamed(t *testing.T) {
|
|
in := aCertificateMesh(t)
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
if _, err := Render(in, "laptop", Foreseeing); err == nil || err.Error() != "laptop wants a certificate and has never told the mesh what key it serves with; it joins again to report one" {
|
|
t.Fatalf("no serving key: %v", err)
|
|
}
|
|
laptop.ServingKey = "serving-key"
|
|
got := rendered(t, in, "laptop", Foreseeing)
|
|
if got.Rendering.Certificate != ForeseenCertificate || got.Rendering.Authority != ForeseenCertificate || len(got.Foreseen) != 1 {
|
|
t.Errorf("no certificate in the Input: %q %q foreseen %v", got.Rendering.Certificate, got.Rendering.Authority, got.Foreseen)
|
|
}
|
|
laptop.Certificate, in.Authority = "the-certificate", "the-authority"
|
|
got = rendered(t, in, "laptop", Foreseeing)
|
|
if got.Rendering.Certificate != "the-certificate" || got.Rendering.Authority != "the-authority" || len(got.Foreseen) != 0 {
|
|
t.Errorf("a certificate given: %q %q foreseen %v", got.Rendering.Certificate, got.Rendering.Authority, got.Foreseen)
|
|
}
|
|
laptop.Place.Address = "" // placed nowhere: no name inside the mesh
|
|
if _, err := Render(in, "laptop", Foreseeing); err == nil || err.Error() != "laptop wants a certificate and is not on the private network, so it has no name inside the mesh to be certified for" {
|
|
t.Fatalf("off the network: %v", err)
|
|
}
|
|
}
|
|
|
|
func aValidKey(t *testing.T, name string) string { return keyOf(t, name) }
|
|
|
|
// 1364/AC5 and AC6: a core credential the node needs and the Input holds no box for is composed with a stand-in and returned as one
|
|
// to keep; a kept box sealed to the key held now is carried; a node-engine below the vault-box contract is sent none (and nothing is
|
|
// asked of it); a machine with no sealing key has its core module left out, said.
|
|
func TestACoreBoxTheInputHoldsNoneOfIsReturnedToKeepAndNothingIsWritten(t *testing.T) {
|
|
mk := func() Input {
|
|
in := aMesh(t)
|
|
in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1",
|
|
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/nats/broker", Taken: catalogue.TakenAtStart}}}
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
laptop.Assigned = append(laptop.Assigned, "nats")
|
|
laptop.SealingKey = aValidKey(t, "laptop")
|
|
laptop.Stated, laptop.Contract = true, VaultBoxContract
|
|
in.BusMinted = []string{"laptop.nats"}
|
|
return in
|
|
}
|
|
in := mk()
|
|
got := rendered(t, in, "laptop", Foreseeing)
|
|
box := got.Rendering.VaultSealed["nats"]["broker"]
|
|
if string(box) != string(catalogue.ForeseenVaultBox) || len(got.CoreBoxesWanted) != 1 ||
|
|
got.CoreBoxesWanted[0] != (CoreBoxWanted{Node: "laptop", Module: "nats", Name: "broker"}) {
|
|
t.Errorf("no box held: %s wanted %v", box, got.CoreBoxesWanted)
|
|
}
|
|
kept := json.RawMessage(`{"v":1,"eph":"e","iv":"i","tag":"t","ct":"c"}`)
|
|
in.KeptCoreBoxes = []CoreBox{{Node: "laptop", Module: "nats", Name: "broker", Generation: 3, SealedTo: nodeOf(t, &in, "laptop").SealingKey, Box: kept}}
|
|
got = rendered(t, in, "laptop", Foreseeing)
|
|
if string(got.Rendering.VaultSealed["nats"]["broker"]) != string(kept) || len(got.CoreBoxesWanted) != 0 {
|
|
t.Errorf("a kept box: %s wanted %v", got.Rendering.VaultSealed["nats"]["broker"], got.CoreBoxesWanted)
|
|
}
|
|
in.KeptCoreBoxes[0].SealedTo = "an-old-key"
|
|
if got = rendered(t, in, "laptop", Foreseeing); len(got.CoreBoxesWanted) != 1 {
|
|
t.Errorf("a box sealed to an old key is kept: %v", got.CoreBoxesWanted)
|
|
}
|
|
// Below the vault-box contract: none, and the controller's own seal as before.
|
|
nodeOf(t, &in, "laptop").Contract = VaultBoxContract - 1
|
|
if got = rendered(t, in, "laptop", Foreseeing); got.Rendering.VaultSealed != nil || len(got.CoreBoxesWanted) != 0 {
|
|
t.Errorf("an engine below the contract is sent boxes: %v %v", got.Rendering.VaultSealed, got.CoreBoxesWanted)
|
|
}
|
|
// No sealing key: the module is left out, said.
|
|
nodeOf(t, &in, "laptop").Contract, nodeOf(t, &in, "laptop").SealingKey = VaultBoxContract, ""
|
|
got = rendered(t, in, "laptop", Reading)
|
|
if why := got.Rendering.NotPlaced["nats"]; !strings.Contains(why, "laptop has no sealing key, so the vault can seal nats's credentials to nothing there") {
|
|
t.Errorf("not placed: %v", got.Rendering.NotPlaced)
|
|
}
|
|
unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) })
|
|
}
|
|
|
|
// 1364/AC1: the bus's user list is carried for the machine that runs the bus and for no other; without it a stand-in is composed and
|
|
// named; the users left out for want of a credential are said; each machine's membership is carried. (The list is composed by the
|
|
// controller from the bus records, which stay the controller's: see the commit message.)
|
|
func TestTheBusUserListIsCarriedForTheMachineThatRunsTheBus(t *testing.T) {
|
|
in := aMesh(t)
|
|
in.Catalogue["bus"] = catalogue.Manifest{Module: "bus", Version: "1", BusUsers: "/etc/bus/accounts.conf",
|
|
Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}}
|
|
anchor := nodeOf(t, &in, "anchor")
|
|
anchor.Assigned = append(anchor.Assigned, "bus")
|
|
in.BusUsers = "accounts { anchor.bus: hash }"
|
|
in.BusMissing = []string{"laptop.files", "laptop.web"}
|
|
in.BusMemberships = map[string]string{"laptop": "sealed-membership"}
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if got.Rendering.BusUsers != in.BusUsers || len(got.Foreseen) != 0 {
|
|
t.Errorf("the list is %q foreseen %v", got.Rendering.BusUsers, got.Foreseen)
|
|
}
|
|
if len(got.Said) != 1 || got.Said[0] != "the bus's user list leaves out 2 user(s) the mesh has minted no credential for: laptop.files, laptop.web. Each is a user that cannot connect until one is issued" {
|
|
t.Errorf("said %v", got.Said)
|
|
}
|
|
if other := rendered(t, in, "laptop", Foreseeing); other.Rendering.BusUsers != "" || other.Rendering.BusMembership != "sealed-membership" {
|
|
t.Errorf("laptop: list %q membership %q", other.Rendering.BusUsers, other.Rendering.BusMembership)
|
|
}
|
|
in.BusUsers = ""
|
|
got = rendered(t, in, "anchor", Foreseeing)
|
|
if got.Rendering.BusUsers != ForeseenBusUsers || len(got.Foreseen) != 1 {
|
|
t.Errorf("no list in the Input: %q foreseen %v", got.Rendering.BusUsers, got.Foreseen)
|
|
}
|
|
}
|
|
|
|
// 1364/AC7: nothing the composition says carries a secret value the Input holds.
|
|
func TestNoSecretValueOfTheInputIsInWhatTheCompositionSays(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
in.Issued[0].ForProvider = secretValue
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
in.Catalogue["assistant"] = anAssistant()
|
|
laptop.Assigned = append(laptop.Assigned, "assistant")
|
|
laptop.SealingKey = keyOf(t, "laptop")
|
|
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}}
|
|
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld, Sealed: secretValue}}
|
|
for _, node := range []string{"anchor", "laptop"} {
|
|
got := rendered(t, in, node, Foreseeing)
|
|
for _, said := range append(append([]string{}, got.Foreseen...), got.Said...) {
|
|
if strings.Contains(said, secretValue) {
|
|
t.Errorf("%s: %q carries a secret value", node, said)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A consumer no credential was made for yet is granted with a stand-in and named foreseen, for every form whose credential
|
|
// the vault mints or takes in; an offer of form none has no credential (issue 1126, 2026-11-02: mesh-cli on ace held for
|
|
// "the grant of mcp-endpoint", whose provider node-tools offers form none): its grant carries the consumer and no
|
|
// credential, nothing stands in on either side, and neither end asks the vault for one.
|
|
func TestAConsumerNoCredentialWasMadeForYetIsGrantedWithAStandInUnlessItsOfferHasNone(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
form string
|
|
cred *catalogue.OfferCredential
|
|
}{
|
|
{"undeclared", nil},
|
|
{catalogue.CredentialFormPair, &catalogue.OfferCredential{Form: catalogue.CredentialFormPair}},
|
|
{catalogue.CredentialFormShared, &catalogue.OfferCredential{Own: "access-key"}},
|
|
{catalogue.CredentialFormNone, &catalogue.OfferCredential{Form: catalogue.CredentialFormNone}},
|
|
} {
|
|
in := aGrantMesh(t)
|
|
in.Issued = nil
|
|
provider := in.Catalogue["objects"]
|
|
provider.Provides[0].Credential = tc.cred
|
|
if tc.cred != nil && tc.cred.Own != "" {
|
|
provider.OwnSecrets = catalogue.OwnSecrets{"access-key": {Path: "/var/lib/mesh/objects/access-key"}}
|
|
}
|
|
in.Catalogue["objects"] = provider
|
|
none := tc.form == catalogue.CredentialFormNone
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if len(got.Rendering.Grants) != 1 {
|
|
t.Fatalf("%s: grants are %+v", tc.form, got.Rendering.Grants)
|
|
}
|
|
g, wantSealed := got.Rendering.Grants[0], catalogue.ForeseenSealed
|
|
if none {
|
|
wantSealed = ""
|
|
}
|
|
if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.Sealed != wantSealed {
|
|
t.Errorf("%s: the grant is %+v", tc.form, g)
|
|
}
|
|
if has(got.Foreseen, "the grant of s3-bucket for files on laptop") == none {
|
|
t.Errorf("%s: foreseen %v", tc.form, got.Foreseen)
|
|
}
|
|
consumer, asProvider, err := CredentialsNeeded(in, "laptop")
|
|
_, anchorProvides, err2 := CredentialsNeeded(in, "anchor")
|
|
if err != nil || err2 != nil || len(asProvider) != 0 || (len(consumer) == 0) != none || (len(anchorProvides) == 0) != none {
|
|
t.Errorf("%s: laptop asks %v, anchor provides %v (%v %v)", tc.form, consumer, anchorProvides, err, err2)
|
|
}
|
|
resolved, err := Resolve(in, "laptop")
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.form, err)
|
|
}
|
|
if n := theFilesNeed(t, resolved); (n.Sealed == "") != none || (len(resolved.Foreseen) == 0) != none {
|
|
t.Errorf("%s: the consumer's credential is %q, foreseen %v", tc.form, n.Sealed, resolved.Foreseen)
|
|
}
|
|
if lap := rendered(t, in, "laptop", Foreseeing); none && len(lap.Foreseen) != 0 {
|
|
t.Errorf("%s: the consumer stands in for %v", tc.form, lap.Foreseen)
|
|
}
|
|
// Only the provider's own: the consumer is granted nothing, and nothing in the Input changed.
|
|
if len(rendered(t, in, "laptop", Foreseeing).Rendering.Grants) != 0 {
|
|
t.Errorf("%s: the consumer is granted its own credential", tc.form)
|
|
}
|
|
if len(in.Issued) != 0 {
|
|
t.Errorf("%s: a row was added to the Input", tc.form)
|
|
}
|
|
// A row made long ago for an offer of form none (the controller's) carries no credential either.
|
|
in.Issued = []Issued{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", ForProvider: "sealed-to-anchor"}}
|
|
if g := rendered(t, in, "anchor", Foreseeing).Rendering.Grants; none && (len(g) != 1 || g[0].Sealed != "") {
|
|
t.Errorf("%s: an old row's grant is %+v", tc.form, g)
|
|
}
|
|
}
|
|
// A credential only a person gives and nobody has is no grant.
|
|
in := aGrantMesh(t)
|
|
in.Issued = nil
|
|
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialNotGiven}}
|
|
if got := rendered(t, in, "anchor", Foreseeing); len(got.Rendering.Grants) != 0 {
|
|
t.Errorf("a credential nobody gave is granted: %+v", got.Rendering.Grants)
|
|
}
|
|
}
|
|
|
|
// And a consumer whose identity overflows the provision's bound is judged before any credential is made for it (issue 263).
|
|
func TestAnOverflowingConsumerNoCredentialWasMadeForYetIsWithheld(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
in.Issued = nil
|
|
in.Catalogue["resolver"] = storeManifests()[1]
|
|
in.Catalogue["networkmanager"] = catalogue.Manifest{Module: "networkmanager", Version: "1", Requires: []string{"wildcard-resolution", "s3-bucket"}}
|
|
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "networkmanager")
|
|
nodeOf(t, &in, "anchor").Assigned = append(nodeOf(t, &in, "anchor").Assigned, "resolver")
|
|
in.Holdings = []catalogue.Held{{Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "resolver", Site: "here"}}
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if len(got.Withheld) != 1 || got.Withheld[0].Module != "networkmanager" {
|
|
t.Fatalf("withheld %+v", got.Withheld)
|
|
}
|
|
}
|
|
|
|
// The grants come in the order the controller reads them, whatever order the Input gives its rows.
|
|
func TestGrantsComeInTheOrderOfProvisionConsumerModuleAndLocal(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
in.Issued = []Issued{
|
|
{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Local: "b", Provider: "anchor", ForProvider: "x"},
|
|
{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Local: "a", Provider: "anchor", ForProvider: "x"},
|
|
}
|
|
rows, err := newComposer(in).rowsFor("anchor")
|
|
// The credential the same send would make (no local name) comes first, then the two the Input gives, a before b.
|
|
if err != nil || len(rows) != 3 || rows[0].Local != "" || rows[1].Local != "a" || rows[2].Local != "b" {
|
|
t.Errorf("rows are %+v %v", rows, err)
|
|
}
|
|
}
|
|
|
|
// Round 1, finding 4: a member of a secret family a person gave, whose sealed value the Input does not carry, is placed with a
|
|
// stand-in and named foreseen, not dropped as if nobody gave it.
|
|
func TestAGivenFamilyMemberWithoutItsValueIsAStandInAndNamedForeseen(t *testing.T) {
|
|
in := aMesh(t)
|
|
in.Catalogue["mailer"] = catalogue.Manifest{Module: "mailer", Version: "1",
|
|
OwnSecrets: catalogue.OwnSecrets{"credential-*": {Path: "/var/lib/mailer/credential-*.secret", Taken: catalogue.TakenAtStart, IssuedBy: catalogue.IssuedOutside}}}
|
|
laptop := nodeOf(t, &in, "laptop")
|
|
laptop.Assigned = append(laptop.Assigned, "mailer")
|
|
laptop.SealingKey = keyOf(t, "laptop")
|
|
in.OwnSecrets = []OwnSecretRow{
|
|
{Node: "laptop", Module: "mailer", Name: "credential-a", Origin: "accepted", SealedTo: laptop.SealingKey},
|
|
{Node: "laptop", Module: "mailer", Name: "credential-b", Origin: "accepted", SealedTo: laptop.SealingKey, Sealed: "b-sealed"},
|
|
}
|
|
got := rendered(t, in, "laptop", Foreseeing)
|
|
needed := got.Rendering.Needed["mailer"]
|
|
if needed["credential-a"] != catalogue.ForeseenSealed || needed["credential-b"] != "b-sealed" {
|
|
t.Errorf("needed %v", needed)
|
|
}
|
|
if !has(got.Foreseen, "mailer/credential-a") || has(got.Foreseen, "mailer/credential-b") {
|
|
t.Errorf("foreseen %v", got.Foreseen)
|
|
}
|
|
}
|
|
|
|
// Round 1, finding 5: the suffix internal names end in is a field of the Input, and the environment is never read.
|
|
func TestTheInternalSuffixIsTheInputsAndNeverTheEnvironments(t *testing.T) {
|
|
t.Setenv(overlay.SuffixVar, "from-the-environment")
|
|
in := aGrantMesh(t)
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if got.Rendering.Suffix != "internal" || got.Rendering.Grants[0].At != "laptop.internal" {
|
|
t.Errorf("by default: suffix %q, grant at %q", got.Rendering.Suffix, got.Rendering.Grants[0].At)
|
|
}
|
|
in.InternalSuffix = ".lan"
|
|
got = rendered(t, in, "anchor", Foreseeing)
|
|
if got.Rendering.Suffix != "lan" || got.Rendering.Grants[0].At != "laptop.lan" {
|
|
t.Errorf("given: suffix %q, grant at %q", got.Rendering.Suffix, got.Rendering.Grants[0].At)
|
|
}
|
|
where, err := newComposer(in).whereEveryoneIs()
|
|
if err != nil || where["anchor"] != "anchor.lan" {
|
|
t.Errorf("where everyone is: %v %v", where, err)
|
|
}
|
|
if names := newComposer(in).namesInTheMesh(); names["anchor.lan"] == "" || names["anchor.internal"] != "" {
|
|
t.Errorf("names: %v", names)
|
|
}
|
|
}
|
|
|
|
// Round 2: a need answered on the same machine is a grant too (catalogue resolve.go gives it From: the node's own name).
|
|
func TestAConsumerOnTheProvidersOwnNodeIsGrantedWithAStandInAndNamedForeseen(t *testing.T) {
|
|
in := aGrantMesh(t)
|
|
in.Issued = nil
|
|
laptop, anchor := nodeOf(t, &in, "laptop"), nodeOf(t, &in, "anchor")
|
|
laptop.Assigned = []string{overlay.Name}
|
|
anchor.Assigned = append(anchor.Assigned, "files")
|
|
got := rendered(t, in, "anchor", Foreseeing)
|
|
if len(got.Rendering.Grants) != 1 || got.Rendering.Grants[0].Consumer != "anchor" || got.Rendering.Grants[0].Sealed != catalogue.ForeseenSealed {
|
|
t.Fatalf("grants are %+v", got.Rendering.Grants)
|
|
}
|
|
if !has(got.Foreseen, "the grant of s3-bucket for files on anchor") {
|
|
t.Errorf("foreseen %v", got.Foreseen)
|
|
}
|
|
}
|