diff --git a/modules/mesh-vault/README.md b/modules/mesh-vault/README.md index a64d59c1..102a21dc 100644 --- a/modules/mesh-vault/README.md +++ b/modules/mesh-vault/README.md @@ -218,7 +218,9 @@ The replaced value is kept sealed as the record's `previous` (dropped by any lat `secret-changed` and `own-replaced` are said, and the act is in `secret_handed` (`own-replaced`, `own-same`, `own-take-refused`). A covered bus account so replaced has its hash written again. `own_drift` lists, from every node's latest shadow answer (`.node` in `node-engine.have`), each own secret whose node says another value, -with what the vault holds and whether the node's ask would be taken now. +with what the vault holds and whether the node's ask would be taken now. **The switch** is the vault's: a person +switches every machine's asks off or on with `own_take_switch` (`on`, `why`), kept in the vault's store over a +restart; it is on until switched off, and `own_drift` shows it. ## Events diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index 5e41e511..9edc92fa 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -415,6 +415,18 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { if _, err := take("node.anchor", pg, "a-value-older-than-the-vaults"); err == nil || !strings.Contains(err.Error(), "less than") { t.Errorf("a running value over one the vault just took: %v", err) } + // Switched off by a person, every ask is refused, saying who and why; on again, taken again. + later += time.Hour + if _, err := v.SwitchOwnTake("jochen", false, "the test"); err != nil { + t.Fatal(err) + } + if _, err := take("node.anchor", pg, "after-the-switch"); err == nil || !strings.Contains(err.Error(), "switched off (by jochen") { + t.Errorf("an ask while switched off: %v", err) + } + if _, err := v.SwitchOwnTake("jochen", true, "the test"); err != nil { + t.Fatal(err) + } + later -= time.Hour // own_drift reads it from the node's latest shadow answer, mesh-wide. answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{ Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "needs-superuser-password", diff --git a/modules/mesh-vault/cmd/mesh-vault/owntake.go b/modules/mesh-vault/cmd/mesh-vault/owntake.go index c05ab440..9c59b3b2 100644 --- a/modules/mesh-vault/cmd/mesh-vault/owntake.go +++ b/modules/mesh-vault/cmd/mesh-vault/owntake.go @@ -22,8 +22,11 @@ package main // (accounts.go), so the bus-accounts key is of the value the bus runs. import ( + "encoding/json" "errors" "fmt" + "os" + "path/filepath" "sort" "strings" "time" @@ -125,10 +128,12 @@ func (v *Vault) OwnTakeOpen(caller string, of OwnTakeOf) (Opened, error) { if err != nil { return Opened{}, err } - refused := "" - if !held { + refused := v.ownTakeOff() + switch { + case refused != "": + case !held: refused = fmt.Sprintf("the vault holds no secret named %s, so there is nothing to replace; nothing is taken", secret) - } else { + default: refused = ownTakeRefusal(rec, v.Now()) } if refused != "" { @@ -237,9 +242,10 @@ type DriftNode struct { // Drift is own_drift's answer. type Drift struct { - Secrets []DriftRow `json:"secrets"` - Nodes []DriftNode `json:"nodes"` - Note string `json:"note"` + Switch OwnTakeSwitch `json:"switch"` + Secrets []DriftRow `json:"secrets"` + Nodes []DriftNode `json:"nodes"` + Note string `json:"note"` } // OwnDrift reads every node's own have key (`.node`) and lists, from its latest answers to the placement @@ -303,6 +309,10 @@ func (v *Vault) OwnDrift() (Drift, error) { names = append(names, n) } sort.Strings(names) + if out.Switch, err = v.Store.OwnTakeSwitch(); err != nil { + return Drift{}, err + } + off := v.ownTakeOff() for _, n := range names { r := rows[n] sort.Strings(r.Groups) @@ -318,9 +328,70 @@ func (v *Vault) OwnDrift() (Drift, error) { r.Previous = rec.Previous.Generation } r.Refused = ownTakeRefusal(rec, v.Now()) + if off != "" { + r.Refused = off + } r.Takeable = r.Refused == "" } out.Secrets = append(out.Secrets, *r) } return out, nil } + +// ---- own_take_switch ---------------------------------------------------------------------------------------- + +// OwnTakeSwitch is whether own-take-running takes values, who switched it last, when and why. On until a person +// switches it off; kept in the vault's own store, so it holds over a restart. +type OwnTakeSwitch struct { + On bool `json:"on"` + By string `json:"by,omitempty"` + At time.Time `json:"at,omitzero"` + Why string `json:"why,omitempty"` +} + +func (s *Store) ownTakeSwitchPath() string { return filepath.Join(s.dir, "own-take-running.json") } + +// OwnTakeSwitch is the switch as kept: on when nothing was ever switched. +func (s *Store) OwnTakeSwitch() (OwnTakeSwitch, error) { + raw, err := os.ReadFile(s.ownTakeSwitchPath()) + if errors.Is(err, os.ErrNotExist) { + return OwnTakeSwitch{On: true}, nil + } + if err != nil { + return OwnTakeSwitch{}, err + } + var sw OwnTakeSwitch + if err := json.Unmarshal(raw, &sw); err != nil { + return OwnTakeSwitch{}, fmt.Errorf("the switch of %s is not readable: %w", OwnTakeRunningVerb, err) + } + return sw, nil +} + +// SwitchOwnTake switches own-take-running on or off, a person's act with why; the caller is recorded as the bus +// proved it. +func (v *Vault) SwitchOwnTake(caller string, on bool, why string) (OwnTakeSwitch, error) { + why = strings.TrimSpace(why) + if why == "" || len(why) > 300 || strings.ContainsAny(why, "\r\n\x00") { + return OwnTakeSwitch{}, errors.New("say why in one short line; nothing was switched") + } + sw := OwnTakeSwitch{On: on, By: someoneOr(caller, stdio.Unproven), At: v.Now().UTC(), Why: why} + raw, _ := json.MarshalIndent(sw, "", " ") + if err := writeWhole(v.Store.ownTakeSwitchPath(), append(raw, '\n')); err != nil { + return OwnTakeSwitch{}, err + } + v.log("%s is switched %s by %s: %s", OwnTakeRunningVerb, map[bool]string{true: "on", false: "off"}[on], sw.By, why) + return sw, nil +} + +// ownTakeOff says why own-take-running takes nothing now, or "". +func (v *Vault) ownTakeOff() string { + sw, err := v.Store.OwnTakeSwitch() + if err != nil { + return err.Error() + "; nothing is taken" + } + if !sw.On { + return fmt.Sprintf("%s is switched off (by %s at %s: %s); nothing is taken", OwnTakeRunningVerb, sw.By, + sw.At.Format(time.RFC3339), sw.Why) + } + return "" +} diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index f1397ceb..e7ff1b4f 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -218,6 +218,24 @@ func definitions(env *Env) []toolDef { return v.OwnTakeOpen(caller, OwnTakeOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), Secret: arg(a, "secret"), Why: arg(a, "why")}) }}, + {Name: "own_take_switch", + Description: "Switch own-take-running on or off for every machine, a person's act with why (on until " + + "switched off; kept over a restart). While off, every machine's ask is refused, saying who switched it " + + "off and why. own_drift shows the switch.", + Input: map[string]any{"type": "object", "required": []string{"on", "why"}, "properties": map[string]any{ + "on": map[string]any{"type": "boolean", "description": "true takes running values, false takes none"}, + "why": str("why, in one line")}}, + run: func(caller string, a map[string]any) (any, error) { + v, err := env.ready() + if err != nil { + return nil, err + } + on, ok := a["on"].(bool) + if !ok { + return nil, errors.New("say on: true or false; nothing was switched") + } + return v.SwitchOwnTake(caller, on, arg(a, "why")) + }}, {Name: "own_drift", Description: "Every own secret whose node's latest shadow answer says the vault holds another value than runs " + "there (the masked word \"another value\", issue 1126), for all nodes in one call: the secret, the " + diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 6872935d..12a50350 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -165,7 +165,8 @@ "provisioner_delete", "vault_status", "vault_health", - "own_drift" + "own_drift", + "own_take_switch" ], "claims": [ {