A change's line the router would refuse is named with its value withheld, or refused at propose (the review of #304)
mesh/merge-gate head deef9421a276 merged into trunk cef22a25264b — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head deef9421a276 merged into trunk cef22a25264b — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance delivered: every member is delivered
mesh/merge-gate head deef9421a276 merged into trunk cef22a25264b — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head deef9421a276 merged into trunk cef22a25264b — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance delivered: every member is delivered
{a:1, b:2, c:3, password:hunter2} was proposed with "+ password: hunter2" in its Details: the router
withheld the line unnamed and Approve set a value the operator never saw (ADR 0347 §1, §3, §4). Each line is
now judged as the router judges it, masked under the content rule and whole under the secret rule; a line
refused is said "+ password (value withheld)", and any line the router would still refuse is refused at
propose in the controller's words, never echoing a secret-shaped key. SDK at mesh-sdk 5933eb7 (no two asks
digest the same).
This commit is contained in:
@@ -277,6 +277,17 @@ func (p settingsProposal) change(machines []string, exact bool) ([]string, []str
|
||||
lines = append(lines, line{k, fmt.Sprintf("- %s (was: %s)", k, say(was[k]))})
|
||||
}
|
||||
}
|
||||
// **Each line is judged as the router will judge it** (the review of #304, ADR 0347 §1 and §3): a line the
|
||||
// content rule (masked) or the secret rule (whole) would refuse — a value given to a secret's name, say
|
||||
// "+ password: hunter2" — is said with its key still named and its value withheld, never left for the
|
||||
// router to withhold whole and unnamed. A line refused even so is the propose's refusal (judgeLines).
|
||||
for i, l := range lines {
|
||||
if lineLeaves(l.text, exact, machines) {
|
||||
continue
|
||||
}
|
||||
lines[i].text = l.text[:2] + l.key + " (value withheld)"
|
||||
whole = false
|
||||
}
|
||||
all := make([]string, len(lines))
|
||||
for i, l := range lines {
|
||||
all[i] = l.text
|
||||
@@ -310,6 +321,37 @@ func (p settingsProposal) change(machines []string, exact bool) ([]string, []str
|
||||
return out, all, whole
|
||||
}
|
||||
|
||||
// lineLeaves says whether a line may leave the mesh as the router judges it: under the content rule for the masked
|
||||
// form, the secret rule alone for the whole one.
|
||||
func lineLeaves(line string, exact bool, machines []string) bool {
|
||||
if exact {
|
||||
_, ok := outward.Secret(line, machines...)
|
||||
return ok
|
||||
}
|
||||
_, ok := outward.Check(line, machines...)
|
||||
return ok
|
||||
}
|
||||
|
||||
// judgeLines is the first line of an ask's details the router would refuse, or withhold, and why: masked under
|
||||
// the content rule line by line, whole under the secret rule. Empty when every line may leave.
|
||||
func judgeLines(details string, exact bool, machines []string) (string, string) {
|
||||
for _, line := range strings.Split(details, "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
if exact {
|
||||
if refusal, ok := outward.Secret(line, machines...); !ok {
|
||||
return line, refusal.String()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if refusal, ok := outward.Check(line, machines...); !ok {
|
||||
return line, refusal.String()
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func layerKeys(m map[string]any) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
@@ -513,6 +555,17 @@ func (pr proposer) propose(ctx context.Context, in proposeInput) (string, error)
|
||||
return refuse("the change shown whole would carry %s, which may not leave the mesh on any channel; %s",
|
||||
refusal, atTheTerminalInstead(in))
|
||||
}
|
||||
// So are the details (ADR 0347 §1, §3): every line the router would withhold from the note, or refuse the ask
|
||||
// for, is refused here, in the controller's own words, naming the line's key — Approve never sets a value the
|
||||
// operator was shown nothing of.
|
||||
if line, why := judgeLines(q.Details, false, machines); line != "" {
|
||||
return refuse("the change's line %q would carry %s, which the operator's note may not show; %s",
|
||||
keyOfLine(line), why, atTheTerminalInstead(in))
|
||||
}
|
||||
if line, why := judgeLines(q.WholeDetails, true, machines); line != "" {
|
||||
return refuse("the change's line %q shown whole would carry %s, which may not leave the mesh on any channel; %s",
|
||||
keyOfLine(line), why, atTheTerminalInstead(in))
|
||||
}
|
||||
// Fail closed, before anything is kept: no router, no grant, no channel means no ask.
|
||||
if pr.routerHere != nil {
|
||||
here, err := pr.routerHere(ctx)
|
||||
@@ -902,3 +955,21 @@ func pushTo(node string) string {
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// keyOfLine is what a refusal names of a line: the key of a change's line ("+ key: …"), or the line's first words.
|
||||
func keyOfLine(line string) string {
|
||||
if len(line) > 2 && strings.ContainsRune("+~-=", rune(line[0])) && line[1] == ' ' {
|
||||
rest := line[2:]
|
||||
if i := strings.IndexAny(rest, ": "); i > 0 {
|
||||
rest = rest[:i]
|
||||
}
|
||||
if _, ok := outward.Secret(rest); !ok {
|
||||
return "whose key is shaped like a secret"
|
||||
}
|
||||
return rest
|
||||
}
|
||||
if r := []rune(line); len(r) > 40 {
|
||||
return string(r[:40]) + "…"
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
@@ -919,3 +919,61 @@ func TestEveryKeyAndItsWholeValueReachTheOperator(t *testing.T) {
|
||||
t.Errorf("the cut is not said, or the note lacks the whole:\n%s\n--\n%s", q2.Whole, q2.WholeDetails)
|
||||
}
|
||||
}
|
||||
|
||||
// The review of #304: a value given to a secret's name, past the second key, is said with its key named and its
|
||||
// value withheld, in the note on every channel; the router has nothing left to withhold unnamed or refuse. A line
|
||||
// the router would refuse even so is refused at propose, in the controller's words.
|
||||
func TestASecretNamedKeyIsNamedAndItsValueWithheld(t *testing.T) {
|
||||
r := newProposerRig(t)
|
||||
values := map[string]any{"a": "1", "b": "2", "c": "3", "password": "hunter2"}
|
||||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: values}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q := r.askSent(t)
|
||||
for _, form := range []string{q.Details, q.WholeDetails, q.Explanation, q.Whole} {
|
||||
if strings.Contains(form, "hunter2") {
|
||||
t.Errorf("the value reaches the phone:\n%s", form)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(q.Details, "+ password (value withheld)") {
|
||||
t.Errorf("the key is not named with its value withheld:\n%s", q.Details)
|
||||
}
|
||||
if q.WholeDetails != "" && !strings.Contains(q.WholeDetails, "+ password (value withheld)") {
|
||||
t.Errorf("the whole details do not name the key:\n%s", q.WholeDetails)
|
||||
}
|
||||
if line, why := judgeLines(q.Details, false, nil); line != "" {
|
||||
t.Errorf("the router would withhold %q: %s", line, why)
|
||||
}
|
||||
if line, why := judgeLines(q.WholeDetails, true, nil); line != "" {
|
||||
t.Errorf("the router would refuse %q: %s", line, why)
|
||||
}
|
||||
if !strings.HasSuffix(q.Explanation, "\nand 2 more, each in the note: c, password") {
|
||||
t.Errorf("the glance:\n%s", q.Explanation)
|
||||
}
|
||||
// A key shaped like a secret cannot be named on any channel: refused at propose, in the controller's words, and
|
||||
// the key itself not echoed.
|
||||
r2 := newProposerRig(t)
|
||||
secretKey := "ghp_abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
_, err := r2.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop",
|
||||
values: map[string]any{"a": "1", "b": "2", secretKey: "3"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "Nothing was proposed") || strings.Contains(err.Error(), secretKey) {
|
||||
t.Errorf("a secret-shaped key: %v", err)
|
||||
}
|
||||
if len(r2.sent) != 0 {
|
||||
t.Errorf("the refused proposal was asked")
|
||||
}
|
||||
}
|
||||
|
||||
// keyOfLine names a change's key, and never one shaped like a secret.
|
||||
func TestARefusalNamesTheKeyAndNeverASecret(t *testing.T) {
|
||||
for line, want := range map[string]string{
|
||||
"+ password (value withheld)": "password",
|
||||
"~ shares: ‹path› (was: none)": "shares",
|
||||
"+ ghp_abcdefghijklmnopqrstuvwxyz0123456789: 1": "whose key is shaped like a secret",
|
||||
"Proposed by g14 at 15:04.": "Proposed by g14 at 15:04.",
|
||||
} {
|
||||
if got := keyOfLine(line); got != want {
|
||||
t.Errorf("%q: %q, want %q", line, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user