mesh-cli: give a terminal line the standard input mesh-cli carried, refuse it to any other, keep it nowhere
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.62s)
mesh/delivery-group group feat/a-terminal-line-takes-standard-input rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.62s)
mesh/delivery-group group feat/a-terminal-line-takes-standard-input rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
A secret given at the controller's terminal through mesh-cli (secret accept … --from -) never reached the line: every line ran with no standard input. A line that runs as the terminal now reads what mesh-cli carried (at most 64 KiB); an ordinary call carrying any is refused and nothing runs; the calls record keeps only that some was given, the journal and the answer nothing of it.
This commit is contained in:
@@ -461,7 +461,7 @@ func kept(args json.RawMessage) json.RawMessage {
|
||||
for k, v := range given {
|
||||
s, isString := v.(string)
|
||||
switch {
|
||||
case k == "values" || k == "secret":
|
||||
case k == "values" || k == "secret" || k == "stdin":
|
||||
out[k] = "(given, not kept)"
|
||||
case k == "line":
|
||||
// A mesh-cli line (ADR 0272): its command word, never the rest, which may carry settings.
|
||||
|
||||
@@ -41,6 +41,30 @@ type CLIAsked struct {
|
||||
// (`session-<id>.scope` under the account's own slice), or empty: a service, a user unit. Only a login session is
|
||||
// the terminal (novox/hq ADR 0272).
|
||||
Session string `json:"session,omitempty"`
|
||||
// Stdin is what mesh-cli's standard input held, at most CLIMaxStdin: given to a line that runs as the terminal,
|
||||
// as its standard input, and refused with any other (novox/hq ADR 0259 §10, ADR 0272). It is kept nowhere: not in
|
||||
// the calls record (cliRecorded), not in the journal, not in the answer.
|
||||
Stdin []byte `json:"stdin,omitempty"`
|
||||
}
|
||||
|
||||
// CLIMaxStdin bounds the standard input a mesh-cli line carries (mesh-sdk go/cli MaxStdin).
|
||||
const CLIMaxStdin = 64 << 10
|
||||
|
||||
// cliRecorded is the request as the calls record keeps it: everything but the standard input, which the record
|
||||
// never holds in any form. The line itself is cut to its command word by the record's own rule (kept).
|
||||
func cliRecorded(raw json.RawMessage) json.RawMessage {
|
||||
var m map[string]json.RawMessage
|
||||
if json.Unmarshal(raw, &m) != nil {
|
||||
return json.RawMessage(`{}`)
|
||||
}
|
||||
if _, given := m["stdin"]; given {
|
||||
// Said as given, under a key of its own: the record still reads as the request it was (followCLI decodes
|
||||
// it), and holds nothing of the input.
|
||||
delete(m, "stdin")
|
||||
m["stdin-given"] = json.RawMessage(`true`)
|
||||
}
|
||||
body, _ := json.Marshal(m)
|
||||
return body
|
||||
}
|
||||
|
||||
// CLIAnswer is what the controller answers, as the `result` of a call's answer: what the command printed, how it
|
||||
@@ -135,6 +159,10 @@ func (b OverNATS) answerCLI(msg *nats.Msg, calls *CallLog, slots chan struct{},
|
||||
case len(asked.Line) == 0:
|
||||
respond(cliEnvelope(CLIRefusal("the request names no command, so nothing ran")))
|
||||
return
|
||||
case len(asked.Stdin) > CLIMaxStdin:
|
||||
respond(cliEnvelope(CLIRefusal(fmt.Sprintf("standard input of more than %d bytes is not taken, so nothing ran",
|
||||
CLIMaxStdin))))
|
||||
return
|
||||
}
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
@@ -145,7 +173,7 @@ func (b OverNATS) answerCLI(msg *nats.Msg, calls *CallLog, slots chan struct{},
|
||||
return
|
||||
}
|
||||
limit := b.Conn.MaxPayload()
|
||||
calls.serveCallWithin(CLISeat, node, msg.Data, msg.Reply, func(ctx context.Context, _ json.RawMessage) (any, error) {
|
||||
calls.serveCallWithin(CLISeat, node, cliRecorded(msg.Data), msg.Reply, func(ctx context.Context, _ json.RawMessage) (any, error) {
|
||||
return fitCLI(handle(ctx, node, asked), limit-4096), nil
|
||||
}, msg.Respond, limit, logger)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ func TestTheMeshCLIRequestAndAnswerKeepTheirFieldNames(t *testing.T) {
|
||||
if got := keysIn(t, body); got != "cut exit refused stderr stdout terminal why" {
|
||||
t.Fatalf("the answer's fields are %q", got)
|
||||
}
|
||||
body, _ = json.Marshal(CLIAsked{Line: []string{"secret"}, Account: "a", UID: 1, Stdin: []byte("x")})
|
||||
if got := keysIn(t, body); got != "account line stdin uid" {
|
||||
t.Fatalf("a request with standard input has the fields %q", got)
|
||||
}
|
||||
body, _ = json.Marshal(CLIAsked{Follow: "call-1", Account: "a", UID: 1})
|
||||
if got := keysIn(t, body); got != "account follow uid" {
|
||||
t.Fatalf("a follow's fields are %q", got)
|
||||
@@ -267,3 +271,26 @@ func TestTheWithholdingTestsHoldSomething(t *testing.T) {
|
||||
t.Fatalf("a record carrying the answer is not found carrying it: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §10: mesh-cli's standard input — a secret given at the terminal — is never in the calls record,
|
||||
// in any form, whichever way the request reaches it.
|
||||
func TestAMeshCLIRecordNeverKeepsItsStandardInput(t *testing.T) {
|
||||
secret := "123456789:AAEhBOweik6ad9r_QxGivenAtTheTerminal"
|
||||
raw, _ := json.Marshal(CLIAsked{Line: []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
|
||||
Account: "operator", UID: 1000, Stdin: []byte(secret)})
|
||||
for name, got := range map[string]json.RawMessage{"as answerCLI records it": kept(cliRecorded(raw)),
|
||||
"as the record alone would keep it": kept(raw)} {
|
||||
if carries(got, secret) || strings.Contains(string(got), base64.StdEncoding.EncodeToString([]byte(secret))[:20]) {
|
||||
t.Fatalf("%s, the record keeps %s", name, got)
|
||||
}
|
||||
if !strings.Contains(string(got), "secret") || !strings.Contains(string(got), "given, not kept") &&
|
||||
!strings.Contains(string(got), "stdin-given") {
|
||||
t.Fatalf("%s, the record does not say a line was asked with standard input: %s", name, got)
|
||||
}
|
||||
}
|
||||
// The record still reads as the request, so the asker can follow its call.
|
||||
var asked CLIAsked
|
||||
if err := json.Unmarshal(kept(cliRecorded(raw)), &asked); err != nil || asked.Account != "operator" || len(asked.Stdin) > 0 {
|
||||
t.Fatalf("the recorded request reads as %+v (%v)", asked, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user