Every reader takes format 2 since R4b (ADR 0456 clause 1), and the assigner's composition check reads the grants and bindings (ADR 0456 clause 2): the writer moves now, names only, never a sealed value.
159 lines
6.6 KiB
Go
159 lines
6.6 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// The facts snapshot (novox/hq to-be 45 §9): composed from the store, every machine under a pseudonym
|
|
// of its name's length, and nothing of the installation in it — no secret, no address, no name.
|
|
|
|
// aMeshWithSecrets is aMesh with a provider and its consumers, a value given by hand, settings carrying
|
|
// a password, an address and a machine's name, and a push's worth of credentials made.
|
|
func aMeshWithSecrets(t *testing.T) (*stores, []string) {
|
|
t.Helper()
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "objects", Version: "1",
|
|
Provides: []catalogue.Offer{{Name: "s3-bucket", Scope: catalogue.ScopeMesh,
|
|
Identity: &catalogue.OfferIdentity{Max: 20, In: "an S3 access key"}}},
|
|
Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}})
|
|
register(t, open, catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"},
|
|
Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/files/config.json",
|
|
"mode": "0600", "content": "{}", "merge": "json"}}})
|
|
for _, a := range [][2]string{{"anchor", "objects"}, {"laptop", "files"}} {
|
|
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
|
|
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
|
|
}
|
|
}
|
|
secrets := []string{"Hunter2-Is-Not-A-Password-9f8e7d", "0123456789abcdefABCDEF0123456789zz"}
|
|
if err := open.inventory.SetSettings(ctx, "", "files", map[string]any{
|
|
"admin_password": secrets[0], "upstream": "10.77.0.9", "hub": "anchor"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.SetSettings(ctx, "laptop", "files", map[string]any{
|
|
"note": "reach me at 192.168.1.135, token " + secrets[1]}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A push's worth of composition, which makes the pair credential the consumer is sent.
|
|
for _, node := range []string{"laptop", "anchor"} {
|
|
if _, err := composeSend(ctx, open, node); err != nil {
|
|
t.Fatalf("%s does not compose: %v", node, err)
|
|
}
|
|
}
|
|
issued, err := open.inventory.SecretsFrom(ctx, "anchor")
|
|
if err != nil || len(issued) == 0 {
|
|
t.Fatalf("no credential was made for the consumer: %v", err)
|
|
}
|
|
for _, s := range issued {
|
|
// Sealed, never kept plain (ADR 0004): the sealed blobs are what the store holds, and none may leave.
|
|
secrets = append(secrets, s.ForConsumer, s.ForProvider)
|
|
}
|
|
// A binding recorded for the consumer on the laptop, to the provider on the anchor.
|
|
if err := open.inventory.RecordBindings(ctx, "laptop", []inventory.Binding{{Machine: "laptop", Consumer: "files",
|
|
Provision: "s3-bucket", Provider: catalogue.Chosen{Node: "anchor", Module: "objects"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return open, secrets
|
|
}
|
|
|
|
func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) {
|
|
open, secrets := aMeshWithSecrets(t)
|
|
f, err := gatherFacts(t.Context(), open, "2.11.17")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.Format != 2 {
|
|
t.Fatalf("the writer wrote format %d, not 2 (issue 1394)", f.Format)
|
|
}
|
|
body, err := f.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := string(body)
|
|
for _, s := range secrets {
|
|
if s != "" && strings.Contains(text, s) {
|
|
t.Errorf("a secret is in the snapshot: %q", s)
|
|
}
|
|
}
|
|
for _, leaked := range []string{"anchor", "laptop", "10.77.0.", "192.168.1.135", ".example:51820"} {
|
|
if strings.Contains(text, leaked) {
|
|
t.Errorf("%q is in the snapshot", leaked)
|
|
}
|
|
}
|
|
// Every address it carries is a documentation address.
|
|
for _, a := range regexp.MustCompile(`\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`).FindAllString(text, -1) {
|
|
if !strings.HasPrefix(a, "192.0.2.") && !strings.HasPrefix(a, "198.51.100.") && !strings.HasPrefix(a, "203.0.113.") {
|
|
t.Errorf("%s is an address outside the documentation ranges", a)
|
|
}
|
|
}
|
|
|
|
// 1394/AC1: the provider's machine lists the grant, the consumer's machine the binding, machine names scrubbed.
|
|
anchorFacts, _ := f.Machine(snapshot.Pseudonym("machine", "anchor"))
|
|
laptopFacts, _ := f.Machine(snapshot.Pseudonym("machine", "laptop"))
|
|
wantGrant := snapshot.Grant{Provision: "s3-bucket", ConsumerNode: snapshot.Pseudonym("machine", "laptop"), ConsumerModule: "files"}
|
|
if len(anchorFacts.Grants) != 1 || anchorFacts.Grants[0] != wantGrant {
|
|
t.Errorf("the anchor's grants read as %+v, want %+v", anchorFacts.Grants, wantGrant)
|
|
}
|
|
wantBinding := snapshot.Binding{ConsumerModule: "files", Provision: "s3-bucket",
|
|
ProviderNode: snapshot.Pseudonym("machine", "anchor"), ProviderModule: "objects"}
|
|
if len(laptopFacts.Bindings) != 1 || laptopFacts.Bindings[0] != wantBinding {
|
|
t.Errorf("the laptop's bindings read as %+v, want %+v", laptopFacts.Bindings, wantBinding)
|
|
}
|
|
// 1394/AC2: the text scan above ran over a snapshot that holds those rows, so no sealed value is among them.
|
|
|
|
// What a check needs is there: every machine, its length, its modules, its declaration composing.
|
|
if len(f.Machines) != 2 || f.Longest() != len("laptop") {
|
|
t.Fatalf("machines %+v, longest %d", f.Machines, f.Longest())
|
|
}
|
|
anchor := snapshot.Pseudonym("machine", "anchor")
|
|
m, ok := f.Machine(anchor)
|
|
if !ok || !m.Hub || !strings.Contains(m.Described(), "the hub") || len(m.Name) != len("anchor") {
|
|
t.Fatalf("the anchor reads as %+v", m)
|
|
}
|
|
if !m.Declaration.Composes || m.Declaration.Digest == "" || len(m.Declaration.Resources) == 0 {
|
|
t.Errorf("the anchor's declaration reads as %+v", m.Declaration)
|
|
}
|
|
laptop, _ := f.Machine(snapshot.Pseudonym("machine", "laptop"))
|
|
if strings.Join(laptop.Assigned, ",") != "files,mesh-wireguard" && !strings.Contains(strings.Join(laptop.Assigned, ","), "files") {
|
|
t.Errorf("the laptop's assignments read as %v", laptop.Assigned)
|
|
}
|
|
var meshWide map[string]any
|
|
for _, s := range f.Settings {
|
|
if s.Module == "files" {
|
|
meshWide = s.Values
|
|
}
|
|
}
|
|
if meshWide["admin_password"] != snapshot.Withheld || meshWide["hub"] != anchor {
|
|
t.Errorf("the mesh-wide settings read as %v", meshWide)
|
|
}
|
|
if f.Versions.Bus != "2.11.17" || f.Versions.Store == "" {
|
|
t.Errorf("versions read as %+v", f.Versions)
|
|
}
|
|
var objects bool
|
|
for _, mod := range f.Modules {
|
|
objects = objects || mod.Name == "objects" && len(mod.Manifest) > 0
|
|
}
|
|
if !objects {
|
|
t.Error("the modules the mesh holds are not in the snapshot")
|
|
}
|
|
|
|
// And an unchanged mesh is the same content a moment later.
|
|
again, err := gatherFacts(t.Context(), open, "2.11.17")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a, _ := f.Content()
|
|
b, _ := again.Content()
|
|
if a != b {
|
|
t.Error("two snapshots of an unchanged mesh differ, so it would be written again every ten minutes")
|
|
}
|
|
}
|