Files
ducoterra 1734d81e48 phase: 11_admin_login
**Verified (final pass, phase 11 — all 7 tasks already in `complete/`)**
- Gate from the wire on a restarted dev server: `/system`→`303 /login?next=/system` (`/llm?range=1h`→`/login?next=/llm%3Frange%3D1h`), `/api/summary`+`/api/config`→`401 {"ok":false,…}`, `/api/health`→`200`; signed-in `GET` of all six pages `200`, `/login`→`303 /`.
- Auth flows on a throwaway server (`:8199`, `/tmp` DB — dev `auth.verifier` untouched): wrong pw `401`, `testpass` `200`, password change `200`, **old cookie `401` / new cookie `200`**, old pw `401` / new pw `200`, short new `422`, wrong current `403`, cookie write without CSRF `403`, logout `200`; 5 failures then `429 retry-after: 60`; `login-denied`/`password-changed`/`password-denied` events; `meta` = the two auth keys.
- Fail-closed boot (no seed, empty DB): **exit 3**, message names `DASH_ADMIN_PASSWORD`, `auth.verifier`, and the `DELETE FROM meta …` reset. `dev-server.sh status` + the Containerfile `HEALTHCHECK` python one-liner both `200`/exit 0 unauthenticated.
- Live payloads unchanged (phases 01–10 walk): `/api/summary` keys + 8 alerts + hosts 3/2 live; UPS `simulated:false` with charge/load/runtime severity; deskwork `nvme0n1` 51 °C/19 159 h/11 % wear; gateway `smartctl_available:false`; driveripper `unreachable` stub.

**Tests/lint (all green)**: `bash .agents/validate.sh` → exit 0 — `1140 passed, 11 skipped`, coverage `98.62%` (floor 94), ruff `All checks passed!`, pyright `0 errors`, `node --check` clean, repo hygiene clean. `uv run pytest -m live --live` → `8 passed, 3 failed`, all `driveripper: 10.1.0.126:9120 … ConnectError` (pre-existing, auth-free; probe confirms nothing listening).

**Completion criteria**: all met except the live-suite caveat above — installing the driveripper agent needs root on that host (no `ops` grant), so per the live-infra rule I made **no live change**; task 07's five screenshots (`auth-login/-rejected/-locked-out/-signed-in-overview/-settings.png`) remain the browser evidence, and the login/rotation/lockout flows were re-proved over HTTP this pass.

**Docs**: README — fixed "only place the CSRF token is published" (login/password responses mint one), added the undocumented `DASH_RELOAD` row, stated that Sign out ends only this browser's cookie. `AGENTS.md` — new known gap 8 (stateless logout cannot revoke a copied token), gap-4 dev-store sizes dated, fleet section notes `data/` is gitignored. `api.py` — same `status` docstring imprecision fixed. Verified the 4 `DASH_*` rows, reset command, seed-wins rule and invariant 12 against `app.py`/`config.py`.

**Deviations**: no dependency bump (`uv pip list --outdated` advisory: certifi/fastapi/starlette — phase added none); verification notes appended to `data/logs/NOTES-auth.md` §10.

**Next pending phase**: `12_nut_self_test_verdict`.
2026-10-19 07:45:44 -04:00

39 lines
1.9 KiB
Python

"""The admin credential the HTTP suite boots with, and the cost it hashes it at.
Phase 11 made every app start need a password: with no `auth.verifier` row and no
`DASH_ADMIN_PASSWORD` the lifespan raises instead of serving the fleet (see
`auth.load_auth_state`). So every test that runs a lifespan passes one, the same way the
operator's `.env.dev` does, and `testpass` is the phase's local-testing password — never a
production fallback.
The cost is the other half. The real 600 000 rounds cost ~0.3 s per hash, and the HTTP suite
starts a server a hundred-odd times; hashing at 1 000 rounds is the documented purpose of
`DASH_PBKDF2_ITERATIONS` and keeps that overhead at microseconds. The real figure is pinned
where it belongs — `tests/test_auth.py::test_the_default_cost_is_the_owasp_figure` and
`tests/test_config.py::test_the_auth_defaults_are_a_locked_door_and_a_long_session` — so
lowering it here cannot lower it there.
Deliberately not a fixture: `Settings(...)` is built inside helper functions and module-level
setup as well as inside tests, and a constant imports everywhere without dragging a request
scope along.
"""
from __future__ import annotations
#: The password every test app is seeded with (and, from task 03 on, signs in with).
ADMIN_PASSWORD = "testpass"
#: PBKDF2 rounds for the suite. See the module docstring; do not use this in a deployment.
FAST_PBKDF2 = 1_000
def auth_settings() -> dict[str, object]:
"""The `Settings` kwargs a test app needs in order to boot at all.
A server with neither a stored verifier nor an `admin_password` **refuses to start**
(task 02, invariant 11), and every `create_app()` in the suite runs a lifespan. Spreading
this into the `Settings(...)` calls keeps that a one-line change in this file instead of an
edit in every fixture that builds its own settings object.
"""
return {"admin_password": ADMIN_PASSWORD, "pbkdf2_iterations": FAST_PBKDF2}