Let the generic command verb only read, and keep keys and tokens at the terminal
Review found a chain through the command verb: set the operator's key to one the caller holds, rotate secrets so they are sealed to it too, read the sealed copies, open them. Whoever may call a verb includes agents (hq ADR 0266), so command now runs an allow list of reading forms, and operator, identity, token, broker, api, licence and every secret command but rotate are refused through any verb.
This commit is contained in:
@@ -252,9 +252,14 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
// settings verb is where what a verb may not set is refused, and one route is one set of words.
|
||||
// The command refuses places and accesses through any verb as well; this says so before it runs.
|
||||
if argv[0] == "settings" && slices.ContainsFunc(argv[1:], func(w string) bool { return w == "set" || w == "clear" }) {
|
||||
return nil, errors.New("settings are set and cleared through the settings verb, not the generic " +
|
||||
"command; and places and accesses only at the controller's terminal (novox/hq issue 339). " +
|
||||
"Nothing was done")
|
||||
return nil, &heldAtTheTerminal{msg: "settings are set and cleared through the settings verb, not the " +
|
||||
"generic command; and places and accesses only at the controller's terminal (novox/hq issue 339). " +
|
||||
"Nothing was done"}
|
||||
}
|
||||
// The generic verb only reads (novox/hq ADR 0266): what writes has a named verb that composes its own
|
||||
// line, or is the operator's at the controller's terminal.
|
||||
if err := commandReads(argv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The generic verb is no way round the hand-act log (novox/hq to-be 45 §7): a repair through
|
||||
// it says why, as it would through its own verb.
|
||||
@@ -1074,7 +1079,8 @@ func seatToolHandlers() (map[string]link.ToolHandler, []string, error) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := argvFor(verb, sampleArguments(v)); err != nil {
|
||||
var policy *heldAtTheTerminal
|
||||
if _, err := argvFor(verb, sampleArguments(v)); err != nil && !errors.As(err, &policy) {
|
||||
// **A row ahead of this binary is not a reason to go silent.**
|
||||
//
|
||||
// The row is the store's and a control plane follows it (novox/hq ADR 0154), so a verb
|
||||
@@ -1339,13 +1345,111 @@ func seatAnnouncement(handlers map[string]link.ToolHandler) micro.Info {
|
||||
// nodeReads are the `node` subcommands a verb may run: the ones that only read.
|
||||
var nodeReads = map[string]bool{"list": true, "show": true}
|
||||
|
||||
// flagsOnly says a command line's rest names no subcommand: empty, or beginning with a flag. For a command
|
||||
// with no subcommands every word is a flag, its value or a name it reads.
|
||||
func flagsOnly(rest []string) bool { return len(rest) == 0 || strings.HasPrefix(rest[0], "-") }
|
||||
|
||||
// subIn says the rest begins with one of these subcommands.
|
||||
func subIn(rest []string, subs ...string) bool {
|
||||
return len(rest) > 0 && slices.Contains(subs, rest[0])
|
||||
}
|
||||
|
||||
// commandReadForms are the command lines the generic `command` verb may run (novox/hq ADR 0266): **an allow
|
||||
// list of the ones that only read**, judged command by command. Anything else — every command that writes a
|
||||
// record, sends, builds, issues an account or a token, sets a key, accepts, rotates, recovers or exports a
|
||||
// secret — is refused, and a command added later is refused until it is judged a read. Writing has its named
|
||||
// verbs, which compose their own lines and are judged by terminalOnly; the rest is the operator's at the
|
||||
// controller's terminal.
|
||||
var commandReadForms = map[string]func(rest []string) bool{
|
||||
"status": flagsOnly, "version": flagsOnly, "help": flagsOnly, "seats": flagsOnly, "healers": flagsOnly,
|
||||
"hand-acts": flagsOnly, "durations": flagsOnly, "collection": flagsOnly, "images": flagsOnly,
|
||||
"artifacts": flagsOnly, "data": flagsOnly, "builds": flagsOnly, "queue": flagsOnly,
|
||||
// `plan <node>` previews a node's declaration; it sends nothing.
|
||||
"plan": func([]string) bool { return true },
|
||||
// `plans` lists and `plans <id>` shows one; `plans stop|close|go` acts.
|
||||
"plans": func(r []string) bool { return !subIn(r, "stop", "close", "go") },
|
||||
// `doctor` answers the last run, `probes` and `signals` describe; `doctor run` runs.
|
||||
"doctor": func(r []string) bool { return flagsOnly(r) || subIn(r, "probes", "signals") },
|
||||
"conditions": func(r []string) bool { return flagsOnly(r) || subIn(r, "list", "show", "history") },
|
||||
"node": func(r []string) bool { return subIn(r, "list", "show") },
|
||||
"module": func(r []string) bool { return subIn(r, "list") },
|
||||
"settings": func(r []string) bool { return subIn(r, "show", "preferences") },
|
||||
"retire": func(r []string) bool { return subIn(r, "list") },
|
||||
"cleanup": func(r []string) bool { return subIn(r, "list") },
|
||||
"delivery": func(r []string) bool { return subIn(r, "plan", "walks") },
|
||||
// `bus` alone says the bus's step; `bus upgrade` takes one.
|
||||
"bus": func(r []string) bool { return len(r) == 0 },
|
||||
// `mirrors` lists; --record and --confirm keep a mirror.
|
||||
"mirrors": func(r []string) bool {
|
||||
return flagsOnly(r) && !slices.ContainsFunc(r, func(w string) bool {
|
||||
return w == "--record" || w == "-record" || strings.HasPrefix(w, "--record=") || strings.HasPrefix(w, "-record=") ||
|
||||
w == "--confirm" || w == "-confirm" || strings.HasPrefix(w, "--confirm=")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// heldAtTheTerminal is a refusal of policy (novox/hq ADR 0266): the verb is known and served, and this line is
|
||||
// the operator's at the controller's terminal. Never read as a verb this binary is behind on.
|
||||
type heldAtTheTerminal struct{ msg string }
|
||||
|
||||
func (e *heldAtTheTerminal) Error() string { return e.msg }
|
||||
|
||||
func terminalRefusal(format string, args ...any) error {
|
||||
return &heldAtTheTerminal{fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// commandReads refuses a line the generic verb may not run, saying what it may.
|
||||
func commandReads(argv []string) error {
|
||||
if read, ok := commandReadForms[argv[0]]; ok && read(argv[1:]) {
|
||||
return nil
|
||||
}
|
||||
return terminalRefusal("%q is not a reading command, and the generic command verb only reads (novox/hq ADR 0266): "+
|
||||
"whoever may call a verb includes agents, and a line that writes, issues, sets a key or reveals a secret "+
|
||||
"would be theirs to run. Use the named verb for it, or run it at the controller's terminal. The verb may "+
|
||||
"run: %s. Nothing was done", strings.Join(argv, " "), commandReadNames())
|
||||
}
|
||||
|
||||
func commandReadNames() string {
|
||||
names := make([]string, 0, len(commandReadForms))
|
||||
for n := range commandReadForms {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return strings.Join(names, ", ") + " (each in its reading forms)"
|
||||
}
|
||||
|
||||
// terminalOnlyCommands are the commands no verb runs, whatever composed them (novox/hq ADR 0266): they set
|
||||
// the operator's key, issue a credential or a token that is answered to the caller, or accept, recover or
|
||||
// export a secret. Their answers or effects hand whoever calls them what the runtime's account holds.
|
||||
var terminalOnlyCommands = map[string]string{
|
||||
"operator": "the operator's key and credential",
|
||||
"identity": "the mesh's identity keys",
|
||||
"token": "a token a machine joins with, answered to the caller",
|
||||
"broker": "the bus's accounts",
|
||||
"api": "the controller's API keys",
|
||||
"licence": "the licences' secrets",
|
||||
}
|
||||
|
||||
// terminalOnly refuses, through any verb, a command that is the operator's at the controller's terminal
|
||||
// alone (novox/hq ADR 0266). **Every `node` subcommand that is not a read**: `node account` and
|
||||
// `node agent-account` above all. Whoever may call a verb includes agents, and an agent that named itself
|
||||
// the operator account, or cleared the agent account, would have the next send grant it root through the
|
||||
// sudo module's rule. An allow list, so a subcommand added later is refused until it is judged a read.
|
||||
func terminalOnly(argv []string) error {
|
||||
if len(argv) == 0 || argv[0] != "node" {
|
||||
if len(argv) == 0 {
|
||||
return nil
|
||||
}
|
||||
if what, kept := terminalOnlyCommands[argv[0]]; kept {
|
||||
return terminalRefusal("%s is run at the controller's terminal only, never through a verb: it holds %s, and "+
|
||||
"whoever may call a verb includes agents (novox/hq ADR 0266). Nothing was done", argv[0], what)
|
||||
}
|
||||
// Of a secret's commands only rotation, which seals the new value to the machine that uses it.
|
||||
if argv[0] == "secret" && (len(argv) < 2 || argv[1] != "rotate") {
|
||||
return terminalRefusal("secret %s is run at the controller's terminal only, never through a verb: accepting, "+
|
||||
"recovering or exporting a secret hands it to whoever asks, and that includes agents (novox/hq ADR "+
|
||||
"0266). Nothing was done", strings.Join(argv[1:], " "))
|
||||
}
|
||||
if argv[0] != "node" {
|
||||
return nil
|
||||
}
|
||||
if len(argv) > 1 && nodeReads[argv[1]] {
|
||||
@@ -1355,7 +1459,7 @@ func terminalOnly(argv []string) error {
|
||||
if len(argv) > 1 {
|
||||
sub += " " + argv[1]
|
||||
}
|
||||
return fmt.Errorf("%s is run at the controller's terminal only, never through a verb: a node's accounts "+
|
||||
return terminalRefusal("%s is run at the controller's terminal only, never through a verb: a node's accounts "+
|
||||
"decide who may become root on it (novox/hq ADR 0266). A verb may run node list and node show. "+
|
||||
"Nothing was done", sub)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user