Commit Graph

2 Commits

Author SHA1 Message Date
Nora 85791e0df7 feat(sp36): absorb 20 admin endpoints into api_routers/admin.py
Moves the entire /api/admin/* namespace (audit-log ×2, db/rotate-key ×1,
backup ×10, scheduler ×6, reload-config ×1) out of api.py and into the
existing api_routers/admin.py, which already owned /api/admin/validate-provider.

- api.py: 4294 → 3547 LOC (Δ -747). Removed the orphaned # --- separator
  left behind by the cut, the orphaned 'return {ok,loaded,errors}'
  tail of reload-config, and the now-unused cyclone.clearhouse.InboundFile
  top-level import.
- api_routers/admin.py: 60 → 851 LOC. Added the imports the moved
  routes need (json, logging, threading, time, AuditEvent, verify_chain,
  InboundFile) and stripped the redundant top-level imports of
  symbols that the route bodies reach via the inline _db_crypto /
  _secrets / _backup_svc_mod / _backup_sched_mod / _scheduler_mod
  / _audit aliases (those aliases stay — tests rely on being able to
  monkeypatch cyclone.api_routers.admin._X.method). Hoisted the inline
  'import time' from inside pull_inbound to module scope. Dropped
  the redundant 'import threading as _threading' alias — top-level
  'import threading' already covers it.
- tests/test_api_rotate_key.py: 4 monkeypatch targets migrated from
  cyclone.api._db_crypto / cyclone.api._secrets to
  cyclone.api_routers.admin._db_crypto / cyclone.api_routers.admin._secrets
  to match the new home of the rotate-key endpoint. Lock acquire/release
  also migrated.

The non-admin /api/config/* and /api/payers/{id}/summary routes that
bracketed the moved admin blocks stay in api.py — they're extracted
in Tasks 5 & 6.

Behaviour-preserving: pytest = 20 failed / 1253 passed / 10 skipped
= baseline match. Admin-only subset (audit-log + backup + scheduler +
rotate-key + reload-config) = 34 passed.
2026-07-06 14:01:37 -06:00
sp15-bot 47902fd6b2 feat(sp15): SQLCipher key rotation via PRAGMA rekey
Adds in-place key rotation for the encrypted DB at rest (HIPAA
sec.164.308(a)(4) - periodic key rotation).

- db_crypto.rotate_db_key(): opens with old key, issues PRAGMA rekey,
  reopens with new key, verifies schema survived (table-count sanity).
- db_crypto.generate_db_key(): fresh 256-bit CSPRNG hex key.
- db_crypto.fingerprint(): SHA-256[:8] of a key, for the operator to
  compare across rotations.
- db.dispose_engine() + db.reinit_engine(): SP15 plumbing. The
  rotation endpoint disposes the pooled connections (SQLCipher
  refuses to rekey while another connection holds the file), runs
  the rekey, then rebuilds the engine with the new key from the
  Keychain.
- API: POST /api/admin/db/rotate-key with module-level threading.Lock
  to serialize rotations. 400 when encryption not enabled, 409 when
  a rotation is already in flight, 503 on rekey or Keychain failure
  with a reason that tells the operator what to do next.
- Engine uses NullPool when SQLCipher is enabled: the default
  QueuePool returns connections to a shared queue that any thread
  can pull from, which breaks SQLCipher's thread affinity. NullPool
  trades connection reuse for thread safety, the only correct
  behavior under FastAPI's per-request threadpool.
- Audit event db.key_rotated with old/new fingerprints and
  table_count, written after the engine is rebuilt so the new key
  proves it can take new writes.
- previous key is stashed to a second Keychain account so the
  operator can roll back if the new key turns out to be broken.

Tests:
- test_db_crypto.py: 12 new tests for generate/fingerprint/rekey
  mechanics (5 require SQLCipher at runtime; skipped otherwise).
- test_api_rotate_key.py: 6 new tests for endpoint wiring
  (encryption-required, Keychain update, audit event, rekey-failure
  rollback, Keychain-write-failure 503, concurrent-rotation 409).
2026-06-21 00:33:37 -06:00