Show what a desk's secret prompt is for, and declare mesh-mail's (issue 789)

The desk's prompt named only the module and the secret's key, so the operator
asking for one of mesh-mail's mail-credential-* accounts could not tell which
one. node-launcher.secret (modules/rofi) now shows the controller's optional
what as its own line under the module and secret names, escaped as text so
the catalogue's words never reach the prompt as markup; omitted as before
when none is declared. mesh-mail's mail-credential-* family now declares that
line with the long-form own-secrets shape the controller's manifest parser
reads (novox/hq issue 789 part b; part a landed in mesh-controller).
This commit is contained in:
jochen
2026-10-15 14:28:45 +02:00
parent 13cd710071
commit 8c60604eb7
3 changed files with 93 additions and 4 deletions
+2 -1
View File
@@ -22,7 +22,8 @@
"own-secrets": {
"mail-credential-*": {
"path": "${dir:state}/mail-credential-*.secret",
"issued-by": "outside"
"issued-by": "outside",
"prompt": "the password of the mail account {member}"
}
},
"tools": [
+33 -3
View File
@@ -46,6 +46,13 @@ var aName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
// break, no markup, no punctuation a sentence is written with — so the prompt's words stay this holder's.
var aCaller = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/@ -]{0,79}$`)
// escapeMarkup is `what` shown as text, never markup (novox/hq issue 789): the window's message is read with
// pango markup on, so a catalogue's words reach it with the characters pango gives meaning to turned to their
// entities first — never interpreted, whatever a module's manifest holds.
var escapeMarkupReplacer = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
func escapeMarkup(s string) string { return escapeMarkupReplacer.Replace(s) }
// secretQuestion reads the call. The prompt is this holder's words, written from the module, the secret and the
// machine (the review of 2026-10-09, M4): it says the mesh's controller asks, which the bus makes true — it
// denies these subjects to every principal but the controller — and a caller's own words are refused, so
@@ -81,14 +88,37 @@ func secretQuestion(args map[string]any) (SecretQuestion, error) {
}
askedBy = v
}
// What the secret is for, in the catalogue's own words (novox/hq issue 789): the controller's own line, read
// from the secret's declared `prompt` and never a word the caller chose. One line, so it is shown under the
// module and secret names as its own; its characters are never markup, whatever the catalogue holds.
what := ""
if raw, there := args["what"]; there {
v, err := text(args, "what", true)
if err != nil {
return q, err
}
if strings.ContainsAny(v, "\n\r") {
return q, fmt.Errorf("what %q holds more than one line", raw)
}
what = v
}
q.Prompt = names["secret"] + " for " + names["module"]
q.Message = fmt.Sprintf("The mesh's controller asks for %s, the own secret of %s on %s.", names["secret"],
names["module"], names["node"])
if askedBy != "" {
q.Message += fmt.Sprintf(" It was asked for by %s.", askedBy)
if what != "" {
q.Message += "\nFor: " + escapeMarkup(what)
}
q.Message += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " +
tail := ""
if askedBy != "" {
tail += fmt.Sprintf(" It was asked for by %s.", askedBy)
}
tail += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " +
"expect this ask."
if what != "" {
q.Message += "\n" + strings.TrimSpace(tail)
} else {
q.Message += tail
}
key, err := text(args, "seal_to", true)
if err != nil {
return q, err
@@ -150,3 +150,61 @@ func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
}
}
}
// What the secret is for shows as its own line under the module and secret names, in the catalogue's own
// words (novox/hq issue 789): read from the secret's declared `prompt`, never a word the caller chose, and
// never shown without a declared one — an old module, or one that declares none, prompts as it does today.
func TestWhatShowsAsItsOwnLineUnderTheNamesWhenDeclaredAndNothingWhenNot(t *testing.T) {
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
base := map[string]any{"module": "mesh-mail", "secret": "mail-credential-gmail", "node": "anchor", "seal_to": key}
q, err := secretQuestion(base)
if err != nil {
t.Fatal(err)
}
if strings.Contains(q.Message, "For:") {
t.Errorf("a \"For:\" line with no what declared: %q", q.Message)
}
withWhat := map[string]any{}
for k, v := range base {
withWhat[k] = v
}
withWhat["what"] = "the password of the mail account gmail"
q, err = secretQuestion(withWhat)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(q.Message, "\nFor: the password of the mail account gmail") {
t.Errorf("no \"For:\" line of its own: %q", q.Message)
}
if !strings.Contains(q.Message, "What you type is not shown") {
t.Errorf("the rest of the message was lost: %q", q.Message)
}
}
// What the secret is for is shown as text, never markup (novox/hq issue 789): the prompt's window reads its
// message with markup on, so what the catalogue holds reaches it with the characters markup gives meaning to
// turned to their entities first, and a line holding more than one is refused before anybody sees it.
func TestWhatIsShownAsTextNeverMarkupAndIsOneLine(t *testing.T) {
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
base := map[string]any{"module": "mesh-mail", "secret": "mail-credential-gmail", "node": "anchor", "seal_to": key,
"what": "the password of <b>the</b> mail & account"}
q, err := secretQuestion(base)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(q.Message, "For: the password of &lt;b&gt;the&lt;/b&gt; mail &amp; account") {
t.Errorf("markup was not escaped: %q", q.Message)
}
for _, bad := range []string{"a\nb", "a\rb"} {
multi := map[string]any{}
for k, v := range base {
multi[k] = v
}
multi["what"] = bad
if _, err := secretQuestion(multi); err == nil {
t.Errorf("what=%q holding more than one line was taken", bad)
}
}
}