- PROJECT_PLAN.md: the decision log of record through D-042 and the M0.1 slice - docs/specs: SPEC-002…009 — routing · door · identity/sessions · /setup · enroll/login · roles/chain/recovery · module registry · the web layer & PWA (plus docs/specs/README.md, the index) - AGENT.md: the working rules — commit AND push at every submilestone (gate green), Conventional Commits, never commit secrets - README.md: the quickstart, the first-run/SSH-tunnel steps and the localhost/loopback caveat (SPEC-009 §13 Q-3): with an https base URL the CSRF/session cookies are Secure, so reach the app at localhost/127.0.0.1 - config.example.yaml: the settings template (D-035)
158 lines
7.4 KiB
Markdown
158 lines
7.4 KiB
Markdown
# SPEC-003 — the core door (`internal/core/server` + `httpx`)
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| **Status** | **Implemented** (2026-10-13) — see §10; `build`/`vet`/`test`/`-race` green · static binary · live-smoked |
|
|
| **Created** | 2026-10-13 |
|
|
| **Author** | OpenArchitect |
|
|
| **Target milestone** | **M0** |
|
|
| **Governing decisions** | **D-002** · **D-003** · **D-025** · **D-031** · **D-039** · `PROJECT_PLAN.md` §4/§12 |
|
|
| **Related** | SPEC-002 (routing) · SPEC-004 (identity) |
|
|
|
|
---
|
|
|
|
## 1. Goal
|
|
|
|
The **single HTTP entry** — *the door* — that turns an inbound request into a dispatched module
|
|
function, applying **identity / session / role** and all cross-cutting concerns. The door
|
|
**resolves and gates**; a **module never sees a request it shouldn't** (D-002, D-039).
|
|
|
|
---
|
|
|
|
## 2. Non-goals
|
|
|
|
- The routing model → **SPEC-002**.
|
|
- Identity/session storage, users, roles, the chain → **SPEC-004**.
|
|
- The `/setup`, enroll, login, recovery **flows** → SPEC-005+.
|
|
|
|
---
|
|
|
|
## 3. Structural changes
|
|
|
|
| Path | Purpose |
|
|
|---|---|
|
|
| `cmd/myday/main.go` | assemble config → core → discover modules → serve |
|
|
| `internal/core/server/` | the door pipeline + dispatch |
|
|
| `internal/core/httpx/` | session cookie · client IP (trusted proxies) · `RedactPath` · security headers · CSRF |
|
|
| `internal/core/middleware/` | correlation id · rate limit · audit |
|
|
|
|
---
|
|
|
|
## 4. The door pipeline
|
|
|
|
```
|
|
request
|
|
1. correlation id — generate a UUID; put it on the context and in every log line
|
|
2. client IP — resolve from `server.trusted_proxies`; SET `X-Forwarded-For` (never append) [A6]
|
|
3. security headers — X-Content-Type-Options · Referrer-Policy · a strict CSP · HSTS (when https)
|
|
4. routing.Dispatch() — the pure model (SPEC-002)
|
|
5. switch on Kind:
|
|
Redirect → 302
|
|
NotFound → 404
|
|
Setup → the setup wizard, ONLY in setup mode; else 404
|
|
Enroll → the enrollment screen (if already enrolled → redirect)
|
|
Route/Action → [the identity gate]
|
|
a. load the session cookie → validate:
|
|
exists · not expired · not revoked · **non-empty session id** [A2]
|
|
b. session.user_id == the resolved GUID's user (the identity match)
|
|
c. missing/invalid → KindLogin (login screen)
|
|
d. ROLE gate: does the user's role permit this function?
|
|
e. build the RequestContext → dispatch to the module function
|
|
6. audit — every auth event + every mutation (A11)
|
|
7. rate limit — `/{GUID}`, `/access`, `/setup` (A3)
|
|
```
|
|
|
|
- Before dispatching, the door attaches **`audit.Meta{ActorUserID · ClientIP · CorrelationID}`** to
|
|
the `context.Context`, so services can write their audit rows (SPEC-004 §5, ruling **R-1**).
|
|
|
|
**The door renders no dynamic HTML** — it resolves, gates, and hands off.
|
|
|
|
---
|
|
|
|
## 5. `RequestContext` (in-process hand-over)
|
|
|
|
```go
|
|
type RequestContext struct {
|
|
Workflow string // PARAM1 classification
|
|
UserID int64 // the resolved user (0 when anonymous)
|
|
Principal string // "user:<id>" — IDENTIFICATION only, never a credential
|
|
Function string // the resolved module function / action
|
|
Page string // the joined remainder
|
|
Correlation string // the request's correlation id
|
|
ClientIP string // resolved client IP
|
|
Location string // IP-derived, AUDIT ONLY (D-031) — no tracking
|
|
}
|
|
```
|
|
|
|
- Carries **identification, never a credential**.
|
|
- `Principal` and the credential (session cookie) are **separate** concerns (D-001).
|
|
|
|
---
|
|
|
|
## 6. Session cookie & CSRF
|
|
|
|
- Cookie `myday_session`: **`HttpOnly` · `Secure` · `SameSite=Lax` · `Path=/`**.
|
|
- Opaque random id; the server-side row is authoritative (SPEC-004).
|
|
- **CSRF:** a per-session token on every **action** (`do`/`action`) — hidden field, verified
|
|
server-side (A8). The action fields are *not* a security control on their own.
|
|
- **Logout** revokes the session row.
|
|
|
|
---
|
|
|
|
## 7. Loopback enforcement (D-025)
|
|
|
|
- In **setup mode**, if `server.listen` is **not loopback**, the process **refuses to start**
|
|
(fail-closed). The operator bootstraps over an **SSH tunnel** to `127.0.0.1:8080`.
|
|
- Once the top admin exists (`setup: false`), a public bind is allowed.
|
|
|
|
---
|
|
|
|
## 8. Logging & redaction
|
|
|
|
- `slog`, structured, with the **correlation id** on every line.
|
|
- The access log records the raw path + `PARAMS`; the **GUID is public** (a username) but any
|
|
**credential that rides the path is redacted** (`RedactPath`) — A8/A11.
|
|
- The door logs the resolved `(workflow, module, function)`; a module logs its own steps.
|
|
|
|
---
|
|
|
|
## 9. Acceptance criteria
|
|
|
|
| # | Criterion |
|
|
|---|---|
|
|
| AC-1 | `/` → 302 `/home`; `/holy-shit` → 404 |
|
|
| AC-2 | `/home` renders with **no session** and **no DB** lookup |
|
|
| AC-3 | `/{GUID}` with no session → login; with a valid session → the module function |
|
|
| AC-4 | A session whose `user_id` ≠ the GUID's user is **rejected** |
|
|
| AC-5 | An **empty / missing session id** is rejected on **every** path (A2) |
|
|
| AC-6 | A `do`/`action` request without a valid CSRF token is **rejected** |
|
|
| AC-7 | In setup mode with a non-loopback `listen`, the process **exits with an error** (D-025) |
|
|
| AC-8 | Every response carries a correlation id, echoed in the log; path credentials are redacted |
|
|
| AC-9 | The rate limiter returns **429** on the auth-sensitive routes (A3) |
|
|
|
|
---
|
|
|
|
## 10. Implementation notes (accepted rulings)
|
|
|
|
Built 2026-10-13. `build` · `vet` · `test` · `-race` green; **static CGO-free** binary; live smoke
|
|
verified (`/`→302 · `/home`→200 · `/setup`→200 · `/holy-shit`→404 · correlation id in header **and**
|
|
log · loopback enforcement · fail-closed secrets). `internal/core/routing` purity intact.
|
|
|
|
| # | Builder choice | Ruling |
|
|
|---|---|---|
|
|
| D-1 | `gopkg.in/yaml.v3` + `KnownFields(true)` (unknown config key = error) | **Accepted** — pure-Go, static-safe, fail-closed. |
|
|
| D-2 | Secret strength: `session_key`/`cookie_secret`/`csrf_key` ≥ 32 bytes; `totp_encryption_key` = 32 bytes; `CHANGE_ME` rejected in productive mode | **Accepted** — to be documented in the config reference. |
|
|
| D-3 | CSRF = stateless `HMAC-SHA256(csrf_key, "myday.csrf.v1:"+session_id)` | **Accepted** — no extra table; dies with the session. |
|
|
| D-4 | In-memory fixed-window rate limiter (30/min; classes `access`·`setup`·`guid`) | **Accepted.** |
|
|
| D-5 | `RoleGate` **seam**, defaulting to *allow any authenticated user* | **Accepted** — the policy lands in **SPEC-007/008**. |
|
|
| D-6 | Env: config **path** via `MYDAY_CONFIG` only | **Accepted**; per-key env override deferred. |
|
|
| D-7 | `RequestContext` = **exactly** §5 (no `Module` field) | **Accepted** — module identity stays on `Resolution`. |
|
|
|
|
**Rulings on the flagged items:**
|
|
|
|
| # | Item | Ruling |
|
|
|---|---|---|
|
|
| **R-4** | `auth.Mint/Rotate/Revoke` hard-code the **owning user** as actor. | **Use the context actor** (creator/approver); the affected user is the **subject** — see SPEC-004 §8. |
|
|
| **R-5** | **Pre-session CSRF** — `/setup`, `/access` and the enroll/login actions run before a session, so §6's session-bound CSRF doesn't cover them. | **Must be added** — a signed **double-submit** cookie for anonymous forms. Applies to **SPEC-005/006/007**; tracked as **A15**. |
|
|
| — | `RequestContext.Location` always empty (D-031) | **Accepted** — the seam exists; the IP-geo lookup is deferred. |
|
|
| — | `RedactPath` over-redacts a segment after a credential-like name | **Accepted** — log-only; no leak. |
|