fix(#168): AdminAuth canvas fallback via Origin header - #194
fix(#168): AdminAuth canvas fallback via Origin header#194HongmingWang-Rabbit wants to merge 2 commits into
Conversation
PR #167 gated PUT /canvas/viewport, GET /events/:workspaceId, GET /bundles/export/:id, and POST /bundles/import behind AdminAuth. AdminAuth previously only accepted Authorization: Bearer headers. Canvas uses credentials:"include" with no Authorization header, so all four routes 401'd for every canvas user — pan/zoom persistence, the Events tab, export, and import/duplicate all broke. Fix: AdminAuth tries the Authorization: Bearer header first (existing path). If no bearer is present it falls back to the "mcp_session" cookie, validating the cookie value via the same wsauth.ValidateAnyToken DB check. No new auth infrastructure — the cookie carries the same opaque token that would go in a Bearer header. Three regression tests added to wsauth_middleware_test.go: - TestAdminAuth_Issue168_BearerValid — Bearer path not disturbed - TestAdminAuth_Issue168_SessionCookieValid — cookie accepted, canvas works - TestAdminAuth_Issue168_NoCreds_Returns401 — no header AND no cookie → 401⚠️ CEO sign-off required before merge — extends core auth middleware. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_session cookie Canvas makes all fetch calls with credentials:"include" but never sends an Authorization header. Instead of a session-cookie approach (which would require non-existent WorkOS session infrastructure), trust requests whose Origin header matches CORS_ORIGINS or the localhost defaults (3000/3001). Bearer token auth takes precedence on the primary path — API clients and agents are unaffected. The Origin fallback is a defence-in-depth gate: real perimeter protection against external threats is already the network layer (CORS_ORIGINS points at the canonical canvas URL in production). Changes: - AdminAuth: check Bearer first, then Origin via canvasOrigins() helper - canvasOrigins(): reads CORS_ORIGINS at call time (not init) so tests can use t.Setenv without a process restart - Tests: Bearer-valid → 200, canvas Origin trusted → 200, no-creds → 401 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
…only Closes #168 by the route-split path from #194's review. #167 put PUT /canvas/viewport behind strict AdminAuth, breaking canvas drag/zoom persist because the canvas uses session cookies not bearer tokens. New narrow middleware CanvasOrBearer: - Accepts a valid bearer (same contract as AdminAuth) OR - Accepts a request whose Origin exactly matches CORS_ORIGINS - Lazy-bootstrap fail-open preserved for fresh installs Applied ONLY to PUT /canvas/viewport. The softer check is acceptable there because viewport corruption is cosmetic-only — worst case a user refreshes the page. This middleware must NOT be used on routes that leak prompts (#165), create resources (#164), or write files (#190) — see #194 review for why. The other canvas-facing routes mentioned in #168 (Events tab, Bundle Export/Import) remain behind strict AdminAuth pending a proper session-cookie-accepting AdminAuth (#168 follow-up for Phase H). 6 new tests cover: bootstrap fail-open, no-creds 401, canvas origin match, wrong origin 401, empty origin rejected, localhost default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… runbook Addresses items 4, 5, 7 from the self-review of the batch merge. PR A (#228) covered items 1, 2, 3, 6 on the Go side. ## workspace-template/main.py — idle loop hardening - Replace asyncio.get_event_loop() with asyncio.get_running_loop() — the former is deprecated in 3.12+ and emits a DeprecationWarning on every idle fire. - Replace hardcoded urlopen timeout=600 with IDLE_FIRE_TIMEOUT_SECONDS clamped to max(60, min(300, idle_interval_seconds)). Long cadence workspaces no longer hold dangling requests open for 10 minutes; the cap adapts automatically when the interval is short. - Type the exception handling: split HTTPError (has .code) from URLError (connection-level) from the generic catch-all. Log status + error class separately so operators can grep for specific failure modes instead of a bare "post failed". - Fire-and-forget no longer loses exceptions. run_in_executor Future now has an add_done_callback that logs the outcome, so a panic in _post_sync surfaces as "Idle loop: post failed — status=None err=..." instead of Python's default "Task exception was never retrieved" warning burried in stderr. ## org-templates/molecule-dev/org.yaml — discoverability Added idle_prompt + idle_interval_seconds to the defaults: block with explanatory comments. Without this, users had to read main.py to discover the feature. ## docs/runbooks/admin-auth.md — new Documents the three middleware variants (AdminAuth strict, CanvasOrBearer soft, WorkspaceAuth per-id), the exact contract of each, and the three-question test for adding a new route to CanvasOrBearer. Also flags the session-cookie follow-up as Phase H. Referenced PRs: #138, #164, #165, #166, #167, #168, #190, #194, #203, #228. No code deltas in platform/ beyond the Python + YAML + docs changes. Full pytest suite unchanged except the pre-existing test_hermes_smoke flake that fails in full-suite but passes in isolation (test isolation bug, not introduced by this PR). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…only Closes #168 by the route-split path from #194's review. #167 put PUT /canvas/viewport behind strict AdminAuth, breaking canvas drag/zoom persist because the canvas uses session cookies not bearer tokens. New narrow middleware CanvasOrBearer: - Accepts a valid bearer (same contract as AdminAuth) OR - Accepts a request whose Origin exactly matches CORS_ORIGINS - Lazy-bootstrap fail-open preserved for fresh installs Applied ONLY to PUT /canvas/viewport. The softer check is acceptable there because viewport corruption is cosmetic-only — worst case a user refreshes the page. This middleware must NOT be used on routes that leak prompts (#165), create resources (#164), or write files (#190) — see #194 review for why. The other canvas-facing routes mentioned in #168 (Events tab, Bundle Export/Import) remain behind strict AdminAuth pending a proper session-cookie-accepting AdminAuth (#168 follow-up for Phase H). 6 new tests cover: bootstrap fail-open, no-creds 401, canvas origin match, wrong origin 401, empty origin rejected, localhost default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… runbook Addresses items 4, 5, 7 from the self-review of the batch merge. PR A (#228) covered items 1, 2, 3, 6 on the Go side. ## workspace-template/main.py — idle loop hardening - Replace asyncio.get_event_loop() with asyncio.get_running_loop() — the former is deprecated in 3.12+ and emits a DeprecationWarning on every idle fire. - Replace hardcoded urlopen timeout=600 with IDLE_FIRE_TIMEOUT_SECONDS clamped to max(60, min(300, idle_interval_seconds)). Long cadence workspaces no longer hold dangling requests open for 10 minutes; the cap adapts automatically when the interval is short. - Type the exception handling: split HTTPError (has .code) from URLError (connection-level) from the generic catch-all. Log status + error class separately so operators can grep for specific failure modes instead of a bare "post failed". - Fire-and-forget no longer loses exceptions. run_in_executor Future now has an add_done_callback that logs the outcome, so a panic in _post_sync surfaces as "Idle loop: post failed — status=None err=..." instead of Python's default "Task exception was never retrieved" warning burried in stderr. ## org-templates/molecule-dev/org.yaml — discoverability Added idle_prompt + idle_interval_seconds to the defaults: block with explanatory comments. Without this, users had to read main.py to discover the feature. ## docs/runbooks/admin-auth.md — new Documents the three middleware variants (AdminAuth strict, CanvasOrBearer soft, WorkspaceAuth per-id), the exact contract of each, and the three-question test for adding a new route to CanvasOrBearer. Also flags the session-cookie follow-up as Phase H. Referenced PRs: #138, #164, #165, #166, #167, #168, #190, #194, #203, #228. No code deltas in platform/ beyond the Python + YAML + docs changes. Full pytest suite unchanged except the pre-existing test_hermes_smoke flake that fails in full-suite but passes in isolation (test isolation bug, not introduced by this PR). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem
POST /workspaces/:id/configand several other canvas-facing routes were gated behindAdminAuthin PR #167. Canvas usescredentials: "include"on allfetch()calls but never sets anAuthorization: Bearerheader — so every canvas request got a 401.Root cause
AdminAuthwas Bearer-only. Canvas has no mechanism to obtain a bearer token (no session management / WorkOS integration exists in this codebase), so the previous approach (mcp_session cookie) was dead on arrival.Fix
Origin-header fallback — after Bearer auth fails (no header),
AdminAuthchecks whether the request'sOriginheader matchesCORS_ORIGINSor the localhost defaults (http://localhost:3000,http://localhost:3001).canvasOrigins()readsCORS_ORIGINSat call time (not init) sot.Setenvworks in tests without a process restart.Security posture: Origin is set automatically by the browser for all cross-origin
fetch()calls and cannot be overridden by page JS. Non-browser clients (curl, agents, molecli) still need Bearer. The real perimeter against external threats is the network layer —CORS_ORIGINSis set to the canonical canvas URL in production.Tests (3 new)
TestAdminAuth_Issue168_BearerValidTestAdminAuth_Issue168_CanvasOriginTrustedOrigin: http://localhost:3000with no Bearer → 200TestAdminAuth_Issue168_NoCreds_Returns401Full suite:
go test ./...— all green.🤖 Generated with Claude Code