From 3967fa955a5d2966db2f93503517aea0c55d5851 Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Wed, 8 Jul 2026 00:03:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20rebuild=20admin=20claims=20from=20truste?= =?UTF-8?q?d=20headers=20=E2=80=94=20v2.0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context does not cross the wasm ABI, so requireAdmin's auth.GetUserFromContext only sees claims if the guest rebuilds them from the host-stamped X-Bn-Verified-* headers. 2.0.3 never mounted auth.TrustedHeaderMiddleware, so every admin endpoint (settings save, event-types, test, rotate) returned 401 for real admins — the settings panel could not store the Cal.com API key. Mount the middleware on the router and pin the contract with a header-driven test (viewer headers must reach requireAdmin and 403, never 401): the existing admin tests injected claims straight into context, which is exactly the path wasm does not have. Co-Authored-By: Claude Fable 5 --- handler.go | 5 +++++ plugin.mod | 2 +- router_paths_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/handler.go b/handler.go index c8aa63a..37f3359 100644 --- a/handler.go +++ b/handler.go @@ -265,6 +265,11 @@ func NewCalcomHandler(settings *SettingsManager, limiter *RateLimiter, appURL st // and encrypt the Cal.com API key + webhook secret. func NewCalcomRouter(deps plugin.CoreServices) http.Handler { r := chi.NewRouter() + // Context does not cross the wasm ABI: requireAdmin's claims exist only if + // the guest rebuilds them from the host-stamped X-Bn-Verified-* headers. + // Without this middleware every admin endpoint 401s for real admins (the + // settings panel could not save the API key — 2.0.3 regression). + r.Use(auth.TrustedHeaderMiddleware) // Settings persistence goes through the SDK settings capabilities: under the // wasm sandbox the plugin's Postgres role cannot touch the CMS `settings` // table (public schema) directly, so the host performs the DB work. diff --git a/plugin.mod b/plugin.mod index 5d0d9b4..b2ff67b 100644 --- a/plugin.mod +++ b/plugin.mod @@ -2,7 +2,7 @@ name = "calcomblock" display_name = "Cal.com Booking" scope = "@ninja" -version = "2.0.3" +version = "2.0.4" description = "Embeddable Cal.com booking calendar block with custom styling, timezone-aware slot windowing, honeypot + captcha + rate-limited public booking endpoints, and webhook receiver." kind = "plugin" categories = ["forms"] diff --git a/router_paths_test.go b/router_paths_test.go index e49155f..23ca89f 100644 --- a/router_paths_test.go +++ b/router_paths_test.go @@ -2,10 +2,13 @@ package main import ( "net/http" + "net/http/httptest" "testing" + "git.dev.alexdunmow.com/block/pluginsdk/auth" "git.dev.alexdunmow.com/block/pluginsdk/plugin" "github.com/go-chi/chi/v5" + "github.com/google/uuid" ) // TestRouterMatchesAbsolutePluginPaths pins the wasm routing contract: the @@ -33,3 +36,28 @@ func TestRouterMatchesAbsolutePluginPaths(t *testing.T) { } } } + +// TestRouterRebuildsClaimsFromTrustedHeaders pins the wasm identity contract: +// context does not cross the ABI, so the ONLY way requireAdmin can see the +// caller is for the router to rebuild claims from the host-stamped +// X-Bn-Verified-* headers (auth.TrustedHeaderMiddleware). 2.0.3 shipped +// without it — every admin endpoint 401'd for real admins. A viewer-role +// header must reach requireAdmin and be rejected 403 (claims seen), never +// 401 (claims lost); no handler runs, so empty CoreServices is safe. +func TestRouterRebuildsClaimsFromTrustedHeaders(t *testing.T) { + router := NewCalcomRouter(plugin.CoreServices{}) + + req := httptest.NewRequest(http.MethodGet, "/api/plugins/calcomblock/settings", nil) + req.Header.Set(auth.HeaderVerifiedUserID, uuid.NewString()) + req.Header.Set(auth.HeaderVerifiedEmail, "viewer@example.test") + req.Header.Set(auth.HeaderVerifiedRole, "viewer") + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code == http.StatusUnauthorized { + t.Fatalf("viewer trusted headers yielded 401 — TrustedHeaderMiddleware is not mounted, claims never reach requireAdmin") + } + if rec.Code != http.StatusForbidden { + t.Errorf("viewer trusted headers: expected 403 from requireAdmin, got %d (body %q)", rec.Code, rec.Body.String()) + } +}