Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions scripts/private-mail-config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ func render(v inputs) ([]byte, error) {
return map[string]any{"@type": "update", "object": object, "value": value}
}
operations := []map[string]any{
// Match the tagged variant so replay keeps the same console tracer. A
// second matching tracer is refused by the native CLI, never chosen by age.
upsert("Tracer", []string{"@type"}, map[string]any{"email-console": map[string]any{
"@type": "Stdout", "enable": true, "level": "info", "lossy": false,
"buffered": false, "ansi": false, "multiline": false, "eventsPolicy": "include",
"events": map[string]bool{
"server.startup": true, "server.shutdown": true, "server.startup-error": true, "server.thread-error": true,
"network.listen-start": true, "network.listen-stop": true, "network.listen-error": true, "network.bind-error": true,
"resource.error": true, "resource.not-found": true, "resource.bad-parameters": true, "resource.application-unpacked": true,
"store.rocksdb-error": true,
},
}}),
// Declare the existing local authentication state explicitly. A main
// recovery must clear a later external directory rather than retain it.
update("Authentication", map[string]any{"directoryId": nil}),
Expand Down
137 changes: 137 additions & 0 deletions scripts/private-mail-config/diagnostics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

// TestConsoleDiagnosticsExcludeMailAndAuthenticationEvents requires a bounded diagnostic tracer.
func TestConsoleDiagnosticsExcludeMailAndAuthenticationEvents(t *testing.T) {
plan, err := render(fixture())
if err != nil {
t.Fatal(err)
}
count := 0
for _, line := range strings.Split(strings.TrimSpace(string(plan)), "\n") {
var op struct {
Type string `json:"@type"`
Object string
MatchOn []string
Value map[string]map[string]any
}
var family struct{ Object string }
if json.Unmarshal([]byte(line), &family) != nil {
t.Fatal("invalid plan")
}
if family.Object != "Tracer" {
continue
}
if json.Unmarshal([]byte(line), &op) != nil || op.Type != "upsert" || len(op.MatchOn) != 1 || op.MatchOn[0] != "@type" || len(op.Value) != 1 {
t.Fatal("diagnostics are not repeatable by tracer type")
}
for _, tracer := range op.Value {
count++
if tracer["@type"] != "Stdout" || tracer["enable"] != true || tracer["level"] != "info" || tracer["buffered"] != false || tracer["ansi"] != false || tracer["multiline"] != false || tracer["lossy"] != false || tracer["eventsPolicy"] != "include" {
t.Fatal("unsafe console diagnostic configuration")
}
events, ok := tracer["events"].(map[string]any)
if !ok || len(events) == 0 || events["server.startup"] != true || events["server.startup-error"] != true || events["store.rocksdb-error"] != true {
t.Fatal("startup diagnostics are incomplete")
}
allowed := map[string]bool{"server.startup": true, "server.shutdown": true, "server.startup-error": true, "server.thread-error": true, "network.listen-start": true, "network.listen-stop": true, "network.listen-error": true, "network.bind-error": true, "resource.error": true, "resource.not-found": true, "resource.bad-parameters": true, "resource.application-unpacked": true, "store.rocksdb-error": true}
for event, enabled := range events {
if !allowed[event] || enabled != true {
t.Fatal("mail or authentication event reached console diagnostics")
}
}
}
}
if count != 1 {
t.Fatal("exactly one console diagnostic tracer is required")
}
}

// TestDiagnosticReadbackRejectsAdditionalEvents prevents silent expansion of the console trace.
func TestDiagnosticReadbackRejectsAdditionalEvents(t *testing.T) {
want := map[string]any{"server.startup": true}
actual := map[string]any{"server.startup": true, "auth.success": true}
if policyMatches(want, actual, "events") {
t.Fatal("diagnostic readback accepted additional private events")
}
}

// TestDiagnosticReadbackUsesTaggedIdentity exercises the authenticated query/get path.
func TestDiagnosticReadbackUsesTaggedIdentity(t *testing.T) {
for _, test := range []string{"valid", "different-type", "duplicate-console", "extra-event"} {
t.Run(test, func(t *testing.T) {
want := map[string]any{"@type": "Stdout", "enable": true, "level": "info", "eventsPolicy": "include", "events": map[string]any{"server.startup": true}}
encoded, err := json.Marshal(map[string]any{"@type": "upsert", "object": "Tracer", "matchOn": []string{"@type"}, "value": map[string]any{"console": want}})
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, password, ok := r.BasicAuth()
if !ok || user != "bootstrap" || password != "synthetic-only" || r.Method != http.MethodPost || r.URL.Path != "/jmap" {
w.WriteHeader(403)
return
}
var request struct {
Calls [][]json.RawMessage `json:"methodCalls"`
}
if json.NewDecoder(r.Body).Decode(&request) != nil || len(request.Calls) != 1 || len(request.Calls[0]) != 3 {
w.WriteHeader(400)
return
}
var method, tag string
if json.Unmarshal(request.Calls[0][0], &method) != nil || json.Unmarshal(request.Calls[0][2], &tag) != nil {
t.Error("invalid tracer method or response tag")
w.WriteHeader(http.StatusBadRequest)
return
}
var result any
switch method {
case "x:Tracer/query":
ids := []string{"console-id"}
if test == "duplicate-console" {
ids = append(ids, "other-id")
}
result = map[string]any{"ids": ids, "total": len(ids), "position": 0}
case "x:Tracer/get":
actual := map[string]any{"id": "console-id", "@type": "Stdout", "enable": true, "level": "info", "eventsPolicy": "include", "events": map[string]any{"server.startup": true}}
if test == "different-type" {
actual["@type"] = "Log"
}
if test == "extra-event" {
actual["events"].(map[string]any)["auth.success"] = true
}
list := []any{actual}
if test == "duplicate-console" {
duplicate := map[string]any{}
for k, v := range actual {
duplicate[k] = v
}
duplicate["id"] = "other-id"
list = append(list, duplicate)
}
result = map[string]any{"list": list, "notFound": []string{}}
default:
w.WriteHeader(400)
return
}
if err := json.NewEncoder(w).Encode(map[string]any{"methodResponses": []any{[]any{method, result, tag}}}); err != nil {
t.Errorf("encode tracer response: %v", err)
}
}))
defer server.Close()
m := management{url: server.URL, password: "synthetic-only", client: server.Client()}
err = m.verify(context.Background(), encoded)
if (err == nil) != (test == "valid") {
t.Fatal("incorrect tracer identity or privacy readback verdict")
}
})
}
}
5 changes: 4 additions & 1 deletion scripts/private-mail-config/readback.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func policyMatches(want, actual any, key string) bool {
return receivingAliasesMatch(want, actual)
}
switch key {
case "match", "enabledPermissions", "disabledPermissions", "bind", "urlPrefix":
case "match", "enabledPermissions", "disabledPermissions", "bind", "urlPrefix", "events":
return reflect.DeepEqual(want, actual)
}
if wm, ok := want.(map[string]any); ok {
Expand Down Expand Up @@ -257,6 +257,9 @@ func (m management) verify(ctx context.Context, plan []byte) error {
var match map[string]any
for _, actual := range objects {
identity := fields["name"] != nil && reflect.DeepEqual(fields["name"], actual["name"]) && (fields["domainId"] == nil || fields["domainId"] == actual["domainId"])
if op.Object == "Tracer" && fields["@type"] == "Stdout" {
identity = actual["@type"] == "Stdout"
}
if op.Object == "Certificate" {
identity = policyMatches(fields["certificate"], actual["certificate"], "")
}
Expand Down
54 changes: 49 additions & 5 deletions scripts/tests/test-private-mail-configuration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,49 @@
set -Eeuo pipefail
trap 'printf "FAIL: native mail proof stopped at line %s\n" "$LINENO" >&2' ERR
umask 077
assert_diagnostic_log_privacy() {
local log=$1 secret
shift
if ! grep -Fq 'server.startup' "$log"; then
echo 'FAIL: normal serving produced no startup diagnostic event' >&2; return 1
fi
for secret in "$@"; do
if grep -Fq -f "$secret" "$log"; then
echo 'FAIL: a generated fixture credential appeared in server logs' >&2; return 1
fi
done
if grep -Eqi 'temporary administrator|private-mail-recovery-fixture-only' "$log"; then
echo 'FAIL: a recovery credential appeared in server logs' >&2; return 1
fi
if grep -Eqi '(^|[^[:alnum:]_-])(smtp|imap|jmap|auth|oauth|message|message-ingest|queue|delivery|http|websocket)\.[[:alnum:]_-]+' "$log"; then
echo 'FAIL: excluded message or authentication events appeared in server logs' >&2; return 1
fi
if grep -Fq -e 'Native policy proof' -e 'mail-policy@example.test' -e 'Preserved native mail fixture.' -e 'Human submission proof' -e 'Receiving continuity' -e 'private-canary' "$log"; then
echo 'FAIL: private fixture message content appeared in server logs' >&2; return 1
fi
}
test_diagnostic_log_privacy() (
local proof
proof=$(mktemp -d)
trap 'rm -rf "$proof"' EXIT
printf 'server.startup\n' >"$proof/log"
assert_diagnostic_log_privacy "$proof/log"
printf 'message-ingest.ham message not in the listed canaries\n' >>"$proof/log"
if assert_diagnostic_log_privacy "$proof/log" >"$proof/result" 2>&1; then
echo 'FAIL: diagnostic privacy check accepted an unlisted message-ingest event' >&2
exit 1
fi
grep -Fq 'excluded message or authentication events' "$proof/result"
printf 'server.startup\nprivate-canary\n' >"$proof/log"
if assert_diagnostic_log_privacy "$proof/log" >"$proof/result" 2>&1; then
echo 'FAIL: diagnostic privacy check accepted a listed message canary' >&2
exit 1
fi
grep -Fq 'private fixture message content' "$proof/result"
echo 'PASS: diagnostic log privacy controls'
)
test_diagnostic_log_privacy
if [ "${1:-}" = --test-log-privacy ]; then exit 0; fi
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
fixture_user="$(id -u):$(id -g)"
[ "$(id -u)" != 0 ] || { echo 'FAIL: native mail proof requires an unprivileged fixture user' >&2; exit 1; }
Expand Down Expand Up @@ -219,6 +262,7 @@ apply_plan
native_cli query Account --fields id --json >"$scratch/accounts-before.jsonl"
native_cli query Certificate --fields id --json >"$scratch/certificates-before.jsonl"
native_cli query Application --fields id --json >"$scratch/applications-before.jsonl"
native_cli query Tracer --fields id --json >"$scratch/tracers-before.jsonl"
# Permanent client rules are outside configuration ownership and must survive
# both configuration replay and the later complete initializer re-entry.
printf '%s\n' '{"@type":"create","object":"BlockedIp","value":{"protected-probe":{"address":"192.0.2.30","reason":"portScanning"},"protected-control":{"address":"192.0.2.31","reason":"manual"}}}' >"$scratch/protected-blocks.ndjson"
Expand Down Expand Up @@ -363,7 +407,8 @@ jq -e '.rewrite.match|keys|sort==["0","1"]' "$scratch/rcpt.json" >/dev/null
native_cli query Account --fields id --json >"$scratch/accounts-after.jsonl"
native_cli query Certificate --fields id --json >"$scratch/certificates-after.jsonl"
native_cli query Application --fields id --json >"$scratch/applications-after.jsonl"
for object in accounts certificates applications; do
native_cli query Tracer --fields id --json >"$scratch/tracers-after.jsonl"
for object in accounts certificates applications tracers; do
jq -s '[.[].id]|sort' "$scratch/$object-before.jsonl" >"$scratch/before.ids"
jq -s '[.[].id]|sort' "$scratch/$object-after.jsonl" >"$scratch/after.ids"
cmp "$scratch/before.ids" "$scratch/after.ids"
Expand Down Expand Up @@ -462,6 +507,7 @@ grep -q 'Preserved native mail fixture.' "$scratch/known-read.eml"
# Prime the native application cache with a previous unversioned bundle. An
# upgrade must invalidate that cache while preserving the Application identity.
docker stop --time 30 "$name" >/dev/null
docker logs "$name" >"$scratch/smtp-auth-server.log" 2>&1
docker rm "$name" >/dev/null
mkdir "$scratch/stale-assets" "$scratch/stale-source"
printf '%s' '<!doctype html><html><head><base href="/" /></head><body>previous-ui-cache-canary</body></html>' >"$scratch/stale-source/index.html"
Expand Down Expand Up @@ -519,10 +565,8 @@ for alias in former-a former-b; do
cmp "$scratch/$alias-receiving-before.json" "$scratch/$alias-receiving-metadata.json"
done
docker logs "$name" >"$scratch/server.log" 2>&1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
for secret in "$scratch/inputs/"*-password; do
if grep -Fq -f "$secret" "$scratch/server.log"; then
echo 'FAIL: a generated fixture credential appeared in server logs' >&2; exit 1
fi
for log in "$scratch/smtp-auth-server.log" "$scratch/server.log"; do
assert_diagnostic_log_privacy "$log" "$scratch/inputs/"*-password
done
if grep -Eqi 'temporary administrator|private-mail-recovery-fixture-only' "$scratch/server.log"; then
echo 'FAIL: a recovery credential appeared in server logs' >&2; exit 1
Expand Down
Loading