From 39ae988101b9b4257edb2063f49381b86bc8bd6f Mon Sep 17 00:00:00 2001 From: Nora Date: Mon, 22 Jun 2026 17:00:30 -0600 Subject: [PATCH] fix(auth): bootstrap init_db on fresh DB; permissions matrix for inbox endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bootability gaps caught by running the live stack: 1. cyclone.auth.bootstrap.run() called SessionLocal() before db.init_db() had run on a fresh DB, so 'python -m cyclone serve' / 'users create' / '--help' all crashed with 'db.init_db() has not been called'. Add init_db() inside bootstrap (idempotent — no-op once the schema is current) so first-boot works without manual prep. 2. PERMISSIONS matrix didn't register the inbox endpoints that were added in SP10 (export.csv) and SP14 (candidates/dismiss, rejected/resubmit, payer-rejected/acknowledge). The matrix's longest-prefix match meant /api/inbox/candidates/dismiss matched the '/api/inbox' prefix but only because that single entry existed; adding the dedicated write-prefix entries makes the intent explicit and keeps viewer role out of every inbox write even if the broader /api/inbox prefix is later broadened. Verified live: - admin can login, GET /api/inbox/lanes (200), GET /api/inbox/export.csv (200), POST /api/inbox/candidates/dismiss (200). - viewer can login, GET /api/inbox/lanes (200), GET /api/inbox/export.csv (200), but POST /api/inbox/candidates/dismiss and POST /api/inbox/rejected/resubmit both return 403. --- backend/src/cyclone/auth/bootstrap.py | 10 ++++++++++ backend/src/cyclone/auth/permissions.py | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/backend/src/cyclone/auth/bootstrap.py b/backend/src/cyclone/auth/bootstrap.py index ca8f72a..3a9b036 100644 --- a/backend/src/cyclone/auth/bootstrap.py +++ b/backend/src/cyclone/auth/bootstrap.py @@ -45,6 +45,16 @@ def run() -> None: username = os.environ.get("CYCLONE_ADMIN_USERNAME") password = os.environ.get("CYCLONE_ADMIN_PASSWORD") + # First-boot fix: ``python -m cyclone`` calls bootstrap before any + # subcommand or the FastAPI lifespan handler runs, so on a brand-new + # DB ``SessionLocal()`` raises "init_db() has not been called". + # Initialize here so ``serve``, ``users create``, and friends can + # all reach the DB without the operator having to know about + # migrations. Idempotent — no-op when the schema is already current. + from cyclone import db as _db + + _db.init_db() + with SessionLocal()() as db: existing = db.execute(select(User)).scalars().first() if existing is not None: diff --git a/backend/src/cyclone/auth/permissions.py b/backend/src/cyclone/auth/permissions.py index c0f01c8..0fec752 100644 --- a/backend/src/cyclone/auth/permissions.py +++ b/backend/src/cyclone/auth/permissions.py @@ -41,6 +41,7 @@ PERMISSIONS: dict[tuple[str, str], set[Role]] = { ("GET", "/api/dashboard/summary"): ALL_ROLES, ("GET", "/api/activity"): ALL_ROLES, ("GET", "/api/inbox/lanes"): ALL_ROLES, + ("GET", "/api/inbox/export.csv"): ALL_ROLES, ("GET", "/api/reconcile"): ALL_ROLES, ("GET", "/api/audit-log"): ADMIN_ONLY, @@ -48,6 +49,9 @@ PERMISSIONS: dict[tuple[str, str], set[Role]] = { ("POST", "/api/parse-837"): WRITE_ROLES, ("POST", "/api/parse-835"): WRITE_ROLES, ("POST", "/api/inbox"): WRITE_ROLES, + ("POST", "/api/inbox/candidates"): WRITE_ROLES, + ("POST", "/api/inbox/rejected"): WRITE_ROLES, + ("POST", "/api/inbox/payer-rejected"): WRITE_ROLES, ("POST", "/api/reconcile"): WRITE_ROLES, ("POST", "/api/resubmit"): WRITE_ROLES, ("POST", "/api/acks"): WRITE_ROLES,