package resolution import ( "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) func fileNamed(out []map[string]any, id string) map[string]any { for _, r := range out { if r["id"] == id { return r } } return nil } // A module's own credential, put into a file it wrote: the value sealed to the node, or, in a composition asked ahead of // the send or for a plan alone, the stand-in its own file has (issue 774: baserow's config.json refused ace's plans). func TestAFileGetsTheSecretItsContentAsksFor(t *testing.T) { for _, row := range []struct { name string with mcat.Rendering want string }{ {"sealed", mcat.Rendering{Needed: map[string]map[string]string{"gitea": {"admin": "sealed-admin"}}}, "sealed-admin"}, {"foreseen", mcat.Rendering{Foreseen: map[string]map[string]bool{"gitea": {"admin": true}}}, mcat.ForeseenSealed}, } { r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{ Module: "gitea", OwnSecrets: mcat.OwnSecrets{"admin": {Path: "/var/lib/gitea/admin.env"}}, Resources: []map[string]any{{ "id": "conf", "type": "file", "path": "/etc/gitea/app.ini", "content": "[security]\nSECRET_KEY = ${secret:admin}\n", }}, }}} out, err := Declaration(r, row.with) if err != nil { t.Fatalf("%s: %v", row.name, err) } file := fileNamed(out, "gitea.conf") if file == nil { t.Fatalf("%s: no file in %v", row.name, out) } got, _ := file["secrets"].(map[string]any) if got["admin"] != row.want { t.Errorf("%s: the file was sent without its secret: %v", row.name, file) } // The hole is still a hole on the wire. The mesh does not fill it in; the host does, on the // machine, which is the only place both halves exist. if !strings.Contains(file["content"].(string), "${secret:admin}") { t.Errorf("%s: the mesh substituted it itself: %v", row.name, file["content"]) } } } // The gap this exists for: a granted credential arrives as a bare password, and the program that // needs it reads KEY=value. func TestAGrantedCredentialCanBeShapedIntoAnEnvFile(t *testing.T) { r := mcat.Resolution{ Node: "anchor", Modules: []mcat.Manifest{{ Module: "keycloak", Requires: []string{"postgres-database"}, Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"}, Resources: []map[string]any{{ "id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env", "content": "KC_DB_USERNAME=keycloak\nKC_DB_PASSWORD=${secret:postgres-database}\n", "mode": "0600", }}, }}, Needs: []mcat.Needed{{Name: "postgres-database", For: "keycloak", From: "anchor", Sealed: "sealed-db"}}, } out, err := Declaration(r, mcat.Rendering{}) if err != nil { t.Fatal(err) } file := fileNamed(out, "keycloak.dbenv") got, _ := file["secrets"].(map[string]any) if got["postgres-database"] != "sealed-db" { t.Errorf("the credential never reached the file it was needed in: %v", file) } } // Two modules on one node requiring the same provision each get THEIR OWN credential in a // ${secret:…} file — not whichever need happened to come last. This is the placeholder-path twin of // the guard novox/hq 04-ISSUES/022 put on the secrets:-map path; without it, a node with baserow and // letta both consuming postgres gave baserow letta's password and authentication failed. func TestTwoConsumersOfOneProvisionEachGetTheirOwnInAPlaceholderFile(t *testing.T) { r := mcat.Resolution{ Node: "anchor", Modules: []mcat.Manifest{ { Module: "keycloak", Requires: []string{"postgres-database"}, Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"}, Resources: []map[string]any{{ "id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env", "content": "PW=${secret:postgres-database}\n", "mode": "0600", }}, }, { Module: "grafana", Requires: []string{"postgres-database"}, Secrets: map[string]string{"postgres-database": "/var/lib/grafana/db.secret"}, Resources: []map[string]any{{ "id": "dbenv", "type": "file", "path": "/var/lib/grafana/database.env", "content": "PW=${secret:postgres-database}\n", "mode": "0600", }}, }, }, Needs: []mcat.Needed{ {Name: "postgres-database", For: "keycloak", From: "anchor", Sealed: "sealed-kc"}, {Name: "postgres-database", For: "grafana", From: "anchor", Sealed: "sealed-gf"}, }, } out, err := Declaration(r, mcat.Rendering{}) if err != nil { t.Fatal(err) } kc, _ := fileNamed(out, "keycloak.dbenv")["secrets"].(map[string]any) if kc["postgres-database"] != "sealed-kc" { t.Errorf("keycloak got the wrong credential (someone else's password): %v", kc) } gf, _ := fileNamed(out, "grafana.dbenv")["secrets"].(map[string]any) if gf["postgres-database"] != "sealed-gf" { t.Errorf("grafana got the wrong credential (someone else's password): %v", gf) } } // A name nothing declared is a typo, and it is refused here rather than on the machine. func TestAFileNamingASecretTheModuleDoesNotHaveIsRefused(t *testing.T) { r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{ Module: "gitea", OwnSecrets: mcat.OwnSecrets{"admin": {Path: "/var/lib/gitea/admin.env"}}, Resources: []map[string]any{{ "id": "conf", "type": "file", "path": "/etc/gitea/app.ini", "content": "SECRET_KEY = ${secret:adnim}\n", }}, }}} _, err := Declaration(r, mcat.Rendering{ Needed: map[string]map[string]string{"gitea": {"admin": "sealed-admin"}}, }) if err == nil { t.Fatal("a file asked for a secret that does not exist and the mesh sent it anyway") } // It says what could have been meant, because the answer is nearly always one of two names. if !strings.Contains(err.Error(), `"admin"`) { t.Errorf("the refusal does not say what it could have named: %v", err) } } // One module may not read another's credential by guessing its name. func TestAFileCannotNameAnotherModulesSecret(t *testing.T) { r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{ {Module: "postgres", OwnSecrets: mcat.OwnSecrets{"superuser": {Path: "/var/lib/postgres/su.env"}}}, {Module: "gitea", Resources: []map[string]any{{ "id": "conf", "type": "file", "path": "/etc/gitea/app.ini", "content": "PASSWORD=${secret:superuser}\n", }}}, }} _, err := Declaration(r, mcat.Rendering{Needed: map[string]map[string]string{ "postgres": {"superuser": "sealed-su"}, }}) if err == nil { t.Fatal("gitea read postgres's credential by naming it") } } // A token inside a JSON document, where a setting put the placeholder there. // // **The case this whole shape was argued from**: a desktop client that reads its token from an // attribute inside a JSON file in a home directory, not from an environment variable. The module // ships an empty document, a setting says which attribute, and the credential never passes // through the mesh in the open. func TestASettingThatCarriesAPlaceholderIsStillFilled(t *testing.T) { r := mcat.Resolution{Node: "workstation", Modules: []mcat.Manifest{{ Module: "chat", OwnSecrets: mcat.OwnSecrets{"api-token": {Path: "/home/operator/.config/chat/token"}}, Resources: []map[string]any{{ "id": "settings", "type": "file", "merge": "json", "path": "/home/operator/.config/chat/settings.json", "content": "{}", }}, }}} out, err := Declaration(r, mcat.Rendering{ Needed: map[string]map[string]string{"chat": {"api-token": "sealed-token"}}, Settings: mcat.SettingsBy{"chat": {{From: "node", Values: map[string]any{ "chat.atlassian.token": "${secret:api-token}", }}}}, }) if err != nil { t.Fatal(err) } file := fileNamed(out, "chat.settings") got, _ := file["secrets"].(map[string]any) if got["api-token"] != "sealed-token" { t.Errorf("a placeholder a setting put there was never filled: %v", file) } // Filling secrets runs after settings for exactly this reason: before the merge, the content // is "{}" and asks for nothing at all. if !strings.Contains(file["content"].(string), "${secret:api-token}") { t.Errorf("the hole did not survive to the machine: %v", file["content"]) } } // An own secret and a requirement of the same name would make ${secret:x} mean either. func TestANameMeaningTwoThingsIsRefused(t *testing.T) { r := mcat.Resolution{ Node: "anchor", Modules: []mcat.Manifest{{ Module: "thing", OwnSecrets: mcat.OwnSecrets{"store": {Path: "/var/lib/thing/own.env"}}, Secrets: map[string]string{"store": "/var/lib/thing/granted.env"}, }}, Needs: []mcat.Needed{{Name: "store", From: "anchor", Sealed: "sealed-granted"}}, } _, err := Declaration(r, mcat.Rendering{ Needed: map[string]map[string]string{"thing": {"store": "sealed-own"}}, }) if err == nil || !strings.Contains(err.Error(), "rename") { t.Errorf("one name meant two credentials and nothing said so: %v", err) } } // A file with no placeholder is not given a secrets map. The host refuses a secret its content // never asks for, so an empty map added helpfully would break every file that has none. func TestAFileWithNoPlaceholderIsLeftAlone(t *testing.T) { r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{ Module: "gitea", OwnSecrets: mcat.OwnSecrets{"admin": {Path: "/var/lib/gitea/admin.env"}}, Resources: []map[string]any{{ "id": "conf", "type": "file", "path": "/etc/gitea/app.ini", "content": "RUN_MODE=prod\n", }}, }}} out, err := Declaration(r, mcat.Rendering{ Needed: map[string]map[string]string{"gitea": {"admin": "sealed-admin"}}, }) if err != nil { t.Fatal(err) } if _, given := fileNamed(out, "gitea.conf")["secrets"]; given { t.Error("a file that asks for nothing was given a secrets map, which the host refuses") } }