Keep the entrypoint UUID out of webhooker's own logs (closes #531)
check / check (push) Waiting to run

On the receiver route the access log and the per-entrypoint 429 line
name the route pattern whatever the status. A request chi never routed,
such as a dev-mode CORS preflight to the receiver, logs (unmatched)
whatever the status. The received line names the entrypoint and webhook
IDs, and the unknown-entrypoint line logs no path. The pattern is
middleware.ReceiverRoute, which routes.go registers the receiver with.

The README says the proxy's access log holds the UUID and must be
protected like the URL, and its access-log, rate-limit, Sentry and
log-size passages now match the code.

Model: opus-5-5
This commit is contained in:
2026-10-15 10:29:37 +00:00
parent c0f7cc29fd
commit df2d9acd4d
10 changed files with 315 additions and 169 deletions
+91 -12
View File
@@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
@@ -27,6 +28,13 @@ const floodRequests = 64
// line for a redirected or rejected request may contain it.
const attackerMarker = "QQATTACKERTEXTQQ"
// knownEntrypointUUID and failingEntrypointUUID are the UUIDs the
// stand-in receiver in accessLogRouter answers 200 and 500 to.
const (
knownEntrypointUUID = "6f1e2a9c-4b7d-4e3a-9c2f-1d8b5a7e3c60"
failingEntrypointUUID = "0b7c3e5a-2d9f-4a61-8e4b-7c1d6f2a9e35"
)
// maxLineBytes bounds a single access log line whose client-supplied
// fields are of ordinary size. Well above what the fixed fields need,
// well below the length of the oversized input the amplification tests
@@ -132,17 +140,21 @@ func accessLogRouter(m *middleware.Middleware) *chi.Mux {
)
router.HandleFunc(
"/h/{uuid}",
middleware.ReceiverRoute,
func(w http.ResponseWriter, r *http.Request) {
// Stands in for the real handler: an unknown entrypoint
// UUID 404s, a known one succeeds.
if chi.URLParam(r, "uuid") != "known" {
// Stands in for the real handler: a known entrypoint
// UUID succeeds, a failing one is a server error, and
// any other 404s.
switch chi.URLParam(r, "uuid") {
case knownEntrypointUUID:
w.WriteHeader(http.StatusOK)
case failingEntrypointUUID:
http.Error(
w, "boom", http.StatusInternalServerError,
)
default:
http.Error(w, "not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
},
)
@@ -649,17 +661,84 @@ func TestAccessLog_SuccessKeepsConcretePathAndRedactsQuery(
router := accessLogRouter(m)
assert.Equal(
t, http.StatusOK, get(t, router, "/h/known?src=ci"),
t, http.StatusOK,
get(t, router, "/.well-known/healthcheck?src=ci"),
)
// The path resolved against a stored entrypoint, so it stays. The
// query never does: see TestAccessLog_UnauthenticatedSuccess...
entries := accessLogEntries(t, buf)
require.Len(t, entries, 1)
assert.Equal(t, "/h/known?(redacted)", entries[0]["url"])
assert.Equal(
t, "/.well-known/healthcheck?(redacted)", entries[0]["url"],
)
assert.NotContains(t, buf.String(), "src=ci")
}
// TestAccessLog_ReceiverNeverLogsTheUUID covers the two statuses that
// keep the concrete path on every other route. On the receiver that
// path carries the entrypoint UUID, so the line names the route
// pattern instead.
func TestAccessLog_ReceiverNeverLogsTheUUID(t *testing.T) {
t.Parallel()
for entrypointUUID, wantStatus := range map[string]int{
knownEntrypointUUID: http.StatusOK,
failingEntrypointUUID: http.StatusInternalServerError,
} {
t.Run(strconv.Itoa(wantStatus), func(t *testing.T) {
t.Parallel()
m, buf := capturingMiddleware(t)
router := accessLogRouter(m)
assert.Equal(
t, wantStatus,
get(t, router, "/h/"+entrypointUUID+"?src=ci"),
)
entries := accessLogEntries(t, buf)
require.Len(t, entries, 1)
assert.Equal(t, middleware.ReceiverRoute, entries[0]["url"])
assert.NotContains(t, buf.String(), entrypointUUID)
})
}
}
// TestAccessLog_ReceiverPreflightNeverLogsTheUUID sends a CORS
// preflight to the receiver in dev, in the production middleware
// order. CORS answers it with 200 before chi routes it, so the line
// names no route and carries no UUID.
func TestAccessLog_ReceiverPreflightNeverLogsTheUUID(t *testing.T) {
t.Parallel()
m, buf := capturingMiddleware(t)
router := chi.NewRouter()
router.Use(m.Logging())
router.Use(m.CORS())
router.HandleFunc(
middleware.ReceiverRoute,
func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
},
)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodOptions,
"/h/"+knownEntrypointUUID, nil,
)
req.Header.Set("Origin", "https://sender.example")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
entries := accessLogEntries(t, buf)
require.Len(t, entries, 1)
assert.Equal(t, unmatchedRouteLiteral, entries[0]["url"])
assert.NotContains(t, buf.String(), knownEntrypointUUID)
}
func TestAccessLog_ServerErrorKeepsConcreteURL(t *testing.T) {
t.Parallel()