ddaf3c6cee
- Add JWT token auth to OPC WebSocket (unauthenticated → 403, per-user tracking) - Externalize Transmission RPC credentials to TRANSMISSION_USER/PASS env vars - Remove hardcoded SECRET_KEY fallback from dev compose - Rate-limit passkey register options endpoint at 5/minute - Add PDD (docs/improvement-plan.md) and sprint specs (specs/) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
123 lines
5.7 KiB
Markdown
123 lines
5.7 KiB
Markdown
# Sprint 02 — Auth Hardening
|
|
|
|
**Depends on:** S00, S01 (need stable environment to test auth changes against)
|
|
**Duration:** ~10h
|
|
**Goal:** Add Pydantic validation to raw-JSON endpoints, bind passkey challenges to sessions, gate sensitive log endpoints behind admin role, fix fragile cross-module patterns.
|
|
|
|
---
|
|
|
|
## S02.1 — Add max_length to LoginRequest
|
|
|
|
- **Files:** `dashboard/backend/routers/auth.py:22-25`
|
|
- **Estimate:** 0.5h
|
|
- **Done means:** `LoginRequest` has `max_length=128` on username, `max_length=1024` on password. Requests exceeding limits get 422 with clear field error.
|
|
- **Verify by:**
|
|
1. `curl -X POST /api/auth/login -d '{"username":"'$(python -c 'print("a"*200)')'","password":"test"}'` → 422, error mentions max_length
|
|
2. Normal login with valid credentials → 200
|
|
- **Risk:** None. Pydantic validation happens before password hashing.
|
|
|
|
- [ ] S02.1 — Add max_length to LoginRequest
|
|
|
|
## S02.2 — Add Pydantic models to RBAC override endpoints
|
|
|
|
- **Files:** `dashboard/backend/routers/auth.py:242-259,303-326`
|
|
- **Estimate:** 2h
|
|
- **Done means:** `rbac_set_override` and `rbac_update_role` use Pydantic models (`RbacOverrideRequest`, `RbacUpdateRoleRequest`) instead of `await request.json()`. Unknown fields are rejected. Existing RBAC functionality unchanged.
|
|
- **Verify by:**
|
|
1. Send valid override payload → 200, override saved
|
|
2. Send payload with unknown field `sidebar_links` → 422 validation error
|
|
3. Send payload missing required `pages` field → 422
|
|
4. Existing RBAC tests pass (`test_rbac.py`, `test_auth_flow.py`)
|
|
- **Risk:** Audit frontend `Settings.svelte` to confirm it only sends expected fields.
|
|
|
|
- [ ] S02.2 — Add Pydantic models to RBAC override endpoints
|
|
|
|
## S02.3 — Add Pydantic models to passkey endpoints
|
|
|
|
- **Files:** `dashboard/backend/routers/passkey.py:78,127,188`
|
|
- **Estimate:** 2h
|
|
- **Done means:** `register_verify`, `login_verify`, `delete` use Pydantic models matching WebAuthn payload structure. Malformed payloads get 422 instead of 500.
|
|
- **Verify by:**
|
|
1. Send valid WebAuthn registration payload → 200
|
|
2. Send malformed payload (missing `response` field) → 422 with field-level error
|
|
3. Existing passkey tests pass (`test_passkey.py`)
|
|
- **Risk:** WebAuthn payloads have specific structure — cross-reference with the `webauthn` library's expected types.
|
|
|
|
- [ ] S02.3 — Add Pydantic models to passkey endpoints
|
|
|
|
## S02.4 — Bind passkey challenges to sessions
|
|
|
|
- **Files:** `dashboard/backend/routers/passkey.py:29-55`
|
|
- **Estimate:** 1.5h
|
|
- **Done means:** Passkey challenges are stored keyed by a session-bound identifier (not globally). `_get_challenge` only returns the challenge if the session matches.
|
|
- **Verify by:**
|
|
1. User A requests challenge → challenge stored with user A's session ID
|
|
2. User B (different session) tries to verify with user A's challenge → 400 "invalid challenge"
|
|
3. User A verifies with own challenge → success
|
|
4. Existing passkey tests pass
|
|
- **Risk:** Need session identifier for unauthenticated users during login flow. Use server-generated `challenge_id` returned in options, required in verify — simplest and stateless.
|
|
|
|
- [ ] S02.4 — Bind passkey challenges to sessions
|
|
|
|
## S02.5 — Gate audit log endpoint behind admin role
|
|
|
|
- **Files:** `dashboard/backend/routers/system.py:82-116`
|
|
- **Estimate:** 1h
|
|
- **Done means:** `/api/system/audit-log` requires admin role. Non-admin users get 403. Log collection unchanged.
|
|
- **Verify by:**
|
|
1. Admin user requests audit log → 200, entries returned
|
|
2. Non-admin user requests audit log → 403
|
|
- **Risk:** Frontend Security page must handle 403 gracefully if current user isn't admin.
|
|
|
|
- [ ] S02.5 — Gate audit log endpoint behind admin role
|
|
|
|
## S02.6 — Gate security log endpoint behind admin role
|
|
|
|
- **Files:** `dashboard/backend/routers/security.py:198`
|
|
- **Estimate:** 1h
|
|
- **Done means:** `/api/security/logs` requires admin role. Non-admin users get 403.
|
|
- **Verify by:**
|
|
1. Admin user requests security logs → 200
|
|
2. Non-admin user requests security logs → 403
|
|
- **Risk:** Same as S02.5 — frontend must handle 403 gracefully.
|
|
|
|
- [ ] S02.6 — Gate security log endpoint behind admin role
|
|
|
|
## S02.7 — Fix fragile opc_db.json.dumps() pattern
|
|
|
|
- **Files:** `dashboard/backend/services/agent_executor.py:273,307`
|
|
- **Estimate:** 0.5h
|
|
- **Done means:** `agent_executor.py` imports `json` directly and uses `json.dumps()` instead of `opc_db.json.dumps()`.
|
|
- **Verify by:**
|
|
1. `grep "opc_db.json" dashboard/backend/services/agent_executor.py` → no match
|
|
2. OPC task creation and agent execution still work end-to-end
|
|
- **Risk:** None — pure refactor.
|
|
|
|
- [ ] S02.7 — Fix fragile opc_db.json.dumps() pattern
|
|
|
|
## S02.8 — Add COOKIE_SECURE startup warning
|
|
|
|
- **Files:** `dashboard/backend/auth_service.py:23`, `dashboard/backend/main.py`
|
|
- **Estimate:** 0.5h
|
|
- **Done means:** If `COOKIE_SECURE=False` at startup, `logger.warning()` fires explaining the risk. `ALLOW_INSECURE_COOKIES=true` env var suppresses the warning.
|
|
- **Verify by:**
|
|
1. Start without `ALLOW_INSECURE_COOKIES` → warning in logs
|
|
2. Start with `ALLOW_INSECURE_COOKIES=true` → no warning
|
|
- **Risk:** Low — purely additive, doesn't change cookie behavior.
|
|
|
|
- [ ] S02.8 — Add COOKIE_SECURE startup warning
|
|
|
|
---
|
|
|
|
## Exit criteria
|
|
|
|
- [ ] `LoginRequest` rejects usernames > 128 chars with 422
|
|
- [ ] RBAC endpoints reject unknown fields with 422
|
|
- [ ] Passkey endpoints reject malformed payloads with 422
|
|
- [ ] Passkey challenges are session-bound (cross-session replay fails)
|
|
- [ ] Audit log and security log endpoints return 403 for non-admin
|
|
- [ ] `grep "opc_db.json" agent_executor.py` → no match
|
|
- [ ] COOKIE_SECURE warning fires at startup unless suppressed
|
|
- [ ] All backend tests pass
|
|
- [ ] All frontend tests pass
|