Commit Graph
2 Commits
Author SHA1 Message Date
ced4568 ae75bb4cbe feat(auth): add server-controlled student and admin sessions
Replaces the Phase 2A pilot ?sid= query param with an HttpOnly,
SameSite=Lax session cookie. Gateway now derives student identity from
the session (via Platform API's /api/session/whoami) instead of trusting
a client-suppliable id, and checks ownership on every proxied HTTP and
WebSocket Lab request, not just the initial navigation.

Fixes a routing bug found during testing: the fallback route was using
'does this session own a running environment' to decide whether an
unprefixed /api/* call belonged to the Lab UI or the dashboard — true in
both places. Replaced with an explicit LAB_OWNED_PATHS allowlist of the
exact unprefixed paths the existing Lab UI actually calls.

e2e-phase2a.js rewritten to use a cookie jar instead of headers/query
params, with explicit impersonation-rejection tests added. 10/10 passing.
2026-08-18 12:22:58 -05:00
ced4568andClaude Sonnet 5 c89e39c5ee feat(gateway): add environment-aware HTTP and websocket routing
New internal component (platform/gateway/) — the only student-facing path to
disposable Lab environments. Browsers never reach a Lab container, the
Provisioner, or Postgres directly.

Routes:
- /api/environment/{status,launch,reset} — student-facing glue (X-Student-Id),
  calls the Provisioner internally (shared secret never reaches the browser).
  Reuses an existing non-terminal environment on launch rather than always
  provisioning a new one; reset destroys+recreates the same environment row.
- /lab/<token>/* — HTTP+WS proxy into that one student's Lab container,
  resolved by opaque routing token via a new Provisioner endpoint
  (GET /environments/by-token/:token). No raw container IP/port ever reaches
  the browser. Rejects invalid tokens (404), unready environments (202/503),
  and cross-student access via an optional ?sid= ownership check (403) — see
  docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md "Security" for why token-alone,
  student-id-alone, and env-id-alone are each individually insufficient and
  this is explicitly pilot-grade, not production auth.
- everything else — proxied to the persistent identity/curriculum backend
  (roster, login, curriculum, progress, platform-shell frontend), or to the
  student's own Lab if a valid lab-session cookie is present.

Routing design note: the existing Lab UI makes root-relative calls
(fetch('/api/scenarios'), the /shell-ws WebSocket) with no idea it might be
served under a /lab/<token>/ prefix — preserving that UI unmodified was a
hard requirement. Solved with an HttpOnly, SameSite=Lax cookie set on the
first /lab/<token>/ response: every later request from that browser carries
it, so the unmodified Lab UI's own unprefixed calls resolve correctly once a
student has navigated into a lab session, with zero changes to its code.
/api/environment/* is explicitly exempted from cookie-based routing so it
never gets hijacked by an active lab session.

Also added platform/docker-compose.dev.yml (the full local stack: Postgres +
egress-proxy + Provisioner + a persistent "identity" instance + Gateway) and
platform/README.md tying the components together. The "identity" instance
required an explicit `network_mode: bridge` — under Compose's own
auto-created bridge network, k3s failed at startup with "no default routes
found in /proc/net/route"; documented as a known limitation, not fully
root-caused.

Found and fixed during testing: express.json() applied globally broke
http-proxy's re-streaming of proxied POST bodies (e.g. POST /api/roster hung
forever, since the body had already been consumed by the parser before
proxy.web() tried to forward it). Removed — none of the Gateway's own routes
actually need a parsed body.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-18 11:15:55 -05:00