3747fe2f49
CTO directive: "nothing should be fail-open." Remove the dev-mode fail-open auth hatch so AdminAuth/WorkspaceAuth (and the discovery caller) ALWAYS require a real credential — fail-CLOSED in every environment, dev included — fix local dev to stay AUTHENTICATED (not open), and add a regression gate so fail-open cannot return. Removed fail-open call-sites (workspace-server): - internal/middleware/wsauth_middleware.go WorkspaceAuth — deleted the isDevModeFailOpen() short-circuit that let a bearer-less /workspaces/:id/* request through when MOLECULE_ENV=dev + ADMIN_TOKEN unset. - internal/middleware/wsauth_middleware.go AdminAuth — deleted BOTH fail-open branches: the Tier-1 lazy-bootstrap (no live tokens + no ADMIN_TOKEN ⇒ pass, the C4 /org/import pre-empt hole) and the Tier-1b isDevModeFailOpen() dev hatch. HasAnyLiveTokenGlobal is still probed for the 503-on-outage semantics but opens no path. - internal/handlers/discovery.go validateDiscoveryCaller — deleted the IsDevModeFailOpen() allow branch; discovery now requires a verified CP session or valid bearer in every env. - Removed the isDevModeFailOpen()/IsDevModeFailOpen() helper entirely. The two legitimately non-auth uses (rate-limit relaxation in ratelimit.go, loopback bind default in cmd/server) now key on a new NON-security isLocalDevEnv() predicate (MOLECULE_ENV only, decoupled from ADMIN_TOKEN). CanvasOrBearer's cosmetic-only behaviour (PUT /canvas/viewport) is unchanged. Dev path stays authenticated, not open: - scripts/dev-start.sh provisions a deterministic ADMIN_TOKEN into .env and exports the matching NEXT_PUBLIC_ADMIN_TOKEN so the dev Canvas sends a real bearer (canvas/src/lib/api.ts already attaches it; next.config.ts pair-guard). - Docs updated: .env.example, docs/quickstart.md, docs/architecture/overview.md. Regression gate: - internal/middleware/no_fail_open_test.go — asserts AdminAuth + WorkspaceAuth fail CLOSED (401) under the EXACT old-hatch conditions (ADMIN_TOKEN unset + MOLECULE_ENV=dev/development × hasLive 0/1). Proven RED against a temporarily restored hatch, GREEN after. Plus a source-guard test forbidding the isDevModeFailOpen(-style helper from re-appearing. - Converted the stale fail-open assertions in wsauth_middleware_test.go, discovery_test.go, security_regression_685_686_687_688_test.go and the devmode/bind tests to pin the fail-closed contract. Audit (other fail-open patterns on the auth surface): CanvasOrBearer and validateDiscoveryCaller retain a fail-open-on-DB-error (and CanvasOrBearer a no-token lazy-bootstrap) — both are documented availability tradeoffs on cosmetic / low-sensitivity routes, left as-is and flagged for follow-up. Verify: go build ./... ok; go vet middleware/cmd/handlers clean; full module go test ./... = 46 ok / 0 fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
2.5 KiB
Go
58 lines
2.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Local-dev environment detection.
|
|
//
|
|
// SECURITY (harden/no-fail-open-auth): this file used to export an auth
|
|
// escape hatch — `isDevModeFailOpen()` — that let AdminAuth, WorkspaceAuth,
|
|
// and the discovery handler serve admin/workspace-protected endpoints with
|
|
// NO bearer token whenever `ADMIN_TOKEN` was unset and `MOLECULE_ENV` was a
|
|
// dev value. The CTO directive is "nothing should be fail-open": auth is now
|
|
// fail-CLOSED in every environment, dev included. The hatch is GONE.
|
|
//
|
|
// What remains here is a NON-security predicate, `isLocalDevEnv()`, that
|
|
// reports ONLY whether `MOLECULE_ENV` names a local-dev environment. It does
|
|
// NOT consult `ADMIN_TOKEN` and it does NOT influence authentication. It is
|
|
// used for two convenience/defense-in-depth knobs that never grant access:
|
|
//
|
|
// - ratelimit.go: relax the per-caller request bucket on a single-user
|
|
// local stack (a DoS knob, not a credential — relaxing it cannot expose
|
|
// any protected data).
|
|
// - cmd/server resolveBindHost(): default the HTTP listener to loopback
|
|
// (127.0.0.1) in local dev. This is strictly *safer* than binding all
|
|
// interfaces and is unrelated to whether a request is authenticated.
|
|
//
|
|
// Local dev now stays AUTHENTICATED, not open: scripts/dev-start.sh
|
|
// provisions a deterministic `ADMIN_TOKEN` and hands the matching
|
|
// `NEXT_PUBLIC_ADMIN_TOKEN` to the Canvas, so the browser sends a real
|
|
// bearer. See scripts/dev-start.sh and canvas/src/lib/api.ts.
|
|
|
|
// devModeEnvValues is the set of MOLECULE_ENV values that count as
|
|
// "explicit local dev". Production callers don't set any of these.
|
|
// Case-insensitive compare via strings.ToLower below.
|
|
var devModeEnvValues = map[string]struct{}{
|
|
"development": {},
|
|
"dev": {},
|
|
}
|
|
|
|
// isLocalDevEnv reports whether MOLECULE_ENV names a local-dev environment
|
|
// ("development" / "dev"). It carries NO authentication semantics — callers
|
|
// must never use it to bypass a credential check. It exists only for
|
|
// dev-convenience / defense-in-depth knobs (rate-limit relaxation, loopback
|
|
// bind default) that cannot expose protected data.
|
|
func isLocalDevEnv() bool {
|
|
env := strings.ToLower(strings.TrimSpace(os.Getenv("MOLECULE_ENV")))
|
|
_, ok := devModeEnvValues[env]
|
|
return ok
|
|
}
|
|
|
|
// IsLocalDevEnv exposes isLocalDevEnv to packages outside the middleware
|
|
// module (cmd/server bind-host default). NON-security: see isLocalDevEnv.
|
|
func IsLocalDevEnv() bool {
|
|
return isLocalDevEnv()
|
|
}
|