diff --git a/modules/mesh-vault/cmd/mesh-vault/followups_test.go b/modules/mesh-vault/cmd/mesh-vault/followups_test.go new file mode 100644 index 00000000..aa5fbb8d --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/followups_test.go @@ -0,0 +1,162 @@ +package main + +// The four follow-ups V1's third review (review 263 on mesh-catalog#286) left for this package: the tool layer +// passes an act's words through; a rolled record of hand-outs is said in the journal; the export's fingerprint +// and vault_status are what they say; and an act's unproven `by` is kept beside the proven caller. + +import ( + "fmt" + "sort" + "strings" + "sync" + "testing" + + "git.novox.be/novox/mesh-sdk/go/seal" +) + +func anyOf(list []string) []any { + out := make([]any, len(list)) + for i, s := range list { + out[i] = s + } + return out +} + +// (1) and (4): the retirement tools pass consumers, why, by, via and confirm through to the custody ledger, and +// a `by` naming somebody other than the proven caller is kept beside it. +func TestTheRetirementToolsPassTheActThrough(t *testing.T) { + c := newCustody(t) + c.ask(apps(5)) + c.passes(1) + c.ask(keepOnly(apps(5), 1)) + c.stable() + set := waitingNames(c.Retirement()) + sort.Strings(set) + defs := definitions(&Env{Custody: c.Custody}) + approve := toolNamed(t, defs, "provisioner_retire_approve") + if _, err := approve.runAs(ControllerCaller, map[string]any{"consumers": anyOf(set[:2]), "why": "x"}); err == nil { + t.Error("the tool took a set other than the one waiting") + } + got, err := approve.runAs(ControllerCaller, map[string]any{"consumers": anyOf(set), "why": "the apps are gone", + "by": "person.jochen", "via": "telegram"}) + if err != nil { + t.Fatal(err) + } + if done := got.(map[string]any)["retired"].([]string); len(done) != 4 { + t.Errorf("retired %v", done) + } + last := c.bus.events[len(c.bus.events)-5]["body"].(map[string]any) + for _, e := range c.bus.events { + if b := e["body"].(map[string]any); b["change"] == "approved" { + last = b + } + } + if last["why"] != "the apps are gone" || last["by"] != "person.jochen" || last["via"] != "telegram, called by controller" { + t.Errorf("the approval said %v", last) + } + + // Delete: consumer and confirm through, the act's words with them. + r := retiredC(t) + del := toolNamed(t, definitions(&Env{Custody: r.Custody}), "provisioner_delete") + if _, err := del.runAs("person.jochen", map[string]any{"consumer": "d-app", "confirm": "c-app", "why": "x"}); err == nil { + t.Error("a confirm naming another was taken through the tool") + } + if _, err := del.runAs("person.jochen", map[string]any{"consumer": "d-app", "confirm": "d-app", "why": "gone"}); err != nil { + t.Fatal(err) + } + body := r.bus.events[len(r.bus.events)-1]["body"].(map[string]any) + if body["change"] != "deleted" || body["by"] != "person.jochen" || body["via"] != nil || body["why"] != "gone" { + t.Errorf("the deletion said %v", body) + } + + // Reject, by the proven caller alone: nothing more is added to via. + k := newCustody(t) + k.ask(apps(5)) + k.passes(1) + k.ask(keepOnly(apps(5), 1)) + k.stable() + waiting := waitingNames(k.Retirement()) + if _, err := toolNamed(t, definitions(&Env{Custody: k.Custody}), "provisioner_retire_reject").runAs("person.jochen", + map[string]any{"consumers": anyOf(waiting), "why": "they come back"}); err != nil { + t.Fatal(err) + } + if body := k.bus.events[len(k.bus.events)-1]["body"].(map[string]any); body["by"] != "person.jochen" || body["via"] != nil { + t.Errorf("the rejection said %v", body) + } +} + +// (2): a roll of the record of hand-outs is said in the journal, loudly. +func TestARolledRecordOfHandOutsIsSaid(t *testing.T) { + v := newVault(t, nil) + var mu sync.Mutex + var said []string + v.Log = func(format string, args ...any) { + mu.Lock() + defer mu.Unlock() + said = append(said, fmt.Sprintf(format, args...)) + } + v.Store.HandedMax = 600 + asker, _ := seal.GenerateKeyPair() + for i := 0; i < 20; i++ { + _, _ = v.Get("g14.flood", "pair/postgres/gitea", asker.PublicKey) + } + rolled := 0 + for _, s := range said { + if strings.Contains(s, "THE RECORD OF HAND-OUTS WAS ROLLED") { + rolled++ + } + } + if rolled == 0 || rolled != v.Store.Rolled() { + t.Errorf("rolled %d times, said %d times", v.Store.Rolled(), rolled) + } +} + +// (3): the vault's export names the operator key's fingerprint as the controller's secrets.Fingerprint says it, +// and vault_status says what needs a person. +func TestTheExportsFingerprintAndVaultStatus(t *testing.T) { + v := newVault(t, nil) + if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil { + t.Fatal(err) + } + answer, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_vault_export").Run(map[string]any{}) + if err != nil { + t.Fatal(err) + } + doc := answer.(map[string]any) + // sha256 of the key, its first eight bytes in hex: the controller's secrets.Fingerprint. + if fp := doc["fingerprint"].(string); !strings.HasPrefix(fp, "sha256:") || len(fp) != len("sha256:")+16 || + fp != keyFingerprint(v.operatorPublic) || doc["operator-key"] != v.operatorPublic { + t.Errorf("the export says %v", doc) + } + if keyFingerprint("") != "" { + t.Error("no key has a fingerprint") + } + + env := &Env{Vault: v.Vault} + status := func() map[string]any { + got, err := toolNamed(t, definitions(env), "vault_status").Run(map[string]any{}) + if err != nil { + t.Fatal(err) + } + return got.(map[string]any) + } + if s := status(); s["ok"] != true || s["copies_on_an_earlier_key"] != 0 || s["operator_key"] != v.operatorPublic { + t.Errorf("a vault in order says %v", s) + } + env.refuseKey("a private key was offered") + if s := status(); s["ok"] != false || s["operator_key_refused"] != "a private key was offered" { + t.Errorf("a refused key says %v", s) + } + env.refuseKey("") + // A new operator key whose resealing fails leaves copies on the earlier key, and the status says so. + pub, _ := operatorKey(t) + v.Store.failWrite = func(Record) error { return fmt.Errorf("disk full") } + _, _ = v.SetOperatorKey(asPEM(t, pub), "the test") + v.Store.failWrite = nil + if s := status(); s["ok"] != false || s["copies_on_an_earlier_key"] != 1 { + t.Errorf("a copy on an earlier key says %v", s) + } + if s, _ := toolNamed(t, definitions(&Env{}), "vault_status").Run(map[string]any{}); s.(map[string]any)["ready"] != false { + t.Errorf("no vault says %v", s) + } +}