Files
mesh-controller/internal/inventory/operator_test.go
T
jschoubben 9f3790dcda Review: one local name is still a local name; a local name is unique; recovery knows it; recipes read as instructions; a tag before a digest; ask fails at once when nothing serves
A secrets object with one local name delivered no file. Two requirements could share
a local name. secret recover and the export could not tell two locals apart. The
recipe check missed continued lines and read heredoc bodies as bases. repo:tag@digest
kept the tag in the repository. ask now publishes mandatory, so a tool nothing serves
is said at once rather than after the wait.
2026-09-21 21:03:22 +02:00

220 lines
7.4 KiB
Go

package inventory
import (
"context"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/secrets"
)
// With an operator key, a module's own secret is sealed to the operator as well — and the operator
// opens exactly the value the node was given.
func TestAnOwnSecretIsSealedToTheOperatorToo(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "postgres", Version: "1"}, Source{}); err != nil {
t.Fatal(err)
}
// Before there is a key: minted, and honestly unrecoverable.
if _, err := inv.SecretForModule(ctx, "consumer", "postgres", "superuser"); err != nil {
t.Fatal(err)
}
if _, err := inv.KeptSecret(ctx, "consumer", "postgres", "superuser", "", ""); err == nil {
t.Fatal("a secret made before the operator key was reported recoverable")
}
kept, _, unrecoverable, err := inv.KeptForOperator(ctx)
if err != nil {
t.Fatal(err)
}
if len(kept) != 0 || len(unrecoverable) != 1 {
t.Fatalf("before a key: %d kept, %d unrecoverable", len(kept), len(unrecoverable))
}
pub, priv, err := secrets.Keypair()
if err != nil {
t.Fatal(err)
}
if orphaned, err := inv.SetOperatorKey(ctx, pub); err != nil || orphaned != 0 {
t.Fatalf("set: orphaned %d, %v", orphaned, err)
}
// A new secret is sealed to both; an accepted one too.
if _, err := inv.SecretForModule(ctx, "provider", "postgres", "superuser"); err != nil {
t.Fatal(err)
}
if err := inv.AcceptSecretForModule(ctx, "provider", "postgres", "replication", "given-by-a-person"); err != nil {
t.Fatal(err)
}
kept, _, unrecoverable, err = inv.KeptForOperator(ctx)
if err != nil {
t.Fatal(err)
}
if len(kept) != 2 || len(unrecoverable) != 1 {
t.Fatalf("after a key: %d kept, %d unrecoverable", len(kept), len(unrecoverable))
}
got, err := inv.KeptSecret(ctx, "provider", "postgres", "replication", "", "")
if err != nil {
t.Fatal(err)
}
value, err := secrets.Open(priv, got.Sealed)
if err != nil {
t.Fatal(err)
}
if string(value) != "given-by-a-person" {
t.Fatalf("recovered %q", value)
}
if got.Origin != "accepted" || got.Key != pub {
t.Fatalf("kept as %+v", got)
}
minted, err := inv.KeptSecret(ctx, "provider", "postgres", "superuser", "", "")
if err != nil {
t.Fatal(err)
}
if v, err := secrets.Open(priv, minted.Sealed); err != nil || len(v) != 40 {
t.Fatalf("the minted secret did not open to a 40-character value: %v", err)
}
// The old secret is kept, not resealed: asking again is a read, the plaintext is gone, and it
// stays honestly unrecoverable.
if _, err := inv.SecretForModule(ctx, "consumer", "postgres", "superuser"); err != nil {
t.Fatal(err)
}
if _, err := inv.KeptSecret(ctx, "consumer", "postgres", "superuser", "", ""); err == nil {
t.Fatal("asking again did not remake, yet it became recoverable")
}
// Until the node rejoins with a new sealing key: then the secret is remade, and the remake is
// sealed to the operator — the one scenario the vault exists for.
rejoined, err := inv.NodeByName(ctx, "consumer")
if err != nil {
t.Fatal(err)
}
newKey, _ := aSealingKey(t)
if err := inv.RecordSealingKey(ctx, rejoined.ID, newKey); err != nil {
t.Fatal(err)
}
if _, err := inv.SecretForModule(ctx, "consumer", "postgres", "superuser"); err != nil {
t.Fatal(err)
}
remade, err := inv.KeptSecret(ctx, "consumer", "postgres", "superuser", "", "")
if err != nil {
t.Fatalf("the remade secret is not recoverable: %v", err)
}
if _, err := secrets.Open(priv, remade.Sealed); err != nil {
t.Fatal(err)
}
// Replacing the key says how many secrets stay sealed to the old one — and those move out of
// the recoverable list, whatever the export is labelled with.
pub2, _, _ := secrets.Keypair()
orphaned, err := inv.SetOperatorKey(ctx, pub2)
if err != nil {
t.Fatal(err)
}
if orphaned != 3 {
t.Fatalf("replacing the key orphaned %d, and three were sealed to it", orphaned)
}
if now, _ := inv.OperatorKey(ctx); now != pub2 {
t.Fatal("the new key is not the mesh's key")
}
kept, earlier, _, err := inv.KeptForOperator(ctx)
if err != nil {
t.Fatal(err)
}
if len(kept) != 0 || len(earlier) != 3 {
t.Fatalf("after replacing the key: %d recoverable with it, %d sealed to the earlier key", len(kept), len(earlier))
}
doc, err := inv.OperatorExport(ctx)
if err != nil || doc == nil || len(doc.EarlierKey) != 3 || len(doc.Kept) != 0 {
t.Fatalf("the export does not say what the current key cannot open: %+v %v", doc, err)
}
}
// A pair credential — what the vault provides a module — is sealed to the operator too, and the
// operator's copy is the very value the consumer's node unseals.
func TestAPairCredentialIsSealedToTheOperatorToo(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
// Two nodes with keys, keeping the consumer's opener: the test made the key, so it can play the
// consumer's host for one assertion.
var openAsConsumer func(string) ([]byte, bool)
for _, n := range []string{"consumer", "provider"} {
node, err := inv.AddNode(ctx, n)
if err != nil {
t.Fatal(err)
}
key, open := aSealingKey(t)
if n == "consumer" {
openAsConsumer = open
}
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
t.Fatal(err)
}
}
for _, m := range []string{"gitea", "mesh-vault"} {
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"}, Source{}); err != nil {
t.Fatal(err)
}
}
pub, priv, err := secrets.Keypair()
if err != nil {
t.Fatal(err)
}
if _, err := inv.SetOperatorKey(ctx, pub); err != nil {
t.Fatal(err)
}
made, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
kept, err := inv.KeptSecret(ctx, "consumer", "gitea", "secret", "", "")
if err != nil {
t.Fatal(err)
}
if kept.Kind != "pair" || kept.Provider != "provider" {
t.Fatalf("kept as %+v", kept)
}
all, _, _, err := inv.KeptForOperator(ctx)
if err != nil {
t.Fatal(err)
}
var pairs int
for _, k := range all {
if k.Kind == "pair" {
pairs++
}
}
if pairs != 1 {
t.Fatalf("%d pair credential(s) recoverable, expected 1", pairs)
}
// A second provider of the same provision: two rows, refused rather than the first one taken,
// unless the provider is named. And replacing the key counts pair credentials as orphaned.
if _, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "consumer", ""); err != nil {
t.Fatal(err)
}
if _, err := inv.KeptSecret(ctx, "consumer", "gitea", "secret", "", ""); err == nil || !strings.Contains(err.Error(), "--provider") {
t.Fatalf("two providers were not refused: %v", err)
}
if byName, err := inv.KeptSecret(ctx, "consumer", "gitea", "secret", "provider", ""); err != nil || byName.Provider != "provider" {
t.Fatalf("naming the provider did not select it: %+v %v", byName, err)
}
pub2, _, _ := secrets.Keypair()
if orphaned, err := inv.SetOperatorKey(ctx, pub2); err != nil || orphaned != 2 {
t.Fatalf("replacing the key orphaned %d pair credential(s), and two were sealed to it (%v)", orphaned, err)
}
fromOperator, err := secrets.Open(priv, kept.Sealed)
if err != nil {
t.Fatal(err)
}
// The consumer's blob is sealed to the consumer node. Same value, two recipients.
fromNode, ok := openAsConsumer(made.ForConsumer)
if !ok {
t.Fatal("the consumer cannot open its own blob")
}
if string(fromOperator) != string(fromNode) {
t.Fatal("the operator's copy of the pair credential differs from the consumer's")
}
}