Prove the vault's path of each secret verb, and today's for a module filled the old way (issue 675)
mesh/delivery-group group pkg/57-v2-vault-verbs rejected: a member's own check failed
mesh/merge-gate head 65531d737514 merged into trunk fcdecb436117 — pass: [release feat/57-config-and-provisioning, judged by its controller 39cc132d5d0f] t…
mesh/repo-check head 65531d737514 merged into trunk fcdecb436117 — fail: [release feat/57-config-and-provisioning, judged by its controller 39cc132d5d0f] i…
mesh/delivery stopped: the pull request closed unmerged
mesh/delivery-group group pkg/57-v2-vault-verbs rejected: a member's own check failed
mesh/merge-gate head 65531d737514 merged into trunk fcdecb436117 — pass: [release feat/57-config-and-provisioning, judged by its controller 39cc132d5d0f] t…
mesh/repo-check head 65531d737514 merged into trunk fcdecb436117 — fail: [release feat/57-config-and-provisioning, judged by its controller 39cc132d5d0f] i…
mesh/delivery stopped: the pull request closed unmerged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/secrets"
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// The controller's secret verbs reach the vault only for a module filled the new way (design 57's V2, issue 675,
|
||||
// the release lead's decision on it; novox/hq ADR 0349 clauses 8 to 10 and 17).
|
||||
|
||||
// standInVault answers the vault's two-step verbs as mesh-vault does: a key made for each call, the value opened
|
||||
// with it here and nowhere else. It keeps what it took and every argument it was sent, as the bus carried them.
|
||||
type standInVault struct {
|
||||
t *testing.T
|
||||
mu sync.Mutex
|
||||
keys map[string]string // call → private key
|
||||
taken map[string]string // secret → value
|
||||
sent []string // every argument, as JSON
|
||||
refuse error // refuses every first step when set
|
||||
outcome []string
|
||||
again bool // the value is held already
|
||||
}
|
||||
|
||||
func newStandInVault(t *testing.T) *standInVault {
|
||||
return &standInVault{t: t, keys: map[string]string{}, taken: map[string]string{}}
|
||||
}
|
||||
|
||||
func (v *standInVault) ask(verb string, args map[string]any) (json.RawMessage, error) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
raw, _ := json.Marshal(map[string]any{"verb": verb, "args": args})
|
||||
v.sent = append(v.sent, string(raw))
|
||||
if verb == "rotate" {
|
||||
return json.Marshal(vaultChanged{Name: "own/g14/dunst/token", Generation: 2, By: fmt.Sprint(args["by"])})
|
||||
}
|
||||
call, _ := args["call"].(string)
|
||||
if call == "" {
|
||||
if v.refuse != nil {
|
||||
return nil, v.refuse
|
||||
}
|
||||
public, private, err := secrets.Keypair()
|
||||
if err != nil {
|
||||
v.t.Fatal(err)
|
||||
}
|
||||
call = fmt.Sprintf("call-%d", len(v.keys)+1)
|
||||
v.keys[call] = private
|
||||
secret := fmt.Sprintf("%s/%v", verb, args["name"])
|
||||
return json.Marshal(vaultOpened{Call: call, SealTo: public, Secret: secret, AskedBy: "as-the-vault-recorded"})
|
||||
}
|
||||
private, ok := v.keys[call]
|
||||
if !ok {
|
||||
return nil, errors.New("no such call")
|
||||
}
|
||||
delete(v.keys, call)
|
||||
if o, _ := args["outcome"].(string); o != "" {
|
||||
v.outcome = append(v.outcome, o)
|
||||
return json.Marshal(vaultChanged{Unchanged: true})
|
||||
}
|
||||
value, err := secrets.Open(private, fmt.Sprint(args["sealed"]))
|
||||
if err != nil {
|
||||
return nil, errors.New("not sealed to this call")
|
||||
}
|
||||
// As mesh-vault takes a typed value: its line ending off (intake.go AskEnd).
|
||||
v.taken[verb] = strings.TrimRight(string(value), "\r\n")
|
||||
return json.Marshal(vaultChanged{Name: verb, Generation: 1, Unchanged: v.again})
|
||||
}
|
||||
|
||||
func (v *standInVault) said() string {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
return strings.Join(v.sent, "\n")
|
||||
}
|
||||
|
||||
// No module's secrets are filled the new way before V4: every verb keeps today's path for every module.
|
||||
func TestNoModulesSecretsAreTheVaultsBeforeV4(t *testing.T) {
|
||||
for _, module := range []string{"telegram", "dunst", "plex", "postgres", "mesh-vault", "kometa", ""} {
|
||||
if secretsOfTheVault(module) {
|
||||
t.Errorf("%q is filled the new way before V4", module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The switch: today's path for a module filled the old way, the vault's for one filled the new way, and never
|
||||
// both.
|
||||
func TestTheSwitchRunsOnePathByTheModulesForm(t *testing.T) {
|
||||
moved := func(m string) bool { return m == "dunst" }
|
||||
for module, want := range map[string]string{"telegram": "today", "dunst": "vault"} {
|
||||
var ran []string
|
||||
err := bySwitch(module, moved,
|
||||
func() error { ran = append(ran, "today"); return nil },
|
||||
func() error { ran = append(ran, "vault"); return nil })
|
||||
if err != nil || strings.Join(ran, ",") != want {
|
||||
t.Errorf("%s ran %v (%v), not %s", module, ran, err, want)
|
||||
}
|
||||
}
|
||||
var ran []string
|
||||
_ = bySwitch("dunst", nil, func() error { ran = append(ran, "today"); return nil },
|
||||
func() error { ran = append(ran, "vault"); return nil })
|
||||
if strings.Join(ran, ",") != "today" {
|
||||
t.Errorf("no switch ran %v", ran)
|
||||
}
|
||||
}
|
||||
|
||||
// `secret accept` for a module filled the new way: the value read at the terminal is sealed there to the key the
|
||||
// vault made for this one call; no clear value crosses the bus or reaches the controller's log. Over a real bus,
|
||||
// with a tap on every subject.
|
||||
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
|
||||
log.SetOutput(&logged)
|
||||
t.Cleanup(func() { log.SetOutput(nil) })
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// The desk's path for a module filled the new way: the vault records the ask and holds its bounds before the
|
||||
// prompt; the prompt names the asker as the vault recorded it; what is typed passes the controller sealed and
|
||||
// unopened; today's store is not touched. For a module filled the old way, the vault is never asked.
|
||||
func TestAnAskForAModuleFilledTheNewWayIsTheVaults(t *testing.T) {
|
||||
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
|
||||
d.askedBy = "g14/claude-code"
|
||||
d.open = func(node, module, name, desk string) (int64, error) {
|
||||
t.Fatal("today's record was asked")
|
||||
return 0, nil
|
||||
}
|
||||
vault := newStandInVault(t)
|
||||
d.vault = vault.ask
|
||||
words, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(*accepted) != 0 {
|
||||
t.Fatal("the controller opened and kept what was typed")
|
||||
}
|
||||
if vault.taken["secret-ask"] != typed {
|
||||
t.Fatalf("the vault opened %q", vault.taken["secret-ask"])
|
||||
}
|
||||
if p := (*asked)[0]; p["asked_by"] != "as-the-vault-recorded" || p["seal_to"] == "" {
|
||||
t.Errorf("the prompt was asked %v", p)
|
||||
}
|
||||
if !strings.Contains(vault.said(), `"asked_by":"g14/claude-code"`) {
|
||||
t.Errorf("the vault was not told who asked: %s", vault.said())
|
||||
}
|
||||
if len(*acts) != 1 || (*acts)[0].Cause != "given-at-the-desk" || !strings.Contains((*acts)[0].Why, "kept in the vault") {
|
||||
t.Errorf("the act: %+v", *acts)
|
||||
}
|
||||
raw, _ := json.Marshal(struct {
|
||||
Words string
|
||||
Acts []link.HandAct
|
||||
Asked []map[string]any
|
||||
}{words, *acts, *asked})
|
||||
if strings.Contains(string(raw)+vault.said(), typed) || strings.Contains(string(raw)+vault.said(), "AAEhBP0") {
|
||||
t.Fatal("the value appears in what was said, asked, recorded or sent to the vault")
|
||||
}
|
||||
|
||||
// Dismissed: the vault's record ends the ask so.
|
||||
d2, _, _, _ := aDesk(t, func(map[string]any) (json.RawMessage, error) { return json.RawMessage(`{"cancelled":true}`), nil })
|
||||
v2 := newStandInVault(t)
|
||||
d2.vault = v2.ask
|
||||
if _, err := d2.give("anchor", "telegram", "telegram-token", "laptop"); !errors.Is(err, errNothingGiven) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
if strings.Join(v2.outcome, ",") != "dismissed" {
|
||||
t.Errorf("the vault was told %v", v2.outcome)
|
||||
}
|
||||
// The vault's bounds refuse: nobody is asked to type.
|
||||
d3, _, _, asked3 := aDesk(t, sealedTo(t, typed))
|
||||
v3 := newStandInVault(t)
|
||||
v3.refuse = errors.New("an ask for telegram-token of telegram on anchor is still open")
|
||||
d3.vault = v3.ask
|
||||
if _, err := d3.give("anchor", "telegram", "telegram-token", "laptop"); err == nil ||
|
||||
!strings.Contains(err.Error(), "nobody was asked to type anything") || len(*asked3) != 0 {
|
||||
t.Errorf("a refused ask: %v, prompts %d", err, len(*asked3))
|
||||
}
|
||||
|
||||
// Filled the old way: today's path, the vault never asked.
|
||||
d4, accepted4, _, _ := aDesk(t, sealedTo(t, typed))
|
||||
if _, err := d4.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(*accepted4) != 1 || d4.vault != nil {
|
||||
t.Error("a module filled the old way did not take today's path")
|
||||
}
|
||||
}
|
||||
|
||||
// `secret issued` for a provider filled the new way: the provider's tool seals to the vault's key for the call,
|
||||
// the controller passes it on unopened, and nothing is kept, sealed or sent here. A provider filled the old way
|
||||
// keeps today's path and the vault is never asked.
|
||||
func TestAnIssuedValueForAProviderFilledTheNewWayGoesToTheVault(t *testing.T) {
|
||||
var r issuedRecorder
|
||||
vault := newStandInVault(t)
|
||||
taker := r.taker(t, issuedSealed(t, "the-servers-own-token"))
|
||||
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 = func(m string) bool { return m == "plex" }
|
||||
taker.vault = vault.ask
|
||||
words, err := taker.take("plex-api")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if vault.taken["issued"] != "the-servers-own-token" {
|
||||
t.Fatalf("the vault opened %q", vault.taken["issued"])
|
||||
}
|
||||
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)
|
||||
}
|
||||
if !strings.Contains(vault.said(), `"provider":"ace"`) || !strings.Contains(vault.said(), `"module":"plex"`) {
|
||||
t.Errorf("the vault was told %s", vault.said())
|
||||
}
|
||||
for _, said := range []string{words, r.acts[0].Why, vault.said()} {
|
||||
if strings.Contains(said, "the-servers-own-token") {
|
||||
t.Fatalf("the value was said: %q", said)
|
||||
}
|
||||
}
|
||||
// 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 = 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)
|
||||
}
|
||||
|
||||
// Filled the old way: today's path.
|
||||
var r3 issuedRecorder
|
||||
t3 := r3.taker(t, issuedSealed(t, "the-servers-own-token"))
|
||||
t3.ofTheVault = 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))
|
||||
}
|
||||
}
|
||||
|
||||
// `secret rotate` for a module filled the new way asks the vault, naming who asked; nothing is sent.
|
||||
func TestARotationForAModuleFilledTheNewWayIsTheVaults(t *testing.T) {
|
||||
vault := newStandInVault(t)
|
||||
words, err := rotateInVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), "g14/claude-code")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(vault.said(), `"verb":"rotate"`) || !strings.Contains(vault.said(), `"by":"g14/claude-code"`) ||
|
||||
!strings.Contains(vault.said(), `"module":"dunst"`) {
|
||||
t.Errorf("the vault was told %s", vault.said())
|
||||
}
|
||||
if !strings.Contains(words, "generation 2") || !strings.Contains(words, "takes it from the vault") {
|
||||
t.Errorf("%q", words)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user