security: Sprint 00 — critical security fixes (OPC WS auth, Transmission creds, SECRET_KEY, passkey rate limit)

- 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>
This commit is contained in:
Gan, Jimmy
2026-05-03 13:31:25 +08:00
parent 5c7d185953
commit ddaf3c6cee
16 changed files with 987 additions and 21 deletions
+122
View File
@@ -0,0 +1,122 @@
# 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