docs(phase3a): document production identity architecture

New docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md covers the schema, persistent session
design, instructor accounts, the full RBAC matrix, cohort scoping (including the documented
raw-environment-id simplification), student session hardening (explicitly still
classroom/pilot identification, not production auth), CSRF, rate limiting, audit logging,
the API inventory, a security hardening checklist, test results, known limitations, the
future OIDC path, and production HTTPS/cookie expectations — with an explicit BUILT NOW vs.
FUTURE split.

Updates ENGINEERING_SOP.md (new §9/§10, closing a stale '(§9)' forward-reference that dated
back to Phase 2B), LAB_ARCHITECTURE.md status line, README.md, platform/README.md, and
platform/api/README.md (bootstrap-owner usage, new config vars, no more ADMIN_PASSWORD).
This commit is contained in:
2026-08-18 13:36:45 -05:00
parent face0ae132
commit 40692926ee
6 changed files with 496 additions and 62 deletions
+26 -14
View File
@@ -7,13 +7,15 @@ Start here, then go to the component you need:
`docs/LAB_ARCHITECTURE.md`, `docs/PHASE1_PLATFORM_FOUNDATION.md`. Operational reference:
`provisioner/README.md`.
- `api/` — the lightweight Platform API: students, cohorts, enrollments, curriculum, progress,
scenario attempts, student/admin sessions, and the instructor/admin control plane. No k3s, no
`--privileged` — this replaced Phase 2A's "identity" instance, which was a full Lab image
booting an unused k3s cluster just to serve these APIs. Design:
`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md`. Operational reference: `api/README.md`.
scenario attempts, per-instructor accounts, persistent student/instructor sessions, RBAC,
audit logging, and the instructor/admin control plane. No k3s, no `--privileged` — this
replaced Phase 2A's "identity" instance, which was a full Lab image booting an unused k3s
cluster just to serve these APIs. Design: `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` (split +
Postgres migration), `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` (sessions, accounts,
RBAC, audit). Operational reference: `api/README.md`.
- `gateway/` — the only student-facing path to a Lab (HTTP + WebSocket proxy, environment-aware
routing derived from a server-controlled session — not a client-suppliable header/param).
Design: `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` (routing), `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §3 (session ownership).
Design: `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` (routing), `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` (persistent sessions, CSRF).
- `egress-proxy/` — shared allowlisting proxy every Lab's outbound traffic passes through.
- `docker-compose.dev.yml` — the full stack (Postgres, egress proxy, Provisioner, Platform API,
Gateway) on one machine. This is the Phase 1/2A/2B **local dev exception** — production is
@@ -24,14 +26,20 @@ Start here, then go to the component you need:
```bash
cd platform
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
echo "ADMIN_PASSWORD=$(openssl rand -hex 12)" >> .env
docker compose -f docker-compose.dev.yml build
docker compose -f docker-compose.dev.yml up -d
# First run only — create the owner account (no ADMIN_PASSWORD anymore, see
# docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md):
cd api
PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
node bootstrap-owner.js [email protected] "Your Name" "a-real-password-12-chars-plus"
```
No k3s boot to wait for at this layer anymore (Platform API starts instantly) — the whole
platform is reachable at `http://localhost:4200` (the Gateway) right away: students at `/`,
instructors at `/admin`.
instructors at `/admin` (sign in with the owner account you just bootstrapped, then create
further instructor accounts from the Instructors tab).
## Tests
@@ -42,16 +50,20 @@ instructors at `/admin`.
- `test/e2e-phase2b.js` — the classroom acceptance workflow: instructor creates a cohort, adds
students, assigns curriculum, Prepare Labs (concurrent), students complete different
scenarios, instructor resets one, instructor destroys all cohort Labs after class — progress
survives every step. Needs `ADMIN_PASSWORD` set (see `platform/.env`).
survives every step.
- `test/e2e-phase3a.js` — persistent sessions (including a real Platform API restart), instructor
auth (no enumeration, rate limiting), RBAC across owner/admin/instructor, cohort-scoped
instructor access, CSRF enforcement, password change, and audit logging.
Both e2e suites are slow (real k3s boots) and need the full stack up. Neither substitutes for
the manual real-Lab-image checks (node-pty, terminal WebSocket, `kubectl`, scenario validation)
in `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §8 — those need a browser-equivalent client and
All three e2e suites need `OWNER_EMAIL` / `OWNER_PASSWORD` set to a real bootstrapped instructor
account (see "Running the full stack locally" above) — the old `ADMIN_PASSWORD` mechanism is
gone. They're slow (real k3s boots) and need the full stack up. None of them substitute for the
manual real-Lab-image checks (node-pty, terminal WebSocket, `kubectl`, scenario validation) in
`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §8 — those need a browser-equivalent client and
aren't automated here.
## Known limitations
See `provisioner/README.md` (Phase 1), `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §9 (Phase
2A), and `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §13 (Phase 2B) for the full, honest lists —
including in-memory sessions, the single shared admin credential, and exam mode's live
bookkeeping staying Lab-local.
2A), and `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` (Phase 3A, supersedes Phase 2B's
identity limitations) for the full, honest lists.
+41 -15
View File
@@ -1,7 +1,9 @@
# RootNode Academy — Platform API (Phase 2B)
# RootNode Academy — Platform API (Phase 3A)
Lightweight service: no k3s, no `--privileged`, no Lab terminal runtime. See
`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` for the design; this is the operational reference.
`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` for the original split/migration design and
`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` for persistent sessions, per-instructor
accounts, RBAC, and audit logging — this file is the operational reference.
## Configuration reference
@@ -10,25 +12,46 @@ Lightweight service: no k3s, no `--privileged`, no Lab terminal runtime. See
| `PLATFORM_DB_URL` | no (defaults to local dev Postgres) | Postgres connection string |
| `PROVISIONER_URL` | no | Provisioner base URL |
| `PROVISIONER_SHARED_SECRET` | **yes** | Internal Provisioner auth — process refuses to start without it |
| `ADMIN_PASSWORD` | **yes** | Instructor login credential — process refuses to start without it. Verified via scrypt+timing-safe compare, never stored or logged in plaintext |
| `PLATFORM_API_PORT` | no | Listen port, default 4300 |
| `FRONTEND_DIST_DIR` | no | Where the built frontend lives — set by the Dockerfile, override for local non-Docker dev |
| `SCENARIOS_DIR` / `BUNDLES_DIR` / `CURRICULUM_PATH_FILE` | no | Override the default `scenarios/` locations, mainly for local dev outside Docker |
| `STUDENT_SESSION_TTL_HOURS` / `STUDENT_SESSION_IDLE_HOURS` | no | Default 8h / 2h |
| `INSTRUCTOR_SESSION_TTL_HOURS` / `INSTRUCTOR_SESSION_IDLE_HOURS` | no | Default 12h / 2h |
| `COOKIE_SECURE` | no | Set to `1` once served over real HTTPS — adds `Secure` to every session/CSRF cookie |
| `TRUST_PROXY` | no | Set to `1` once fronted by a trusted reverse proxy — honors `X-Forwarded-For` for `req.ip` (audit `source_ip`, login rate limiting) |
There is no `ADMIN_PASSWORD` anymore — instructor accounts live in Postgres (`instructors`
table), created via `bootstrap-owner.js` for the first one and the admin portal after that.
## Local dev
```bash
cd platform
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
echo "ADMIN_PASSWORD=$(openssl rand -hex 12)" >> .env
docker compose -f docker-compose.dev.yml build
docker compose -f docker-compose.dev.yml up -d
cd api
PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
node bootstrap-owner.js [email protected] "Your Name" "a-real-password-12-chars-plus"
```
Everything is reachable through the Gateway at `http://localhost:4200` — students at `/`,
instructors at `/admin`. Platform API itself isn't meant to be hit directly by a browser (same
boundary as the Provisioner relative to Platform API), though nothing stops you from curling
it directly at `:4300` for local debugging.
instructors at `/admin` (sign in with the bootstrapped owner). Platform API itself isn't meant
to be hit directly by a browser (same boundary as the Provisioner relative to Platform API),
though nothing stops you from curling it directly at `:4300` for local debugging.
## Owner bootstrap
```bash
node bootstrap-owner.js <email> <display_name> [password]
```
Prefer `BOOTSTRAP_OWNER_PASSWORD` over the positional argument outside a throwaway local test —
CLI args can leak into shell history and `ps`. Refuses to run if an active owner already exists
unless the target email belongs to an existing instructor (the intended recovery path if a
platform is ever locked out with zero owners: point this at a known instructor account to
promote them).
## SQLite migration
@@ -42,17 +65,20 @@ docker run --rm -v <identity-data-volume>:/data \
Always backs up the SQLite file first, is safe to re-run, and writes a JSON report — see
`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §2/§3 for the full guarantees and rollback
procedure.
procedure. This step is about academic data (students/cohorts/progress) — it has nothing to do
with instructor accounts, which are Phase 3A-native and start empty until you bootstrap one.
## Tests
`platform/test/e2e-phase2a.js` (student-flow regression, cookie-based) and
`platform/test/e2e-phase2b.js` (the classroom acceptance workflow: cohort → students →
curriculum → Prepare Labs → complete scenarios → reset → destroy-labs → progress survives).
Both need the full stack up and `ADMIN_PASSWORD` set — see `platform/README.md`.
`platform/test/e2e-phase2a.js` (student-flow regression), `platform/test/e2e-phase2b.js` (the
classroom acceptance workflow), and `platform/test/e2e-phase3a.js` (persistent sessions
including a real restart, instructor auth, RBAC, cohort scoping, CSRF, audit log). All three
need the full stack up and `OWNER_EMAIL` / `OWNER_PASSWORD` set to a bootstrapped account — see
`platform/README.md`.
## Known limitations
See `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §13 — in-memory sessions (a restart logs
everyone out), exam mode's live bookkeeping stays Lab-local, single shared admin credential
rather than per-instructor accounts.
See `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` "Known limitations" — sessions are
Postgres-backed (survive a restart) but there's no distributed revocation channel to the
Gateway's short-lived whoami cache, exam mode's live bookkeeping stays Lab-local, and OIDC/SSO
is a documented adapter interface, not a working integration yet.