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