molecule-core/workspace-server/internal/middleware/devmode.go
Hongming Wang 47d3ef5b9e refactor(middleware): extract dev-mode fail-open predicate
AdminAuth and WorkspaceAuth both carried the same 5-line
`ADMIN_TOKEN == "" && MOLECULE_ENV in {development, dev}` check. If a
third middleware ever needs the hatch — or if "dev mode" semantics
change (new env name, allowlist, runtime flag) — the previous shape
made N places to keep in sync and N places a security reviewer has to
audit.

This commit factors the predicate into a single `isDevModeFailOpen()`
helper in `internal/middleware/devmode.go`. Each call site becomes

    if isDevModeFailOpen() { c.Next(); return }

`devmode.go` carries the full rationale (why the hatch exists, why
it's safe for SaaS) so call sites don't need to restate it.

### Also

- Moved the dev-mode env-value set to a package-level `devModeEnvValues`
  map so adding aliases is one line. Matches the existing convention
  (`handlers/admin_test_token.go`) of treating `MOLECULE_ENV != "production"`
  as dev — but stays explicit about which values opt IN rather than
  blanket-accepting everything non-prod.
- Added case-insensitive compare + trim on the env value so operators
  don't have to remember exact casing.
- New `devmode_test.go` unit-tests the predicate directly: 6 cases
  covering happy path, both opt-out signals (ADMIN_TOKEN, production
  mode), short alias, case-insensitive + whitespace tolerance, and an
  explicit negative-space sweep of arbitrary non-dev values
  ("staging", "preview", "test", "devel", "") to lock in that typos
  don't silently enable the hatch.

Existing AdminAuth/WorkspaceAuth integration tests still exercise the
helper indirectly via HTTP — they pass unchanged, confirming the
behaviour is preserved.

### No behavioural change

Before and after this commit, `go test -race ./internal/middleware/`
reports identical results. Zero production surface change — this is a
pure refactor, but it collapses the dev-mode seam from two inline
blocks into one named predicate, which is the shape future
contributors (and security reviewers) can follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:55:34 -07:00

57 lines
2.3 KiB
Go

package middleware
import (
"os"
"strings"
)
// Dev-mode escape hatch — factored out of AdminAuth + WorkspaceAuth so a
// future third caller (or a change to what "dev mode" means) touches one
// place. Narrowing the exposed seam also makes it grep-able from security
// reviews: every `isDevModeFailOpen()` call is an intentional fail-open.
//
// Why the helper exists at all: on `go run ./cmd/server` the Canvas (at
// localhost:3000) calls the platform (at localhost:8080) cross-port. Both
// `isSameOriginCanvas` (Referer==Host) and the AdminAuth Tier-1 fail-open
// (no tokens in DB) close the moment the user creates their first
// workspace. Without this hatch the Canvas 401s on every /workspaces
// enumeration and every /workspaces/:id/* read until the operator sets
// `ADMIN_TOKEN` and rebuilds the Canvas bundle with a matching
// `NEXT_PUBLIC_ADMIN_TOKEN`. That's too much friction for a local smoke
// test — hence the hatch.
//
// Why it's safe for SaaS: hosted tenants are provisioned with both
// `ADMIN_TOKEN` (a random secret, checked by Tier-2 above) and
// `MOLECULE_ENV=production`. Either one being set makes this helper
// return false, so the fail-open branch is unreachable in production.
// The convention matches `handlers/admin_test_token.go`, which gates
// the e2e test-token mint on `MOLECULE_ENV != "production"`.
// devModeEnvValues is the set of MOLECULE_ENV values that count as
// "explicit dev mode". Production callers don't set any of these.
// Case-insensitive compare via strings.ToLower below.
var devModeEnvValues = map[string]struct{}{
"development": {},
"dev": {},
}
// isDevModeFailOpen reports whether the AdminAuth / WorkspaceAuth
// middleware should let a bearer-less request through despite live
// workspace tokens existing in the DB.
//
// True only when BOTH:
// - `ADMIN_TOKEN` is empty (operator has not opted in to the #684
// closure), AND
// - `MOLECULE_ENV` is explicitly a dev value ("development" / "dev").
//
// Either condition failing returns false — that's the SaaS safety
// guarantee. Tests: `devmode_test.go` covers every branch.
func isDevModeFailOpen() bool {
if os.Getenv("ADMIN_TOKEN") != "" {
return false
}
env := strings.ToLower(strings.TrimSpace(os.Getenv("MOLECULE_ENV")))
_, ok := devModeEnvValues[env]
return ok
}