`secret rotate <node> <module> <name>` makes the secret anew the way the first mint did, seals it
to the machine and the operator, and sends the machine, so the module starts again on the new value
— said in the log with who asked and when, never the value. Only for a secret whose definition says
`"taken": "at-start"`: an own secret is now a path, or {path, taken}, and a definition that says
nothing of how a secret is taken is refused with the word to write, because a credential rotated
under software that never reads it again is worse than one left alone (issue 179). `applied` is
refused by name until the staged form ADR 0114 decided is built; a value given to the mesh is
refused as ADR 0113 says. `rotate` is a verb on the controller's seat with two shapes — a pair
credential by provision, an own secret by machine, module and name — so the console can ask.
Registered manifests keep their bytes: a path alone is written back as a path.
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A secret a module needs in order to be itself.
|
|
//
|
|
// A database has a superuser password, a broker an administrator, a registry an account. None is
|
|
// *for* anybody — it is not the credential a consumer is given, and the mechanism that hands
|
|
// those out has a consumer in the middle of it.
|
|
|
|
func needy() Manifest {
|
|
return Manifest{
|
|
Module: "postgres", Version: "1",
|
|
OwnSecrets: OwnSecrets{"superuser": {Path: "/var/lib/mesh/postgres/superuser"}},
|
|
Resources: []map[string]any{
|
|
{"id": "store", "type": "container", "name": "mesh-postgres", "image": "postgres@sha256:x"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestAModulesOwnSecretLandsSealed(t *testing.T) {
|
|
got, err := Resolve(shelf(needy()), []string{"postgres"}, reachable(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := got.Declaration(Rendering{
|
|
Needed: map[string]map[string]string{"postgres": {"superuser": "c2VhbGVk"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range out {
|
|
if r["path"] != "/var/lib/mesh/postgres/superuser" {
|
|
continue
|
|
}
|
|
if r["sealed"] != "c2VhbGVk" {
|
|
t.Fatalf("got %v", r)
|
|
}
|
|
if r["content"] != nil {
|
|
t.Fatal("a module's own secret was written in the clear")
|
|
}
|
|
return
|
|
}
|
|
t.Fatalf("no secret was written: %v", out)
|
|
}
|
|
|
|
func TestADeclaredNeedThatWasNotMadeIsRefused(t *testing.T) {
|
|
// Skipping it would start a database with no password it knows, which fails to authenticate
|
|
// three layers from the machine reporting it.
|
|
got, _ := Resolve(shelf(needy()), []string{"postgres"}, reachable(), World{})
|
|
_, err := got.Declaration(Rendering{})
|
|
if err == nil {
|
|
t.Fatal("a module needing a secret was declared without one")
|
|
}
|
|
if !strings.Contains(err.Error(), "superuser") {
|
|
t.Fatalf("the refusal does not name what is missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestANeedIsAnAbsolutePath(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"postgres","version":"1",
|
|
"own-secrets":{"superuser":"superuser.txt"}}`))
|
|
if err == nil {
|
|
t.Fatal("a relative path was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "absolute path") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAModuleMayNeedSeveralThings(t *testing.T) {
|
|
// A password and a token, say. Telling them apart is the module's business, not the mesh's.
|
|
m := needy()
|
|
m.OwnSecrets["replication"] = OwnSecret{Path: "/var/lib/mesh/postgres/replication"}
|
|
got, _ := Resolve(shelf(m), []string{"postgres"}, reachable(), World{})
|
|
out, err := got.Declaration(Rendering{Needed: map[string]map[string]string{
|
|
"postgres": {"superuser": "b25l", "replication": "dHdv"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := map[string]string{}
|
|
for _, r := range out {
|
|
if path, ok := r["path"].(string); ok && strings.Contains(path, "/var/lib/mesh/postgres/") {
|
|
seen[path], _ = r["sealed"].(string)
|
|
}
|
|
}
|
|
if len(seen) != 2 || seen["/var/lib/mesh/postgres/superuser"] == seen["/var/lib/mesh/postgres/replication"] {
|
|
t.Fatalf("two needs did not land as two secrets: %v", seen)
|
|
}
|
|
}
|
|
|
|
// A manifest written against the old name is told what the field became.
|
|
//
|
|
// `needs` and `secrets` were both name-to-path and differed only in whose secret it was, so
|
|
// reaching for the wrong one parsed cleanly and failed somewhere else entirely. Refusing the old
|
|
// name with "unknown field" would be correct and unhelpful: whoever wrote it knew what they meant,
|
|
// and the mesh knows what it is called now.
|
|
func TestAManifestUsingTheOldNameIsToldTheNewOne(t *testing.T) {
|
|
_, err := ParseManifest([]byte(
|
|
`{"module":"postgres","version":"1","needs":{"superuser":"/var/lib/superuser"}}`))
|
|
if err == nil {
|
|
t.Fatal("a manifest using the old name was accepted, so two fields now mean one thing")
|
|
}
|
|
for _, want := range []string{"needs", "own-secrets"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("the refusal does not mention %q: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And an ordinary unknown key is still refused as one, rather than being guessed at.
|
|
func TestAnInventedKeyIsNotTreatedAsARename(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"postgres","version":"1","nonsense":{}}`))
|
|
if err == nil {
|
|
t.Fatal("an invented key was accepted")
|
|
}
|
|
if strings.Contains(err.Error(), "is now called") {
|
|
t.Fatalf("an invented key was reported as a rename: %v", err)
|
|
}
|
|
}
|