6f56b1fa30
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Successful in 3s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 2s
CI / Detect changes (pull_request) Successful in 15s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 10s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
gate-check-v3 / gate-check (pull_request_target) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 30s
E2E Chat / detect-changes (pull_request) Successful in 30s
qa-review / approved (pull_request_target) Failing after 4s
sop-checklist / review-refire (pull_request_target) Has been skipped
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 24s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 25s
sop-tier-check / tier-check (pull_request_target) Failing after 5s
security-review / approved (pull_request_target) Failing after 9s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 5s
sop-checklist / all-items-acked (pull_request_target) Successful in 13s
CI / Canvas (Next.js) (pull_request) Successful in 2s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 1m8s
Harness Replays / Harness Replays (pull_request) Successful in 26s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 58s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 55s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 55s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m6s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Failing after 2m11s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 2m25s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 12s
E2E Chat / E2E Chat (pull_request) Successful in 24s
CI / Canvas Deploy Status (pull_request) Has been skipped
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 59s
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Failing after 7m13s
CI / Platform (Go) (pull_request) Successful in 7m28s
CI / all-required (pull_request) Successful in 17s
qa-review / approved (pull_request_review) Has been skipped
security-review / approved (pull_request_review) Has been skipped
sop-tier-check / tier-check (pull_request_review) Failing after 4s
audit-force-merge / audit (pull_request_target) Successful in 26s
The prior pass (#2291) made AdminAuth/WorkspaceAuth fail-closed but RETAINED two fail-open patterns 'as a cosmetic tradeoff'. The CTO directive 'nothing should be fail-open' is ABSOLUTE, so this pass removes them too. ZERO fail-open paths now remain anywhere in workspace-server auth. CanvasOrBearer (workspace-server/internal/middleware/wsauth_middleware.go): - DB-error fail-open (`if err != nil { log; c.Next() }`) → now 503 fail-CLOSED via abortAuthLookupError (availability tradeoff, NO access). - lazy-bootstrap fail-open (`if !hasLive { c.Next() }`) → REMOVED. A zero-token install no longer passes EVERYTHING; bootstrap is via ADMIN_TOKEN (dev-start.sh provisions it for local dev; operator/SaaS sets it in prod — local mimics production). - forgeable cross-origin Origin-match pass (canvasOriginAllowed) → REMOVED. A no-bearer request passing purely on a spoofable Origin is effectively open even for a cosmetic route. The canvas now always sends a bearer (NEXT_PUBLIC_ADMIN_TOKEN), so nothing legitimate relied on it. The non-forgeable same-origin path (isSameOriginCanvas, gated by CANVAS_PROXY_URL) is kept. Helper + its 2 unit tests removed. validateDiscoveryCaller (workspace-server/internal/handlers/discovery.go): - DB-error fail-open (`if err != nil { return nil }`) → now writes 503 and returns a non-nil error (caller already `if err != nil { return }`). Bootstrap: ADMIN_TOKEN is the first-token credential (AdminAuth accepts it); documented in docs/runbooks/admin-auth.md (fail-closed everywhere; MOLECULE_ENV no longer gates any auth decision). quickstart.md already covered this. Tests: - no_fail_open_test.go: extended with CanvasOrBearer fail-closed cases (401 zero-token, 503 DB-error). discovery_test.go: added TestPeers/Discover_AuthProbeDBError_FailsClosed (503). - Flipped the stale assertions: CanvasOrBearer NoTokens/CanvasOrigin/DBError now assert fail-closed; removed canvasOriginAllowed tests. - tests/e2e/test_dev_mode.sh: repurposed from 'dev-mode fail-open works' to 'dev-mode is fail-CLOSED' (401 no-bearer, 200 with dev ADMIN_TOKEN). - Seeded the HasAnyLiveToken auth probe (grandfather count=0) in ~13 pre- existing discovery handler-body tests that previously relied on the fail-open swallowing the unmatched probe query. Watch-it-fail: restoring each removed branch turns the matching gate test RED (verified for all three: CanvasOrBearer lazy-bootstrap, CanvasOrBearer DB-error, discovery DB-error), reverting → green. go build ./..., go vet, and full go test ./... (46 pkgs) all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
171 lines
7.0 KiB
Bash
Executable File
171 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# E2E regression suite asserting that "dev mode" is fail-CLOSED.
|
|
#
|
|
# History: this file used to assert the local-dev fail-open escape hatches
|
|
# (GET /workspaces 200 with NO bearer, /workspaces/:id/activity 200 with no
|
|
# bearer) added in fix/quickstart-bugless. Under the CTO "nothing should be
|
|
# fail-open" directive (harden/no-fail-open-auth) those hatches were REMOVED:
|
|
# auth is fail-CLOSED in EVERY environment, local dev included. This suite now
|
|
# pins the inverse contract — bearer-less admin/workspace requests 401, and the
|
|
# SAME requests with the dev ADMIN_TOKEN bearer succeed.
|
|
#
|
|
# What it verifies:
|
|
# 1. GET /workspaces 401s with NO bearer once tokens exist (was: 200 via the
|
|
# removed AdminAuth Tier-1b dev-mode hatch); 200 WITH the admin bearer.
|
|
# 2. GET /workspaces/:id/activity (and /delegations, /approvals/pending) 401
|
|
# with no bearer (was: 200 via the WorkspaceAuth hatch); 200 WITH bearer.
|
|
# 3. GET /org/templates returns the curated set populated by clone-manifest.sh
|
|
# (unauth-readable bootstrap surface — unchanged).
|
|
#
|
|
# Requires: platform running on :8080 with MOLECULE_ENV=development AND
|
|
# ADMIN_TOKEN set (the dev value), with MOLECULE_ADMIN_TOKEN (or
|
|
# ADMIN_TOKEN) exported here so the suite can present the bearer.
|
|
# scripts/dev-start.sh provisions ADMIN_TOKEN locally; the e2e-api CI
|
|
# job sets it on the platform and exports the matching bearer.
|
|
#
|
|
# Usage:
|
|
# MOLECULE_ADMIN_TOKEN=dev-local-admin-token bash tests/e2e/test_dev_mode.sh
|
|
set -euo pipefail
|
|
|
|
# shellcheck source=_lib.sh
|
|
source "$(dirname "$0")/_lib.sh"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
fail() {
|
|
echo "FAIL: $1"
|
|
FAIL=$((FAIL + 1))
|
|
}
|
|
|
|
pass() {
|
|
echo "PASS: $1"
|
|
PASS=$((PASS + 1))
|
|
}
|
|
|
|
check_http() {
|
|
local desc="$1" expected="$2" actual="$3"
|
|
if [ "$actual" = "$expected" ]; then
|
|
pass "$desc (HTTP $actual)"
|
|
else
|
|
fail "$desc — expected HTTP $expected, got $actual"
|
|
fi
|
|
}
|
|
|
|
echo "=== Dev-mode fail-CLOSED regression tests ==="
|
|
echo ""
|
|
|
|
# The platform is fail-closed in every environment now, so the suite MUST have
|
|
# the admin bearer to drive the authenticated (200) assertions. Without it we
|
|
# cannot create / clean up workspaces — bail loudly rather than silently skip.
|
|
ADMIN_BEARER="${MOLECULE_ADMIN_TOKEN:-${ADMIN_TOKEN:-}}"
|
|
if [ -z "$ADMIN_BEARER" ]; then
|
|
echo "FAIL: MOLECULE_ADMIN_TOKEN/ADMIN_TOKEN not set — auth is fail-closed in"
|
|
echo " every environment, so this suite needs the dev ADMIN_TOKEN bearer."
|
|
echo " e.g. MOLECULE_ADMIN_TOKEN=dev-local-admin-token bash $0"
|
|
exit 1
|
|
fi
|
|
ADMIN_AUTH=(-H "Authorization: Bearer $ADMIN_BEARER")
|
|
|
|
e2e_cleanup_all_workspaces
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Section 1 — AdminAuth is fail-CLOSED (dev-mode hatch removed)
|
|
# ----------------------------------------------------------------------
|
|
echo "--- Section 1: AdminAuth fail-closed ---"
|
|
|
|
# No bearer → 401 in dev mode (the removed hatch used to return 200).
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspaces")
|
|
check_http "GET /workspaces (no bearer) is fail-CLOSED" "401" "$R"
|
|
|
|
# With the dev admin bearer → 200.
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspaces" "${ADMIN_AUTH[@]}")
|
|
check_http "GET /workspaces (with admin bearer)" "200" "$R"
|
|
|
|
# Create a workspace (authenticated) so tokens land in the DB.
|
|
R=$(curl -s -w "\n%{http_code}" -X POST "$BASE/workspaces" \
|
|
"${ADMIN_AUTH[@]}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"Dev-Mode-Test","tier":1,"runtime":"external","external":true}')
|
|
CODE=$(echo "$R" | tail -n1)
|
|
BODY=$(echo "$R" | sed '$d')
|
|
check_http "POST /workspaces (create, with admin bearer)" "201" "$CODE"
|
|
|
|
WS_ID=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
if [ -z "$WS_ID" ]; then
|
|
fail "Could not extract workspace ID from create response"
|
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
exit 1
|
|
fi
|
|
|
|
# Ensure a real workspace token exists so AdminAuth sees a live token globally.
|
|
TOKEN=$(echo "$BODY" | e2e_extract_token)
|
|
if [ -z "$TOKEN" ]; then
|
|
e2e_mint_workspace_token "$WS_ID" >/dev/null
|
|
fi
|
|
|
|
# With tokens now in the DB, the bearer-less call STILL 401s (no lazy-bootstrap
|
|
# / dev-mode fall-through), and the authenticated call still 200s.
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspaces")
|
|
check_http "GET /workspaces (after token minted, no bearer) is fail-CLOSED" "401" "$R"
|
|
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspaces" "${ADMIN_AUTH[@]}")
|
|
check_http "GET /workspaces (after token minted, with admin bearer)" "200" "$R"
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Section 2 — WorkspaceAuth is fail-CLOSED (dev-mode hatch removed)
|
|
# ----------------------------------------------------------------------
|
|
echo ""
|
|
echo "--- Section 2: WorkspaceAuth fail-closed ---"
|
|
|
|
# No bearer → 401 (the removed hatch used to return 200).
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"$BASE/workspaces/$WS_ID/activity?type=a2a_receive&limit=50")
|
|
check_http "GET /workspaces/:id/activity (no bearer) is fail-CLOSED" "401" "$R"
|
|
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"$BASE/workspaces/$WS_ID/delegations")
|
|
check_http "GET /workspaces/:id/delegations (no bearer) is fail-CLOSED" "401" "$R"
|
|
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/approvals/pending")
|
|
check_http "GET /approvals/pending (no bearer) is fail-CLOSED" "401" "$R"
|
|
|
|
# Same requests WITH the admin bearer → 200.
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"$BASE/workspaces/$WS_ID/activity?type=a2a_receive&limit=50" "${ADMIN_AUTH[@]}")
|
|
check_http "GET /workspaces/:id/activity (with admin bearer)" "200" "$R"
|
|
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"$BASE/workspaces/$WS_ID/delegations" "${ADMIN_AUTH[@]}")
|
|
check_http "GET /workspaces/:id/delegations (with admin bearer)" "200" "$R"
|
|
|
|
R=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/approvals/pending" "${ADMIN_AUTH[@]}")
|
|
check_http "GET /approvals/pending (with admin bearer)" "200" "$R"
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Section 3 — Template registry populated by setup.sh
|
|
# ----------------------------------------------------------------------
|
|
# GET /org/templates is an unauthenticated bootstrap surface (the template
|
|
# palette must render before the user has a credential) — unchanged.
|
|
echo ""
|
|
echo "--- Section 3: Template registry ---"
|
|
|
|
R=$(curl -s "$BASE/org/templates")
|
|
COUNT=$(echo "$R" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
|
|
if [ "$COUNT" -gt 0 ]; then
|
|
pass "GET /org/templates returns $COUNT template(s)"
|
|
else
|
|
fail "GET /org/templates returned empty list — is clone-manifest.sh run? (bash scripts/clone-manifest.sh manifest.json workspace-configs-templates/ org-templates/ plugins/)"
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Cleanup
|
|
# ----------------------------------------------------------------------
|
|
e2e_delete_workspace "$WS_ID" "Dev-Mode-Test"
|
|
|
|
echo ""
|
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
if [ "$FAIL" -gt 0 ]; then
|
|
exit 1
|
|
fi
|