The secret the vault provides a module is the credential of the consumer↔vault pair, and so is every credential a provider grants; sealing only own secrets to the operator left exactly those unrecoverable. Same column, same call; the export and `secret recover` address a pair by consumer node, module and the provision's name, and say which kind each entry is.
170 lines
5.1 KiB
Go
170 lines
5.1 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"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, remade for a rejoined node, becomes recoverable — it was issued again.
|
|
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")
|
|
}
|
|
|
|
// Replacing the key says how many secrets stay sealed to the old one.
|
|
pub2, _, _ := secrets.Keypair()
|
|
orphaned, err := inv.SetOperatorKey(ctx, pub2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if orphaned != 2 {
|
|
t.Fatalf("replacing the key orphaned %d, and two were sealed to it", orphaned)
|
|
}
|
|
if now, _ := inv.OperatorKey(ctx); now != pub2 {
|
|
t.Fatal("the new key is not the mesh's key")
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
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")
|
|
}
|
|
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) in the export, expected 1", pairs)
|
|
}
|
|
}
|