Files
mesh-controller/cmd/mesh-controller/vault_secrets_integration_test.go
T

230 lines
8.2 KiB
Go

//go:build integration
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"strings"
"sync"
"testing"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/testbus"
)
func TestAModulesSecretsAreTheVaultsOnceItIsMarkedMoved(t *testing.T) {
open := aMesh(t)
register(t, open, aBot("moved-bot", true))
register(t, open, aBot("kept-bot", false))
for module, want := range map[string]bool{"moved-bot": true, "kept-bot": false, "nowhere": false} {
got, err := secretsOfTheVault(module)
if err != nil || got != want {
t.Errorf("%s: the vault's %v (%v), want %v", module, got, err, want)
}
}
}
func TestAnAcceptedValueCrossesTheBusOnlySealedToTheVault(t *testing.T) {
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(conn.Close)
before := handActConn
handActConn = conn
t.Cleanup(func() { handActConn = before })
var tapped bytes.Buffer
var tapMu sync.Mutex
tap, err := conn.Subscribe(">", func(m *nats.Msg) {
tapMu.Lock()
defer tapMu.Unlock()
tapped.WriteString(m.Subject + " " + string(m.Data) + "\n")
})
if err != nil {
t.Fatal(err)
}
defer tap.Unsubscribe()
vault := newStandInVault(t)
var subjects []string
holder, err := conn.Subscribe(broker.CalledPattern(link.SeatToolSubject("mesh-vault", "accept")), func(m *nats.Msg) {
subjects = append(subjects, m.Subject)
var args map[string]any
_ = json.Unmarshal(m.Data, &args)
result, err := vault.ask("accept", args)
answer := link.Answer{Result: result}
if err != nil {
answer = link.Answer{Error: err.Error()}
}
raw, _ := json.Marshal(answer)
_ = m.Respond(raw)
})
if err != nil {
t.Fatal(err)
}
defer holder.Unsubscribe()
var logged bytes.Buffer
was := log.Writer()
log.SetOutput(&logged)
t.Cleanup(func() { log.SetOutput(was) })
const value = "pw-of-last-year-that-must-not-show"
words, err := acceptIntoVault(theVault(context.Background()), provisionArgs("novox", "postgres"), value, "jochen")
if err != nil {
t.Fatal(err)
}
if vault.taken["accept"] != value {
t.Fatalf("the vault opened %q", vault.taken["accept"])
}
if err := conn.Flush(); err != nil {
t.Fatal(err)
}
tapMu.Lock()
crossed := tapped.String()
tapMu.Unlock()
if !strings.Contains(crossed, "mesh.seat.mesh-vault.call.accept.controller") {
t.Fatalf("the tap heard no call to the vault: %s", crossed)
}
for _, s := range subjects {
if !strings.HasSuffix(s, ".controller") {
t.Errorf("the vault was asked on %s, not in the controller's name", s)
}
}
for where, text := range map[string]string{"the bus": crossed, "the log": logged.String(), "the answer": words} {
if strings.Contains(text, value) || strings.Contains(text, "must-not-show") {
t.Errorf("the value crossed %s in the clear", where)
}
}
if !strings.Contains(words, "sealed at this terminal") {
t.Errorf("the answer: %q", words)
}
// A vault that refuses the first step: nothing is sealed or sent.
vault.refuse = errors.New("the vault has no operator key")
if _, err := acceptIntoVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), value, "jochen"); err == nil ||
!strings.Contains(err.Error(), "nothing was read or sealed") {
t.Errorf("a refused call said %v", err)
}
}
func TestAnIssuedValueForAProviderFilledTheNewWayGoesToTheVault(t *testing.T) {
var r issuedRecorder
vault := newStandInVault(t)
taker := r.taker(t, func(issuedProvider, map[string]any) (link.Answer, error) {
t.Fatal("the controller asked the provider's tool itself")
return link.Answer{}, nil
})
taker.keep = func(string, string, string) (inventory.IssuedKept, error) {
t.Fatal("today's store was asked to keep it")
return inventory.IssuedKept{}, nil
}
taker.ofTheVault = switchOf(func(m string) bool { return m == "plex" })
taker.vault = vault.ask
words, err := taker.take("plex-api")
if err != nil {
t.Fatal(err)
}
if len(r.sent) != 0 {
t.Errorf("machines were sent: %v", r.sent)
}
if len(r.acts) != 1 || r.acts[0].Verb != handActIssued || !strings.Contains(r.acts[0].Why, "kept in the vault") {
t.Errorf("the act: %+v", r.acts)
}
said := vault.said()
for _, want := range []string{`"verb":"issued"`, `"provider":"ace"`, `"module":"plex"`, `"tool":"plex_issued_credential"`} {
if !strings.Contains(said, want) {
t.Errorf("the vault was told %s, without %s", said, want)
}
}
if strings.Contains(said, "seal_to") || strings.Contains(said, "sealed") || strings.Contains(said, `"call"`) {
t.Errorf("the controller carried a key or a sealed value: %s", said)
}
if !strings.Contains(words, "provision/ace/plex-api") {
t.Errorf("said %q", words)
}
// The same value again: the vault writes nothing, and nothing is recorded.
again := newStandInVault(t)
again.again = true
r2 := issuedRecorder{}
t2 := r2.taker(t, issuedSealed(t, "the-servers-own-token"))
t2.ofTheVault, t2.vault = switchOf(func(string) bool { return true }), again.ask
if words, err := t2.take("plex-api"); err != nil || len(r2.acts) != 0 || !strings.Contains(words, "nothing was written") {
t.Errorf("%q %v %+v", words, err, r2.acts)
}
// The vault refuses (its tool refused it): said, nothing recorded.
refused := newStandInVault(t)
refused.refuse = errors.New("its tool plex_issued_credential refused: this tool answers the vault alone")
r4 := issuedRecorder{}
t4 := r4.taker(t, issuedSealed(t, "x"))
t4.ofTheVault, t4.vault = switchOf(func(string) bool { return true }), refused.ask
if _, err := t4.take("plex-api"); err == nil || !strings.Contains(err.Error(), "answers the vault alone") || len(r4.acts) != 0 {
t.Errorf("a refusal: %v, acts %d", err, len(r4.acts))
}
// Filled the old way: today's path.
var r3 issuedRecorder
t3 := r3.taker(t, issuedSealed(t, "the-servers-own-token"))
t3.ofTheVault = switchOf(func(string) bool { return false })
t3.vault = func(string, map[string]any) (json.RawMessage, error) {
t.Fatal("the vault was asked for a provider filled the old way")
return nil, nil
}
if _, err := t3.take("plex-api"); err != nil || len(r3.kept) != 1 {
t.Errorf("today's path: %v, kept %d", err, len(r3.kept))
}
}
func TestAnIssuedValuePerHolderNamesTheHoldersToTheVault(t *testing.T) {
var r issuedRecorder
vault := newStandInVault(t)
vault.perHolder = true
taker := r.taker(t, func(issuedProvider, map[string]any) (link.Answer, error) {
t.Fatal("the controller asked the provider's tool itself")
return link.Answer{}, nil
})
taker.providers = func(string) ([]issuedProvider, bool, error) {
return []issuedProvider{{Node: "novox", Module: "gitea", Tool: "gitea_agent_token", Holders: []issuedHolder{
{Node: "anchor", Module: "claude-code"}, {Node: "laptop", Module: "claude-code"},
{Node: "shanks", Module: "claude-code"}}}}, true, nil
}
taker.ofTheVault = switchOf(func(m string) bool { return m == "gitea" })
taker.vault = vault.ask
words, err := taker.take("gitea-read")
if err != nil {
t.Fatal(err)
}
said := vault.said()
if !strings.Contains(said, `"holders":[{"node":"anchor","module":"claude-code"},{"node":"laptop","module":"claude-code"},{"node":"shanks","module":"claude-code"}]`) {
t.Errorf("the vault was told %s", said)
}
for _, want := range []string{"provision/novox/gitea-read/anchor/claude-code", "provision/novox/gitea-read/laptop/claude-code",
"gave nothing for claude-code on shanks"} {
if !strings.Contains(words, want) {
t.Errorf("said %q, without %q", words, want)
}
}
if len(r.acts) != 1 || !strings.Contains(r.acts[0].Why, "claude-code on anchor") || !strings.Contains(r.acts[0].Why, "claude-code on laptop") {
t.Errorf("the act: %+v", r.acts)
}
if len(r.sent) != 0 || len(r.kept) != 0 {
t.Error("the controller kept or sent something")
}
// The same values again: nothing written, nothing recorded.
again := newStandInVault(t)
again.perHolder, again.again = true, true
r2 := issuedRecorder{}
t2 := r2.taker(t, nil)
t2.providers, t2.ofTheVault, t2.vault = taker.providers, taker.ofTheVault, again.ask
if words, err := t2.take("gitea-read"); err != nil || len(r2.acts) != 0 || !strings.Contains(words, "nothing was written") {
t.Errorf("again: %q %v %+v", words, err, r2.acts)
}
}