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).
253 lines
12 KiB
Go
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)
|
|
}
|
|
}
|