Files
mesh-controller/internal/catalogue/handed_secrets_test.go
T

253 lines
12 KiB
Go

package catalogue
import (
"reflect"
"slices"
"strings"
"testing"
)
// A moved module's own secrets come from the vault (novox/hq ADR 0349 clauses 7 and 11, design 57 V4, issue 647):
// where the module is handed, its declaration carries no sealed value — each file holding a secret alone is placed
// without one and filled by the client, and the plan names every secret its templates and those files need, by
// name. Where it is not handed, and for every module not moved, the composition is byte for byte what it was.
const (
handedBot = `{"module":"bot","version":"1",
"own-secrets":{"token":"${dir:state}/token"},
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[
{"id":"state","type":"directory","mode":"0700","place":"."},
{"id":"conf","type":"file","path":"/etc/bot/bot.conf","mode":"0640","template":"files/bot.conf"},
{"id":"daemon","type":"process","name":"bot","run":["bot"],"env":{"BOT_TOKEN_FILE":"${dir:state}/token"}}]}`
botText = "token = ${secret:token}\nhome = ${machine:account-home}\n"
)
func botMoved() string {
return strings.Replace(handedBot, `"version":"1",`, `"version":"1","moved":true,`, 1)
}
func builtBot(text string) []Built {
return []Built{{Name: "files", Kind: ArtifactArchive,
Reference: ArtifactStoreScheme + "bot/files/blobs/sha256:" + strings.Repeat("e", 64),
Digest: "sha256:" + strings.Repeat("e", 64), Texts: map[string]string{"bot.conf": text}}}
}
// What the controller seals for the bot when it fills it the old way: a stand-in sealed value.
func sealedForBot(with Rendering) Rendering {
with.Needed = map[string]map[string]string{"bot": {"token": "sealed-to-the-node"}}
return with
}
func TestAHandedModulesSecretsAreNamedAndNoValueIsSealed(t *testing.T) {
r := notifierOn(t, held(t, botMoved(), builtBot(botText)))
c, got := composeWith(t, r, handingTo("store:5000"))
if strings.Contains(got, "sealed") || strings.Contains(got, "${secret:") {
t.Fatalf("a handed module's declaration carries a sealed value or a secret placeholder: %s", got)
}
var token map[string]any
for _, res := range c.Resources {
if res["id"] == "bot.needs-token" {
token = res
}
}
if token == nil {
t.Fatalf("the token's file is not declared: %s", got)
}
if token["path"] == nil || strings.Contains(token["path"].(string), "${") || token["mode"] != "0600" ||
token["owner-at-runtime"] != "owned" || token[HandedSecretField] != nil {
t.Fatalf("the token's file is declared %v: placed, 0600, owned at runtime, no value", token)
}
path := token["path"].(string)
if !reflect.DeepEqual(c.Filled, []string{path, "/etc/bot/bot.conf"}) {
t.Fatalf("the filled files are %v", c.Filled)
}
if want := []PlanSecret{{Name: "token", Path: path}}; !reflect.DeepEqual(c.Plans["bot"].Secrets, want) {
t.Fatalf("the plan names secrets %+v, want %+v", c.Plans["bot"].Secrets, want)
}
// A process naming the token's file still restarts when its client writes it.
for _, res := range c.Resources {
if res["id"] == "bot.daemon" && !strings.Contains(strings.Join(stringsIn(res["restart-on"]), " "), "bot.needs-token") {
t.Errorf("the daemon does not restart on its token's file: %v", res["restart-on"])
}
}
}
// A secret only a template names is in the plan by name alone.
func TestASecretOnlyATemplateNamesIsPlannedByName(t *testing.T) {
raw := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"},`,
`"own-secrets":{"token":"${dir:state}/token","api-key":"${dir:state}/api-key"},`, 1)
r := notifierOn(t, held(t, raw, builtBot(botText+"key = ${secret:api-key}\n")))
c, _ := composeWith(t, r, handingTo("store:5000"))
names := []string{}
for _, s := range c.Plans["bot"].Secrets {
names = append(names, s.Name)
}
if strings.Join(names, " ") != "api-key token" {
t.Fatalf("the plan names %v", c.Plans["bot"].Secrets)
}
}
// Nothing changes for a module that has not moved, nor for a moved one where it is not handed: its secrets are
// sealed into the declaration as before.
func TestAMovedModulesSecretsAreSealedAsBeforeWhereItIsNotHanded(t *testing.T) {
_, before := composeWith(t, notifierOn(t, held(t, handedBot, builtBot(botText))),
sealedForBot(handingTo("store:5000")))
if !strings.Contains(before, "sealed-to-the-node") {
t.Fatalf("the unmoved module is not sealed the old way: %s", before)
}
for name, with := range map[string]Rendering{
"an engine that does not take a filled file alone": {Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"},
"no provisioner client on the machine": {Hands: true, ArtifactStore: "store:5000"},
} {
c, got := composeWith(t, notifierOn(t, held(t, botMoved(), builtBot(botText))), sealedForBot(with))
if got != before {
t.Errorf("%s: the moved module composes differently from its unmarked twin:\nbefore: %s\nnow: %s",
name, before, got)
}
if len(c.Filled) > 0 || c.Plans != nil {
t.Errorf("%s: something was handed: %v %v", name, c.Filled, c.Plans)
}
}
}
func TestWhatAHandedModuleCannotHaveFromTheVaultIsRefused(t *testing.T) {
// A template naming a credential the module requires: a binding's, which moves with its provider.
requires := strings.Replace(botMoved(), `"own-secrets":`, `"requires":["database"],"secrets":{"database":"/etc/bot/db"},"own-secrets":`, 1)
r, err := Resolve(shelf(held(t, requires, builtBot("db = ${secret:database}\n")),
Manifest{Module: "db", Version: "1", Provides: []Offer{{Name: "database", Scope: ScopeNode}}}),
[]string{"bot"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
r.Account = "op"
if _, err := r.Compose(handingTo("store:5000")); err == nil || !strings.Contains(err.Error(), "moves with its provider") {
t.Errorf("a template naming a required credential: %v", err)
}
// A file with its text inline naming a secret, in a handed module.
inline := strings.Replace(botMoved(), `{"id":"daemon"`,
`{"id":"legacy","type":"file","path":"/etc/bot/legacy","content":"t = ${secret:token}\n"},{"id":"daemon"`, 1)
r = notifierOn(t, held(t, inline, builtBot(botText)))
if _, err := r.Compose(handingTo("store:5000")); err == nil || !strings.Contains(err.Error(), "keep it in a template") {
t.Errorf("an inline file naming a secret in a handed module: %v", err)
}
// A family and a secret issued outside the mesh are not handed by name yet: the mark is refused.
for why, raw := range map[string]string{
"a family": strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"own-secrets":{"token":"${dir:state}/token","key-*":"${dir:state}/keys"}`, 1),
"issued outside": strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"own-secrets":{"token":{"path":"${dir:state}/token","issued-by":"outside"}}`, 1),
} {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "design 57 V4") {
t.Errorf("a moved module with %s: %v", why, err)
}
}
}
// A credential of what a handed module requires that the vault keeps for it alone (issue 757): the forge's read
// token for one agent, issued by the forge's software per holder. Where the module is handed, its file is declared
// placed and without a value, and the plan names it with its provision and provider; anywhere else, and for a
// credential the mesh makes, the composition is what it was.
func forgeReader(t *testing.T, moved bool, issued bool) Resolution {
t.Helper()
raw := strings.Replace(handedBot, `"own-secrets":`,
`"requires":[{"name":"forge-read","optional":true}],"secrets":{"forge-read":"${dir:state}/forge.secret"},"own-secrets":`, 1)
if moved {
raw = strings.Replace(raw, `"version":"1",`, `"version":"1","moved":true,`, 1)
}
r := notifierOn(t, held(t, raw, builtBot(botText)))
r.Needs = append(r.Needs, Needed{Name: "forge-read", From: "novox", For: "bot", Module: "forge",
Sealed: "sealed-forge-read", IssuedToHolder: issued})
return r
}
func TestAHandedModulesCredentialIssuedForItIsNamedInItsPlanAndNoValueIsSealed(t *testing.T) {
c, got := composeWith(t, forgeReader(t, true, true), handingTo("store:5000"))
if strings.Contains(got, "sealed") || strings.Contains(got, HandedProvisionField) {
t.Fatalf("a handed module's declaration carries a sealed value or the marker: %s", got)
}
var file map[string]any
for _, res := range c.Resources {
if res["id"] == "bot."+SecretID("forge-read") {
file = res
}
}
if file == nil {
t.Fatalf("the credential's file is not declared: %s", got)
}
path, _ := file["path"].(string)
if path == "" || strings.Contains(path, "${") || file["mode"] != "0600" || file["owner-at-runtime"] != "owned" {
t.Fatalf("the credential's file is declared %v: placed, 0600, owned at runtime, no value", file)
}
if !strings.Contains(strings.Join(c.Filled, " "), path) {
t.Fatalf("the credential's file is not among the filled files %v", c.Filled)
}
want := []PlanProvision{{Name: "forge-read", Provision: "forge-read", Provider: "novox", ProviderModule: "forge",
Path: path, From: "novox", As: ConsumerIdentity("workstation", "bot")}}
if !reflect.DeepEqual(c.Plans["bot"].Provisions, want) {
t.Fatalf("the plan names %+v, want %+v", c.Plans["bot"].Provisions, want)
}
}
func TestACredentialIssuedForAHolderIsSealedAsBeforeWhereItsModuleIsNotHanded(t *testing.T) {
_, before := composeWith(t, forgeReader(t, false, false), sealedForBot(handingTo("store:5000")))
if !strings.Contains(before, "sealed-forge-read") {
t.Fatalf("the unmoved module's credential is not sealed the old way: %s", before)
}
for why, r := range map[string]Resolution{
"a module not moved": forgeReader(t, false, true),
"a moved module whose credential is made": forgeReader(t, true, false),
} {
c, got := composeWith(t, r, sealedForBot(handingTo("store:5000")))
if !strings.Contains(got, "sealed-forge-read") {
t.Errorf("%s: the credential is not sealed the old way: %s", why, got)
}
// Its provider is named (issue 760), and no credential for the client to fill.
if p, ok := c.Plans["bot"]; ok && slices.ContainsFunc(p.Provisions, func(pr PlanProvision) bool {
return pr.Name != "" || pr.Path != ""
}) {
t.Errorf("%s: the plan names a credential to fill: %+v", why, p.Provisions)
}
}
c, got := composeWith(t, forgeReader(t, true, true), sealedForBot(Rendering{Provisioner: "mesh-provisioner-client",
ArtifactStore: "store:5000"}))
if !strings.Contains(got, "sealed-forge-read") || c.Plans != nil {
t.Errorf("where the engine does not take a filled file alone, the credential is not sealed the old way: %s", got)
}
}
// A handed consumer's own login from a provider that gives each consumer one (issue 779): the same entry as for an
// issued credential, with the provider's form on it, and nothing sealed; where the consumer is not handed, the
// login is sealed as it was.
func TestAHandedConsumersOwnLoginFromAPerLoginProviderIsNamedInItsPlanAndNoValueIsSealed(t *testing.T) {
pair := func(handed bool) Resolution {
r := forgeReader(t, true, handed)
r.Needs[len(r.Needs)-1].CredentialForm = CredentialFormPair
return r
}
c, got := composeWith(t, pair(true), handingTo("store:5000"))
if strings.Contains(got, "sealed") || strings.Contains(got, HandedProvisionField) {
t.Fatalf("a handed consumer's declaration carries its login sealed or the marker: %s", got)
}
var path string
for _, res := range c.Resources {
if res["id"] == "bot."+SecretID("forge-read") {
path, _ = res["path"].(string)
if res["mode"] != "0600" || res["owner-at-runtime"] != "owned" || res["content"] != nil || res["sealed"] != nil {
t.Fatalf("the login's file is declared %v: placed, 0600, owned at runtime, no value", res)
}
}
}
if path == "" || !strings.Contains(strings.Join(c.Filled, " "), path) {
t.Fatalf("the login's file %q is not declared and filled: %v", path, c.Filled)
}
want := []PlanProvision{{Name: "forge-read", Provision: "forge-read", Provider: "novox", ProviderModule: "forge",
CredentialForm: CredentialFormPair, Path: path, From: "novox", As: ConsumerIdentity("workstation", "bot")}}
if !reflect.DeepEqual(c.Plans["bot"].Provisions, want) {
t.Fatalf("the plan names %+v, want %+v", c.Plans["bot"].Provisions, want)
}
// Not marked as the vault's, the login is sealed as before.
_, before := composeWith(t, pair(false), sealedForBot(handingTo("store:5000")))
if !strings.Contains(before, "sealed-forge-read") {
t.Fatalf("a consumer whose login the vault does not keep for it has it sealed no more: %s", before)
}
}