230 lines
8.2 KiB
Go
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)
|
|
}
|
|
}
|