Skip to content

Commit ac8b561

Browse files
feat(e2e): guarded ephemeral test-account API for CI integration tests vs prod
POST/DELETE /internal/e2e/account — lets CI mint a real ephemeral test account on prod per run, exercise integration flows, then reap. Security: - X-E2E-Token constant-time guard; unset/wrong token → 404 (inert + no existence leak). Ships INERT (no E2E_ACCOUNT_TOKEN config = off by default). - Create makes an is_test_cohort=true team+user (live worker skip-guards neuter billing/churn/email/quota), tier ≤ pro — team/growth rejected 400 (Team gated). Returns a JWT_SECRET-signed session JWT (no Brevo dependency). - Reap ONLY deletes is_test_cohort teams — a real team → 403 not_test_cohort (the load-bearing safety test: TestE2EAccount_Reap_NonCohortTeam_Forbidden). - Rate-limited (fail-open), audit-logged (e2e.account.created/reaped), metric instant_e2e_account_total{op,result}. Foundation for running the real-backend integration suite against production in CI with on-the-fly accounts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 94cc58c commit ac8b561

14 files changed

Lines changed: 1519 additions & 0 deletions

dump.rdb

3.65 KB
Binary file not shown.

internal/config/config.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,29 @@ type Config struct {
263263
// WORKER_INTERNAL_JWT_SECRET in BOTH the api and the worker
264264
// (same value, generated via `openssl rand -hex 32`).
265265
WorkerInternalJWTSecret string
266+
267+
// E2EAccountToken is the shared secret that guards the CI-only
268+
// ephemeral-test-account surface (POST/DELETE /internal/e2e/account).
269+
// CI mints real test-cohort accounts against PRODUCTION to run
270+
// integration tests, then reaps them — that is the only thing this
271+
// token authorizes.
272+
//
273+
// INERT BY DEFAULT (flag-protection): when this is empty, BOTH e2e
274+
// routes return 404 for every request, hiding the endpoint's
275+
// existence entirely. The endpoint cannot mint or reap a single
276+
// account until an operator sets E2E_ACCOUNT_TOKEN — so the surface
277+
// ships safe-by-default and is only "armed" in the environments
278+
// (CI/prod) where the secret is wired. The caller authenticates by
279+
// sending the exact value in the X-E2E-Token request header; the
280+
// handler does a crypto/subtle constant-time compare and 404s on any
281+
// mismatch (never 401/403 — a distinguishable status would leak that
282+
// the route exists).
283+
//
284+
// Distinct secret from JWTSecret and WorkerInternalJWTSecret: this
285+
// one authorizes account *creation/destruction*, a strictly more
286+
// dangerous capability than session-signing, so it gets its own key
287+
// and its own k8s Secret entry (generate via `openssl rand -hex 32`).
288+
E2EAccountToken string
266289
}
267290

268291
// ErrMissingConfig is returned when a required env var is absent.
@@ -427,6 +450,9 @@ func Load() *Config {
427450
cfg.SendGridWebhookKey = os.Getenv("SENDGRID_WEBHOOK_PUBLIC_KEY")
428451

429452
cfg.WorkerInternalJWTSecret = strings.TrimSpace(os.Getenv("WORKER_INTERNAL_JWT_SECRET"))
453+
// E2E_ACCOUNT_TOKEN: empty = the /internal/e2e/* surface is inert
454+
// (every call 404s). See Config.E2EAccountToken for the full posture.
455+
cfg.E2EAccountToken = strings.TrimSpace(os.Getenv("E2E_ACCOUNT_TOKEN"))
430456
cfg.DeployDomain = getenv("DEPLOY_DOMAIN", "instant.dev")
431457
cfg.ComputeProvider = getenv("COMPUTE_PROVIDER", "noop")
432458
cfg.KubeNamespaceApps = getenv("KUBE_NAMESPACE_APPS", "instant-apps")

internal/config/config_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func allKeys() []string {
6868
"BREVO_WEBHOOK_SECRET", "SES_SNS_SUBSCRIPTION_ARN",
6969
"SENDGRID_WEBHOOK_PUBLIC_KEY",
7070
"WORKER_INTERNAL_JWT_SECRET", "ADMIN_PATH_PREFIX",
71+
"E2E_ACCOUNT_TOKEN",
7172
}
7273
}
7374

@@ -196,6 +197,19 @@ func TestConfig_IsServiceEnabled(t *testing.T) {
196197
}
197198
}
198199

200+
func TestLoad_E2EAccountToken(t *testing.T) {
201+
// Unset → empty (inert-by-default: the /internal/e2e/* surface 404s).
202+
applyBaselineEnv(t, nil)
203+
if got := Load().E2EAccountToken; got != "" {
204+
t.Errorf("E2EAccountToken default: want empty (inert), got %q", got)
205+
}
206+
// Set (with surrounding whitespace) → trimmed value.
207+
applyBaselineEnv(t, map[string]string{"E2E_ACCOUNT_TOKEN": " secret-token "})
208+
if got := Load().E2EAccountToken; got != "secret-token" {
209+
t.Errorf("E2EAccountToken: want trimmed 'secret-token', got %q", got)
210+
}
211+
}
212+
199213
func TestLoad_HappyPath_AppliesDefaults(t *testing.T) {
200214
applyBaselineEnv(t, nil)
201215
cfg := Load()

0 commit comments

Comments
 (0)