nox cleanup delete without --confirm deletes nothing; the vault refuses it for its missing confirm alone and judges only a confirmed delete, and nox records the warranting act only for that one (issue 1126 review 2). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
119 lines
7.0 KiB
Go
119 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ADR 0590 §8 (issue 1126): approving or deleting a retirement, rotating a secret and exporting sealed copies each
|
|
// answer the operator's account, or a caller naming the hand act it recorded for that verb and that subject, and
|
|
// nothing else: the controller that judged for them is not placed.
|
|
func TestEachSensitiveVerbJudgesItsOwnCaller(t *testing.T) {
|
|
now := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)
|
|
acts := map[string]HandActSeen{
|
|
"mine": {ID: "mine", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}},
|
|
"old": {ID: "old", By: "novox.mesh-healers", At: now.Add(-25 * time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}},
|
|
"other": {ID: "other", By: "novox.mesh-config", At: now.Add(-time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}},
|
|
"del": {ID: "del", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "mesh-vault.provisioner_delete", Args: []string{"d-app"}},
|
|
// Review 1: a verb of another seat or module that ends in the vault verb's name warrants nothing.
|
|
"alike": {ID: "alike", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "other-module.rotate", Args: []string{"own/g14/app/key"}},
|
|
"bare": {ID: "bare", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}},
|
|
// As mesh-hand-acts records nox's act: one what, the consumers space-separated (review 1 of issue 1126).
|
|
"two": {ID: "two", By: "shanks.node-tools", At: now.Add(-time.Minute), Verb: "mesh-vault.provisioner_retire_approve", Args: []string{"c1 c2"}},
|
|
}
|
|
read := func(id string) (*HandActSeen, error) {
|
|
if a, ok := acts[id]; ok {
|
|
return &a, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
clock := func() time.Time { return now }
|
|
for _, row := range []struct {
|
|
name, caller, verb, act string
|
|
about []string
|
|
want string // empty: allowed
|
|
}{
|
|
{"the operator's account", "person.jochen", SensitiveRotate, "", []string{"own/g14/app/key"}, ""},
|
|
{"the operator's account exports", "person.jochen", SensitiveExport, "", nil, ""},
|
|
{"unproven", "unproven", SensitiveVaultExport, "", nil, "proved none"},
|
|
{"a bare person prefix", "person.", SensitiveExport, "", nil, "named none"},
|
|
{"a module with no act", "novox.mesh-healers", SensitiveRotate, "", []string{"own/g14/app/key"}, "named none"},
|
|
{"a module's own act", "novox.mesh-healers", SensitiveRotate, "mine", []string{"own/g14/app/key"}, ""},
|
|
{"an act of another subject", "novox.mesh-healers", SensitiveRotate, "mine", []string{"own/ace/app/key"}, "not own/ace/app/key"},
|
|
{"an act of another verb", "novox.mesh-healers", SensitiveRotate, "del", []string{"own/g14/app/key"}, "warrants no rotate"},
|
|
{"an act of another caller", "novox.mesh-healers", SensitiveRotate, "other", []string{"own/g14/app/key"}, "recorded by novox.mesh-config"},
|
|
{"an act too old", "novox.mesh-healers", SensitiveRotate, "old", []string{"own/g14/app/key"}, "older than"},
|
|
{"an act not recorded", "novox.mesh-healers", SensitiveDelete, "none", []string{"d-app"}, "holds no hand act"},
|
|
{"a delete's own act", "novox.mesh-healers", SensitiveDelete, "del", []string{"d-app"}, ""},
|
|
{"an act of another seat's rotate", "novox.mesh-healers", SensitiveRotate, "alike", []string{"own/g14/app/key"}, "warrants no rotate"},
|
|
{"an act naming the verb without its seat", "novox.mesh-healers", SensitiveRotate, "bare", []string{"own/g14/app/key"}, "warrants no rotate"},
|
|
{"one act for two consumers", "shanks.node-tools", SensitiveRetireApprove, "two", []string{"c1", "c2"}, ""},
|
|
{"one act naming two, asked for a third", "shanks.node-tools", SensitiveRetireApprove, "two", []string{"c1", "c3"}, "not c3"},
|
|
} {
|
|
err := JudgeSensitive(read, clock, row.caller, row.verb, row.act, row.about...)
|
|
if (row.want == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), row.want)) {
|
|
t.Errorf("%s: %v, want %q", row.name, err, row.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The tools themselves judge: a module's call with no act is refused before anything is read or changed, and the
|
|
// operator's account goes through.
|
|
func TestTheSensitiveToolsRefuseACallerTheyDoNotJudgeTheirs(t *testing.T) {
|
|
env := &Env{Root: t.TempDir()}
|
|
defs := definitions(env)
|
|
for _, name := range []string{Seat + ".rotate", "provisioner_retire_approve", "provisioner_delete", "secret_export",
|
|
"secret_vault_export"} {
|
|
if _, err := toolNamed(t, defs, name).runAs("novox.some-module", map[string]any{"why": "x", "consumer": "c",
|
|
"confirm": "c", "consumers": []any{"c"}, "node": "g14", "module": "app", "name": "key"}); err == nil ||
|
|
!strings.Contains(err.Error(), "ADR 0590") {
|
|
t.Errorf("%s answered a module with no act: %v", name, err)
|
|
}
|
|
}
|
|
if _, err := toolNamed(t, defs, "secret_export").runAs("person.jochen", map[string]any{}); err != nil {
|
|
t.Errorf("the operator's export was refused: %v", err)
|
|
}
|
|
// Listing holders alone is no sealed copy: any proven caller reads it.
|
|
if _, err := toolNamed(t, defs, "secret_export").runAs("novox.some-module", map[string]any{"sealed": false}); err != nil {
|
|
t.Errorf("a listing without sealed copies was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// Review 1 of issue 1126: nox reaches the vault as its machine's runtime (`<node>.node-tools`), never as a person; it
|
|
// records the hand act first and names it, and so the export answers it, and refuses it without.
|
|
func TestNoxsRuntimeExportsWithItsHandActAndIsRefusedWithout(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v.ReadAct = func(id string) (*HandActSeen, error) {
|
|
if id != "nox-1" {
|
|
return nil, nil
|
|
}
|
|
return &HandActSeen{ID: id, By: "shanks.node-tools", At: v.Now(), Verb: "mesh-vault." + SensitiveVaultExport,
|
|
Args: []string{"the vault's sealed copies"}}, nil
|
|
}
|
|
export := toolNamed(t, definitions(&Env{Vault: v.Vault}), SensitiveVaultExport)
|
|
if _, err := export.runAs("shanks.node-tools", map[string]any{}); err == nil || !strings.Contains(err.Error(), "named none") {
|
|
t.Errorf("an export with no act: %v", err)
|
|
}
|
|
if _, err := export.runAs("shanks.node-tools", map[string]any{"act": "nox-1"}); err != nil {
|
|
t.Errorf("an export with nox's act: %v", err)
|
|
}
|
|
}
|
|
|
|
// Review 2 of issue 1126: a delete without its confirm deletes nothing, so it is refused for that, never judged as an
|
|
// act wanting a hand act; nox records none for it.
|
|
func TestADeleteWithoutConfirmIsRefusedForItsConfirmAlone(t *testing.T) {
|
|
del := toolNamed(t, definitions(&Env{Custody: retiredC(t).Custody}), "provisioner_delete")
|
|
if _, err := del.runAs("shanks.node-tools", map[string]any{"consumer": "d-app", "why": "look"}); err == nil ||
|
|
!strings.Contains(err.Error(), "confirm must repeat") {
|
|
t.Errorf("an unconfirmed delete: %v", err)
|
|
}
|
|
if _, err := del.runAs("shanks.node-tools", map[string]any{"consumer": "d-app", "confirm": "d-app", "why": "gone"}); err == nil ||
|
|
!strings.Contains(err.Error(), "named none") {
|
|
t.Errorf("a confirmed delete with no act: %v", err)
|
|
}
|
|
}
|