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") } }