CAT-V1: the seat mesh-vault's retire removes one module's own secret on the warrant of mesh-config's hand act, and says own-retired
The caller must be on the vault's trusted list retire-callers (the holder of mesh-config alone) and carry a hand act it recorded for exactly this node, module and name. A waiter on rotated or own-replaced does not wake on own-retired. Reviewer note of issue 1966: HandedOf returns a placement read error instead of dropping the modules placed there. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -128,6 +128,22 @@ func enrolmentTools(env *Env) []toolDef {
|
||||
}
|
||||
return v.AbandonRotation(caller, arg(a, "node"))
|
||||
}},
|
||||
{Name: Seat + "." + RetireVerb,
|
||||
Description: "Retire one module's own secret on a node because no manifest reads it any more (design 32): removed " +
|
||||
"whole, and own-retired said (node, module, name, the ask, the caller; never a value). Answered to the holder of " +
|
||||
"the seat mesh-config alone, as the vault's list retire-callers names it, and only for a hand act it recorded " +
|
||||
"through mesh-hand-acts for this verb naming exactly this node, module and name (`act`), read back by its id.",
|
||||
Input: map[string]any{"node": node, "module": str("the module whose secret it is"), "name": str("the secret's name"),
|
||||
"act": str("the id of the hand act recorded through mesh-hand-acts that warrants it"),
|
||||
"ask": str("the id of the operator's ask whose Approve this answers, kept in what is said")},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
v, err := env.ready()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.RetireOwn(caller, RetireArgs{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
|
||||
Act: arg(a, "act"), Ask: arg(a, "ask")})
|
||||
}},
|
||||
{Name: "node_logins",
|
||||
Description: "Every node's key of the vault's state node-logins: the login its node-engine dials as and, while a rotation " +
|
||||
"runs, the other login and its stage; with the rotations queued and the enrolment tokens held. Never a value or a hash.",
|
||||
|
||||
@@ -61,7 +61,7 @@ func build() *Env {
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant,
|
||||
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{},
|
||||
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")),
|
||||
Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")),
|
||||
RetireCallers: retireCallers(os.Getenv("MESH_VAULT_RETIRE_CALLERS")), Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")),
|
||||
Bound: Bound{
|
||||
Get: func(key string) ([]byte, error) {
|
||||
e, err := stdio.State(BoundState).Get(key)
|
||||
@@ -368,6 +368,26 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// retireCallers reads the vault's list `retire-callers`, the file the mesh writes from its trusted config value
|
||||
// (stdio.ParseCallers): a list that cannot be read allows nobody, and says why.
|
||||
func retireCallers(path string) stdio.Callers {
|
||||
if path == "" {
|
||||
say("the vault was started without its retire-callers list (MESH_VAULT_RETIRE_CALLERS): retire answers nobody")
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
say("the retire-callers list cannot be read, so retire answers nobody: %v", err)
|
||||
return nil
|
||||
}
|
||||
callers, err := stdio.ParseCallers(raw)
|
||||
if err != nil {
|
||||
say("%v: retire answers nobody", err)
|
||||
return nil
|
||||
}
|
||||
return callers
|
||||
}
|
||||
|
||||
// askCallers are the callers the vault's manifest names for secret-ask besides the controller (novox/hq ADR 0563 §8):
|
||||
// MESH_VAULT_SECRET_ASK_CALLERS, comma-separated, each a module (on any machine), <machine>.<module> or person.<name>.
|
||||
func askCallers(list string) stdio.Callers {
|
||||
|
||||
@@ -347,10 +347,30 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(root, "export.json"), export, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// An own secret of dunst on g14 is held before the vault starts, for the retire case below (CAT-V1).
|
||||
seedStore, err := OpenStore(filepath.Join(dir, "store"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedKey, err := OwnKey(filepath.Join(dir, "state"), seedStore)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seed := &Vault{Store: seedStore, Own: seedKey, Now: time.Now, Log: t.Logf, HashCost: bcrypt.MinCost}
|
||||
if _, err := seed.SetOperatorKey(asPEM(t, base64.StdEncoding.EncodeToString(opPub[:])), "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := seed.keep("own/g14/dunst/token", "a-token-value", "the test", "g14.dunst", OriginMade); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
callers := filepath.Join(dir, "retire-callers.json")
|
||||
if err := os.WriteFile(callers, []byte(`{"callers":["mesh-config"]}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, bin)
|
||||
cmd.Env = append(os.Environ(), "MESH_VAULT_STORE="+filepath.Join(dir, "store"), "MESH_VAULT_STATE="+filepath.Join(dir, "state"),
|
||||
"MESH_VAULT_ROOT="+root, "MESH_VAULT_LEDGER="+filepath.Join(dir, "ledger"), "MESH_RECEIVES="+filepath.Join(dir, "mesh.json"),
|
||||
"MESH_NODE=novox", "MESH_MODULE=mesh-vault")
|
||||
"MESH_VAULT_RETIRE_CALLERS="+callers, "MESH_NODE=novox", "MESH_MODULE=mesh-vault")
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -359,6 +379,15 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(func() { _ = stdin.Close(); cancel(); _ = cmd.Wait() })
|
||||
p := &player{t: t, ctx: ctx, nc: nc, js: js, in: stdin, pending: map[string]chan wire{}}
|
||||
// mesh-hand-acts holds the acts mesh-config recorded: one warranting the token's retirement, one for another name.
|
||||
p.ask = func(key string, _ json.RawMessage) (any, error) {
|
||||
if key != "mesh-hand-acts.hand-acts" {
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
return map[string]any{"acts": []HandActSeen{
|
||||
{ID: "act-ok", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst token"}},
|
||||
{ID: "act-other", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst other"}}}}, nil
|
||||
}
|
||||
go p.serve(stdout)
|
||||
|
||||
status := func() MintState {
|
||||
@@ -418,4 +447,31 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
|
||||
if !strings.Contains(said, AccountIssuedEvent+` {"`) || !strings.Contains(said, `"user":"ace.mod-x"`) || strings.Contains(said, held.Password) {
|
||||
t.Errorf("the vault said: %s", said)
|
||||
}
|
||||
|
||||
// CAT-V1 (design 32): retire on the real bus. A waiter on rotated and own-replaced hears nothing of it.
|
||||
waiter, err := nc.SubscribeSync("mesh.mod.mesh-vault.event.>")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []struct{ caller, act string }{{"g14.claude-code", "act-ok"}, {"novox.mesh-config", "act-other"}, {"novox.mesh-config", ""}} {
|
||||
if _, err := p.call(Seat+".retire", c.caller, map[string]any{"node": "g14", "module": "dunst", "name": "token", "act": c.act}); err == nil {
|
||||
t.Errorf("%s retired the token with act %q", c.caller, c.act)
|
||||
}
|
||||
}
|
||||
if m, err := waiter.NextMsg(300 * time.Millisecond); err == nil {
|
||||
t.Fatalf("a refused retire said %s", m.Subject)
|
||||
}
|
||||
if _, err := p.call(Seat+".retire", "novox.mesh-config", map[string]any{"node": "g14", "module": "dunst", "name": "token", "act": "act-ok",
|
||||
"ask": "ret-1"}); err != nil {
|
||||
t.Fatalf("retire with its warrant: %v", err)
|
||||
}
|
||||
m, err := waiter.NextMsg(10 * time.Second)
|
||||
if err != nil || m.Subject != "mesh.mod.mesh-vault.event."+OwnRetiredEvent || !strings.Contains(string(m.Data), `"ask":"ret-1"`) {
|
||||
t.Fatalf("retire said %v, %v", m, err)
|
||||
}
|
||||
for _, other := range []string{"rotated", OwnReplacedEvent} {
|
||||
if extra, err := waiter.NextMsg(300 * time.Millisecond); err == nil {
|
||||
t.Errorf("a waiter on %s heard %s", other, extra.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +455,11 @@ func (v *Vault) HandedOf(node string) ([]HandedJudged, error) {
|
||||
}
|
||||
plan := v.plansOf(node)
|
||||
placed, placedErr := plan.placedModules()
|
||||
if placedErr != nil {
|
||||
// Never dropped: an answer that leaves out the modules placed there, with no word of it, would read as "the others
|
||||
// are not placed" (issue 1966 review note).
|
||||
return nil, fmt.Errorf("the placement of %s could not be read, so the modules placed there are not all judged: %w", node, placedErr)
|
||||
}
|
||||
for _, m := range placed {
|
||||
modules[m] = true
|
||||
}
|
||||
@@ -462,9 +467,6 @@ func (v *Vault) HandedOf(node string) ([]HandedJudged, error) {
|
||||
for _, module := range slices.Sorted(maps.Keys(modules)) {
|
||||
out = append(out, plan.judged(module))
|
||||
}
|
||||
if placedErr != nil && len(out) == 0 {
|
||||
return nil, fmt.Errorf("the placement of %s could not be read and the vault keeps no own secret there: %w", node, placedErr)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -270,6 +270,7 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) {
|
||||
}
|
||||
v := newVault(t, nil)
|
||||
v.Askers = Declared{Read: plans.read, Store: v.Store}
|
||||
v.Placement = placementsOf(t, map[string][]string{"anchor": {"grafana", "web", "db"}})
|
||||
pub, _ := aSealingKey(t)
|
||||
for _, m := range []MoveOf{
|
||||
{Node: "anchor", Module: "grafana", Name: "admin", Origin: "made", Generation: "g-1", SealingKey: pub},
|
||||
@@ -314,4 +315,14 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) {
|
||||
if len(judged) != 4 {
|
||||
t.Errorf("own_handed names %v, want builder, db, grafana and web", judged)
|
||||
}
|
||||
// Review note of issue 1966: a placement that cannot be read is an error the tool answers, never a list that leaves out
|
||||
// the modules placed there while the vault keeps an own secret of others.
|
||||
v.Placement = PlacementReader{Keys: func() ([]string, error) { return nil, errors.New("the bus did not answer") },
|
||||
Read: func(string) ([]byte, error) { return nil, errors.New("the bus did not answer") }}
|
||||
if got, err := v.HandedOf("anchor"); err == nil || !strings.Contains(err.Error(), "placement of anchor could not be read") || got != nil {
|
||||
t.Errorf("an unreadable placement answered %v, %v", got, err)
|
||||
}
|
||||
if _, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "own_handed").Run(map[string]any{"node": "anchor"}); err == nil {
|
||||
t.Error("own_handed answered with the placement unread")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
// The seat's verb `retire` (design 32 *A value no longer read*, novox/hq ADR 0609): one module's own secret on one node
|
||||
// removed because no manifest reads it any more, asked by the holder of the seat mesh-config alone after the operator's
|
||||
// Approve. Two proofs, both required:
|
||||
//
|
||||
// - the caller is on the vault's list `retire-callers`, a trusted config value of the vault (ADR 0363's form, as
|
||||
// mesh-mail-reader's `callers`), which names the holder of mesh-config alone, checked against the caller the tool
|
||||
// runner proved (stdio.Callers); and
|
||||
// - the warrant: a hand act the caller recorded through mesh-hand-acts for this verb, read back here by its id, recent,
|
||||
// naming exactly this node, module and secret (ADR 0259 §6). An act for another name warrants nothing.
|
||||
//
|
||||
// The vault then removes the record whole and says `own-retired` (node, module, name, the ask's id, the caller it proved)
|
||||
// beside `rotated` and `own-replaced`, under its own subject: a waiter on either does not wake on it. Never a value.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// OwnRetiredEvent is what the vault says when it retired an own secret.
|
||||
const OwnRetiredEvent = "own-retired"
|
||||
|
||||
// RetireVerb is the seat's verb, and the verb of the hand act that warrants it (`mesh-vault.retire`).
|
||||
const RetireVerb = "retire"
|
||||
|
||||
// RetireArgs are `retire`'s arguments: the secret by node, module and name, the hand act that warrants it, and the ask of
|
||||
// the operator's Approve it answers (kept in what is said).
|
||||
type RetireArgs struct {
|
||||
Node, Module, Name, Act, Ask string
|
||||
}
|
||||
|
||||
// OwnRetired is what one `retire` did. Never a value.
|
||||
type OwnRetired struct {
|
||||
Secret string `json:"secret"`
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
Name string `json:"name"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
Caller string `json:"caller"`
|
||||
Act string `json:"act"`
|
||||
}
|
||||
|
||||
// fromRetireCallers refuses every caller the vault's list `retire-callers` does not name.
|
||||
func (v *Vault) fromRetireCallers(caller string) error {
|
||||
if !v.RetireCallers.Allows(caller) {
|
||||
if strings.TrimSpace(caller) == "" {
|
||||
caller = stdio.Unproven
|
||||
}
|
||||
return fmt.Errorf("retire is asked by the holder of the seat mesh-config alone, as the vault's list retire-callers "+
|
||||
"names it; %s is not on it (design 32)", caller)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// warrantsRetire reads the hand act back and refuses what it does not warrant for exactly this secret.
|
||||
func (v *Vault) warrantsRetire(caller string, a RetireArgs) error {
|
||||
if strings.TrimSpace(a.Act) == "" {
|
||||
return fmt.Errorf("retire carries the hand act that warrants it (`act`): record it through mesh-hand-acts first (design 32)")
|
||||
}
|
||||
if v.ReadAct == nil {
|
||||
return fmt.Errorf("the vault reads no hand acts, so it retires nothing")
|
||||
}
|
||||
act, err := v.ReadAct(a.Act)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the hand act %s could not be read back from mesh-hand-acts: %w", a.Act, err)
|
||||
}
|
||||
switch {
|
||||
case act == nil:
|
||||
return fmt.Errorf("mesh-hand-acts holds no hand act %s: record one first", a.Act)
|
||||
case act.By != caller:
|
||||
return fmt.Errorf("the hand act %s was recorded by %s, not by %s who asks", a.Act, act.By, caller)
|
||||
case v.Now().Sub(act.At) > HandActWithin:
|
||||
return fmt.Errorf("the hand act %s is older than %s: record a new one", a.Act, HandActWithin)
|
||||
case act.Verb != Seat+"."+RetireVerb:
|
||||
return fmt.Errorf("the hand act %s is %q, not %s.%s: it warrants no retirement", a.Act, act.Verb, Seat, RetireVerb)
|
||||
}
|
||||
var named []string
|
||||
for _, arg := range act.Args {
|
||||
named = append(named, strings.Fields(arg)...)
|
||||
}
|
||||
// The warrant is bound to the node, module and name it was asked for: all three, and no others.
|
||||
if len(named) != 3 || named[0] != a.Node || named[1] != a.Module || named[2] != a.Name {
|
||||
return fmt.Errorf("the hand act %s names %q, not %s %s %s: a warrant retires nothing else",
|
||||
a.Act, strings.Join(named, " "), a.Node, a.Module, a.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetireOwn removes one module's own secret on a node on the warrant of a hand act, and says `own-retired`.
|
||||
func (v *Vault) RetireOwn(caller string, a RetireArgs) (OwnRetired, error) {
|
||||
if err := v.fromRetireCallers(caller); err != nil {
|
||||
return OwnRetired{}, err
|
||||
}
|
||||
secret, err := OwnSecret(a.Node, a.Module, a.Name)
|
||||
if err != nil {
|
||||
return OwnRetired{}, err
|
||||
}
|
||||
if err := v.warrantsRetire(caller, a); err != nil {
|
||||
return OwnRetired{}, err
|
||||
}
|
||||
switch {
|
||||
case Covered(secret) || isLedgersSecret(secret):
|
||||
return OwnRetired{}, fmt.Errorf("%s is a bus account or the tool runner's ledgers, which the vault retires and rewrites with the wants (ADR 0230, ADR 0597 §9), never a retirement of a value no manifest reads", secret)
|
||||
}
|
||||
_, held, err := v.Store.Read(secret)
|
||||
if err != nil {
|
||||
return OwnRetired{}, err
|
||||
}
|
||||
if !held {
|
||||
return OwnRetired{}, fmt.Errorf("the vault holds no secret named %s, so there is nothing to retire", secret)
|
||||
}
|
||||
if err := v.Store.Delete(secret); err != nil {
|
||||
return OwnRetired{}, fmt.Errorf("%s is not retired: %w", secret, err)
|
||||
}
|
||||
out := OwnRetired{Secret: secret, Node: a.Node, Module: a.Module, Name: a.Name, Ask: a.Ask, Caller: caller, Act: a.Act}
|
||||
v.log("retired %s on the warrant of the hand act %s, asked by %s (ask %s)", secret, a.Act, caller, a.Ask)
|
||||
if v.Emit != nil {
|
||||
body := map[string]any{"secret": secret, "node": a.Node, "module": a.Module, "name": a.Name, "ask": a.Ask, "caller": caller}
|
||||
if err := v.Emit(OwnRetiredEvent, body); err != nil {
|
||||
return out, fmt.Errorf("%s is retired, but own-retired was not said: %w", secret, err)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// 774/AC9 (novox/hq ADR 0561 §2, ADR 0230 §15, §18): a wanted account whose key is deleted is retired (its hash taken
|
||||
@@ -80,3 +82,86 @@ func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
|
||||
t.Errorf("after a person's retirement %d keys remain", len(accounts.keys))
|
||||
}
|
||||
}
|
||||
|
||||
// 1966/CAT-V1 (design 32): `retire` removes one module's own secret on a node, and says own-retired, only for a caller on
|
||||
// the vault's list retire-callers who carries a hand act it recorded for this verb naming exactly this node, module and
|
||||
// secret. Without the act, with another name's, with another caller's or an older one, from a caller not on the list, or
|
||||
// for a bus account, nothing is removed and nothing is said; a waiter on rotated or own-replaced is not woken.
|
||||
func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
const caller = "novox.mesh-config"
|
||||
v.RetireCallers = stdio.Callers{"mesh-config"}
|
||||
acts := map[string]*HandActSeen{}
|
||||
v.ReadAct = func(id string) (*HandActSeen, error) { return acts[id], nil }
|
||||
act := func(id, by, verb, what string, age time.Duration) {
|
||||
acts[id] = &HandActSeen{ID: id, By: by, At: v.Now().Add(-age), Verb: verb, Args: []string{what}}
|
||||
}
|
||||
act("ok", caller, "mesh-vault.retire", "g14 dunst token", time.Minute)
|
||||
act("other-name", caller, "mesh-vault.retire", "g14 dunst other", time.Minute)
|
||||
act("other-node", caller, "mesh-vault.retire", "ace dunst token", time.Minute)
|
||||
act("other-caller", "g14.claude-code", "mesh-vault.retire", "g14 dunst token", time.Minute)
|
||||
act("other-verb", caller, "mesh-vault.rotate", "g14 dunst token", time.Minute)
|
||||
act("ledgers", caller, "mesh-vault.retire", "g14 node-tools ledgers", time.Minute)
|
||||
act("old", caller, "mesh-vault.retire", "g14 dunst token", 25*time.Hour)
|
||||
const secret = "own/g14/dunst/token"
|
||||
for _, name := range []string{secret, "own/g14/dunst/other", "own/g14/node-tools/ledgers"} {
|
||||
if _, err := v.keep(name, "a-value-of-"+name, "the test", "g14.dunst", OriginMade); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, caller string
|
||||
args RetireArgs
|
||||
want string
|
||||
}{
|
||||
{"no act", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token"}, "carries the hand act"},
|
||||
{"an act that does not exist", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "nope"}, "holds no hand act"},
|
||||
{"an act for another name", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-name"}, "a warrant retires nothing else"},
|
||||
{"an act for another node", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-node"}, "a warrant retires nothing else"},
|
||||
{"an act of another caller", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-caller"}, "was recorded by g14.claude-code"},
|
||||
{"an act for another verb", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-verb"}, "warrants no retirement"},
|
||||
{"an act older than a day", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "old"}, "older than"},
|
||||
{"a caller not on the list", "g14.claude-code", RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-caller"}, "retire-callers"},
|
||||
{"an unproven caller", stdio.Unproven, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "ok"}, "retire-callers"},
|
||||
{"the tool runner's ledgers", caller, RetireArgs{Node: "g14", Module: "node-tools", Name: "ledgers", Act: "ledgers"}, "bus account or the tool runner's ledgers"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
before := len(v.bus.events)
|
||||
if _, err := v.RetireOwn(tc.caller, tc.args); err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Errorf("answered %v, want a refusal saying %q", err, tc.want)
|
||||
}
|
||||
if _, held, _ := v.Store.Read(secret); !held {
|
||||
t.Error("a refused retire removed the secret")
|
||||
}
|
||||
if n := len(v.bus.events) - before; n != 0 {
|
||||
t.Errorf("a refused retire said %d events", n)
|
||||
}
|
||||
})
|
||||
}
|
||||
before := len(v.bus.events)
|
||||
got, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "ok", Ask: "ret-1"})
|
||||
if err != nil || got.Secret != secret || got.Caller != caller {
|
||||
t.Fatalf("retire answered %+v, %v", got, err)
|
||||
}
|
||||
if _, held, _ := v.Store.Read(secret); held {
|
||||
t.Error("the secret is still held")
|
||||
}
|
||||
if _, held, _ := v.Store.Read("own/g14/dunst/other"); !held {
|
||||
t.Error("another secret of the module was removed")
|
||||
}
|
||||
if n := eventsNamed(v, before, OwnRetiredEvent); n != 1 || len(v.bus.events)-before != 1 {
|
||||
t.Fatalf("said %d own-retired among %d events, want one and no other", n, len(v.bus.events)-before)
|
||||
}
|
||||
if body, _ := v.bus.events[before]["body"].(map[string]any); body["ask"] != "ret-1" || body["caller"] != caller || body["node"] != "g14" ||
|
||||
body["module"] != "dunst" || body["name"] != "token" {
|
||||
t.Errorf("own-retired said %+v", body)
|
||||
}
|
||||
if OwnRetiredEvent == OwnReplacedEvent || OwnRetiredEvent == "rotated" {
|
||||
t.Error("own-retired shares a name with an event a waiter hears")
|
||||
}
|
||||
// Asked again for what is gone: nothing to retire.
|
||||
if _, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "ok"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "nothing to retire") {
|
||||
t.Errorf("a second retire answered %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ type Vault struct {
|
||||
// EnrolCallers are the callers of the enrolment verbs `enrolment-token` and `node-account` and of `sign` for an
|
||||
// enrolment: the seat mesh-enrolment (enrolment.go). Nil: that module alone.
|
||||
EnrolCallers stdio.Callers
|
||||
// RetireCallers are the callers of the seat's verb `retire`: the vault's trusted config value `retire-callers`, the
|
||||
// holder of the seat mesh-config alone (ownretire.go). Nil: nobody.
|
||||
RetireCallers stdio.Callers
|
||||
// TokenBase says where the bus is and its certificate's fingerprint, for the token shown (enrolment.go): nil shows none.
|
||||
TokenBase func() (TokenBase, error)
|
||||
// Logins is the vault's state node-logins (noderotation.go): nil writes none.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"callers": ["mesh-config"]
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
"signed",
|
||||
"rotation-begun",
|
||||
"node-account-ready",
|
||||
"rotation-done"
|
||||
"rotation-done",
|
||||
"own-retired"
|
||||
],
|
||||
"invokes": [
|
||||
"seat:mesh-hand-acts.hand-acts"
|
||||
@@ -142,6 +143,19 @@
|
||||
"mode": "0700",
|
||||
"owner": "mesh-vault",
|
||||
"unknown": "program"
|
||||
},
|
||||
{
|
||||
"id": "retire-callers",
|
||||
"trusted": true,
|
||||
"type": "file",
|
||||
"path": "${dir:state}/retire-callers.json",
|
||||
"mode": "0600",
|
||||
"owner": "mesh-vault",
|
||||
"merge": "json",
|
||||
"template": "files/retire-callers.json",
|
||||
"takes-config": [
|
||||
"retire-callers"
|
||||
]
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
@@ -163,8 +177,14 @@
|
||||
"MESH_VAULT_STORE": "${dir:store}",
|
||||
"MESH_VAULT_STATE": "${dir:state}",
|
||||
"MESH_VAULT_SECRET_ASK_CALLERS": "node-tools",
|
||||
"MESH_VAULT_ENROLMENT_CALLERS": "mesh-enrolment"
|
||||
"MESH_VAULT_ENROLMENT_CALLERS": "mesh-enrolment",
|
||||
"MESH_VAULT_RETIRE_CALLERS": "${dir:state}/retire-callers.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -211,8 +231,19 @@
|
||||
"node-account",
|
||||
"rotate-node",
|
||||
"node-account-confirm",
|
||||
"abandon"
|
||||
"abandon",
|
||||
"retire"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"config": {
|
||||
"retire-callers": {
|
||||
"type": "list",
|
||||
"description": "Who may call the seat's verb retire, which removes one module's own secret on a node because no manifest reads it any more: a module's name (on every machine), <machine>.<module>, or person.<name>. The holder of the seat mesh-config alone by default (design 32); the verb also needs a hand act the caller recorded for exactly that node, module and secret.",
|
||||
"trusted": true,
|
||||
"default": [
|
||||
"mesh-config"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user