Files
mesh-controller/cmd/mesh-controller/vault_switch_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

414 lines
16 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/testbus"
)
// Each of the four secret commands honours the switch at its own call site (the review of issue 675): run as the
// terminal runs them, against the real store and a real bus with a stand-in vault serving the vault's seat, a
// module filled the old way takes today's path and the vault is never asked, and one filled the new way reaches
// the vault. Sending every module to the vault at any one call site — or none — fails one of these. Needs the
// store (MESH_TEST_POSTGRES), and is named in testdata/store-tests.
// vaultOnTheBus serves the vault's four intake verbs on the test's bus with a stand-in vault, and says which verbs
// were asked, in order.
type vaultOnTheBus struct {
vault *standInVault
mu sync.Mutex
asked []string
}
func (v *vaultOnTheBus) verbs() string {
v.mu.Lock()
defer v.mu.Unlock()
return strings.Join(v.asked, ",")
}
func (v *vaultOnTheBus) reset() {
v.mu.Lock()
defer v.mu.Unlock()
v.asked = nil
}
// aMeshWithAVault is aMesh with the controller's bus pointed at a test bus where a stand-in vault serves its seat,
// and the switch saying that only the modules named are filled the new way.
func aMeshWithAVault(t *testing.T) (*stores, *vaultOnTheBus, func(moved ...string)) {
t.Helper()
open := aMesh(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 })
served := &vaultOnTheBus{vault: newStandInVault(t)}
for _, verb := range []string{"accept", "secret-ask", "issued", "rotate"} {
sub, err := conn.Subscribe(broker.CalledPattern(link.SeatToolSubject("mesh-vault", verb)), func(m *nats.Msg) {
served.mu.Lock()
served.asked = append(served.asked, verb)
served.mu.Unlock()
var args map[string]any
_ = json.Unmarshal(m.Data, &args)
result, err := served.vault.ask(verb, 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)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
}
if err := conn.Flush(); err != nil {
t.Fatal(err)
}
switchWas := filledTheNewWay
t.Cleanup(func() { filledTheNewWay = switchWas })
moveOnly := func(moved ...string) {
filledTheNewWay = switchOf(func(module string) bool {
for _, m := range moved {
if m == module {
return true
}
}
return false
})
}
return open, served, moveOnly
}
// aValueFile is a file holding a value, for `--from`.
func aValueFile(t *testing.T, value string) string {
t.Helper()
file := filepath.Join(t.TempDir(), "value")
if err := os.WriteFile(file, []byte(value+"\n"), 0o600); err != nil {
t.Fatal(err)
}
return file
}
// `secret accept` of a module's own secret.
func TestAcceptOfAnOwnSecretTakesThePathOfItsModulesForm(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
t.Fatal(err)
}
accept := []string{"accept", "anchor", "dunst", "token", "--from", aValueFile(t, "the-bots-token")}
moveOnly("some-other-module")
if err := secretCommand(ctx, accept); err != nil {
t.Fatalf("today's path: %v", err)
}
if asked := vault.verbs(); asked != "" {
t.Fatalf("the vault was asked %s for a module filled the old way", asked)
}
if origin, _, err := open.inventory.OwnSecretOrigin(ctx, "anchor", "dunst", "token"); err != nil || origin == "" {
t.Fatalf("today's store does not hold it: %q %v", origin, err)
}
moveOnly("dunst")
if err := secretCommand(ctx, accept); err != nil {
t.Fatalf("the vault's path: %v", err)
}
if asked := vault.verbs(); asked != "accept,accept" {
t.Fatalf("the vault was asked %q for a module filled the new way", asked)
}
if vault.vault.taken["accept"] != "the-bots-token" {
t.Fatalf("the vault opened %q", vault.vault.taken["accept"])
}
}
// `secret accept --provider`: the provider's module decides, not the consumer's.
func TestAcceptForAProviderTakesThePathOfTheProvidersModule(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh}}})
register(t, open, catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": "/var/lib/mesh/requests/tv-api.secret"}})
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
accept := []string{"accept", "laptop", "requests", "tv-api", "--provider", "anchor", "--from",
aValueFile(t, "the-pairs-key")}
// The consumer filled the new way, the provider the old: the provider's module decides, today's path.
moveOnly("requests")
if err := secretCommand(ctx, accept); err != nil {
t.Fatalf("today's path: %v", err)
}
if asked := vault.verbs(); asked != "" {
t.Fatalf("the vault was asked %s for a provider filled the old way", asked)
}
moveOnly("tv")
if err := secretCommand(ctx, accept); err != nil {
t.Fatalf("the vault's path: %v", err)
}
if asked := vault.verbs(); asked != "accept,accept" {
t.Fatalf("the vault was asked %q for a provider filled the new way", asked)
}
if said := vault.vault.said(); !strings.Contains(said, `"provider":"anchor"`) || !strings.Contains(said, `"provision":"tv-api"`) {
t.Errorf("the vault was told %s", said)
}
}
// `secret accept` of a provision's credential issued outside the mesh, given once for its provider.
func TestAcceptOfAnOutsideOfferTakesThePathOfTheProvidersModule(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}})
register(t, open, catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": "/var/lib/mesh/requests/tv-api.secret"}})
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
accept := []string{"accept", "anchor", "tv", "tv-api", "--from", aValueFile(t, "the-apps-key")}
moveOnly("requests")
// Today's path seals it to every consumer; whether it then sends is not this test's.
_ = secretCommand(ctx, accept)
if asked := vault.verbs(); asked != "" {
t.Fatalf("the vault was asked %s for a provider filled the old way", asked)
}
moveOnly("tv")
if err := secretCommand(ctx, accept); err != nil {
t.Fatalf("the vault's path: %v", err)
}
if asked := vault.verbs(); asked != "accept,accept" {
t.Fatalf("the vault was asked %q for a provider filled the new way", asked)
}
if vault.vault.taken["accept"] != "the-apps-key" {
t.Fatalf("the vault opened %q", vault.vault.taken["accept"])
}
}
// `secret ask` (and `secret accept --at-desk`): the desk is never answered on this bus, so today's path ends at the
// prompt; the vault's path is one ask of the vault, which asks the desk itself (issue 723), so it is done without
// the controller reaching the desk at all.
func TestAnAskAtTheDeskTakesThePathOfItsModulesForm(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
t.Fatal(err)
}
for _, line := range [][]string{
{"ask", "anchor", "dunst", "token", "--at", "laptop"},
{"accept", "anchor", "dunst", "token", "--at-desk", "laptop"},
} {
vault.reset()
moveOnly("some-other-module")
err := secretCommand(ctx, line)
if err == nil || !strings.Contains(err.Error(), "could not be asked") {
t.Fatalf("%v: today's path did not reach the desk: %v", line, err)
}
if asked := vault.verbs(); asked != "" {
t.Fatalf("%v: the vault was asked %s for a module filled the old way", line, asked)
}
vault.reset()
moveOnly("dunst")
if err := secretCommand(ctx, line); err != nil {
t.Fatalf("%v: the vault's path reached the desk itself, or failed: %v", line, err)
}
// One ask of the vault, which asks the desk in its own name: the controller carries nothing to it.
if asked := vault.verbs(); asked != "secret-ask" {
t.Fatalf("%v: the vault was asked %q for a module filled the new way", line, asked)
}
}
}
// `secret issued`: the provider's tool is never answered on this bus, so today's path ends at it; the vault's path
// is one ask of the vault, which asks the tool itself (issue 723).
func TestAnIssuedValueTakesThePathOfItsProvidersModule(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "plex", Version: "1", Tools: []string{"plex_token"},
Provides: []catalogue.Offer{{Name: "plex-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "plex_token"}}}})
register(t, open, catalogue.Manifest{Module: "kometa", Version: "1", Requires: []string{"plex-api"},
Secrets: map[string]string{"plex-api": "/var/lib/mesh/kometa/plex-api.secret"}})
for _, a := range [][2]string{{"anchor", "plex"}, {"laptop", "kometa"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
// The consumer's send makes the pair, so the provision has a holder to take it for.
plan, settings, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatal(err)
}
if _, err := declarationFor(ctx, open, "laptop", plan, settings); err != nil {
t.Fatal(err)
}
moveOnly("kometa")
err = secretCommand(ctx, []string{"issued", "plex-api"})
if err == nil || !strings.Contains(err.Error(), "could not be asked") {
t.Fatalf("today's path did not reach the provider's tool: %v", err)
}
if asked := vault.verbs(); asked != "" {
t.Fatalf("the vault was asked %s for a provider filled the old way", asked)
}
moveOnly("plex")
if err := secretCommand(ctx, []string{"issued", "plex-api"}); err != nil {
t.Fatalf("the vault's path reached the provider's tool itself, or failed: %v", err)
}
if asked := vault.verbs(); asked != "issued" {
t.Fatalf("the vault was asked %q for a provider filled the new way", asked)
}
}
// `secret rotate` of a module's own secret.
func TestARotationTakesThePathOfItsModulesForm(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
t.Fatal(err)
}
moveOnly()
if err := secretCommand(ctx, []string{"accept", "anchor", "dunst", "token", "--from", aValueFile(t, "tok")}); err != nil {
t.Fatal(err)
}
rotate := []string{"rotate", "anchor", "dunst", "token", "--why", "the test rotates it"}
moveOnly("some-other-module")
_ = secretCommand(ctx, rotate) // today's path: rotated in the store, or refused there
if asked := vault.verbs(); asked != "" {
t.Fatalf("the vault was asked %s for a module filled the old way", asked)
}
moveOnly("dunst")
if err := secretCommand(ctx, []string{"rotate", "anchor", "dunst", "token"}); err == nil ||
!strings.Contains(err.Error(), "--why") || vault.verbs() != "" {
t.Fatalf("a rotation in the vault without a why: %v, the vault asked %q", err, vault.verbs())
}
if err := secretCommand(ctx, rotate); err != nil {
t.Fatalf("the vault's path: %v", err)
}
if asked := vault.verbs(); asked != "rotate" {
t.Fatalf("the vault was asked %q for a module filled the new way", asked)
}
if said := vault.vault.said(); !strings.Contains(said, `"why":"the test rotates it"`) {
t.Errorf("the vault was not told why: %s", said)
}
}
// A moved module that runs as an account of its own is announced on the operator's channels before the vault is
// asked, and with the announcement failing the vault is never called (issue 836, the N1-give rule).
func TestAcceptForAMovedTrustedModuleAnnouncesBeforeTheVaultIsAsked(t *testing.T) {
open, vault, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "telegram", Version: "1", RunsAs: "telegram", SecretsOwner: "telegram",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/telegram/token"}}})
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
for _, m := range []string{"telegram", "dunst"} {
if _, err := assign(ctx, open, "anchor", m); err != nil {
t.Fatal(err)
}
}
moveOnly("telegram", "dunst")
was := announceTheGiven
t.Cleanup(func() { announceTheGiven = was })
var order []string
announceTheGiven = func(_ context.Context, node, module, name, how, askedBy string) error {
order = append(order, "announce:"+module+":"+vault.verbs())
return nil
}
accept := func(module string) error {
return secretCommand(ctx, []string{"accept", "anchor", module, "token", "--from", aValueFile(t, "the-bots-token")})
}
if err := accept("telegram"); err != nil {
t.Fatal(err)
}
if strings.Join(order, ",") != "announce:telegram:" || vault.verbs() != "accept,accept" {
t.Fatalf("announced %v, the vault was asked %q", order, vault.verbs())
}
vault.reset()
announceTheGiven = func(context.Context, string, string, string, string, string) error {
return errors.New("no channel took it")
}
err := accept("telegram")
if err == nil || !strings.Contains(err.Error(), "nothing was sealed") || vault.verbs() != "" {
t.Fatalf("a failed announcement: %v, the vault was asked %q", err, vault.verbs())
}
// Any other module is kept first and announced after; a failed announcement is said, not undone.
if err := accept("dunst"); err != nil || vault.verbs() != "accept,accept" {
t.Fatalf("an untrusted module: %v, asked %q", err, vault.verbs())
}
}
// issue 851 (hand-act): a moved module's terminal give is recorded in the hand-act log like acceptKept's
// (novox/hq ADR 0380 §6), naming where, and never carrying the value.
func TestAcceptForAMovedModuleRecordsAHandActAndNeverTheValue(t *testing.T) {
open, _, moveOnly := aMeshWithAVault(t)
ctx := t.Context()
api, err := jetstream.New(handActConn)
if err != nil {
t.Fatal(err)
}
if _, err := api.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: broker.HandActsBucket}); err != nil {
t.Fatal(err)
}
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
t.Fatal(err)
}
moveOnly("dunst")
was := announceTheGiven
t.Cleanup(func() { announceTheGiven = was })
announceTheGiven = func(context.Context, string, string, string, string, string) error { return nil }
if err := secretCommand(ctx, []string{"accept", "anchor", "dunst", "token", "--from", aValueFile(t, "the-bots-token")}); err != nil {
t.Fatal(err)
}
acts, err := link.HandActs(ctx, handActConn, time.Now().Add(-time.Minute))
if err != nil || len(acts) != 1 || acts[0].Verb != "secret accept" || acts[0].Cause != "given-at-the-terminal" ||
!strings.Contains(acts[0].Why, "at the controller's terminal") {
t.Fatalf("recorded %+v (%v)", acts, err)
}
if strings.Contains(acts[0].Why, "the-bots-token") {
t.Error("the act carries the value")
}
}