From cc02cb99c5b88681d16ff3c3e711c8d6d069a823 Mon Sep 17 00:00:00 2001 From: Nora Date: Tue, 7 Jul 2026 12:17:14 -0600 Subject: [PATCH] fix(sp37-followup): remove dead /api/resubmit permissions entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Followup #1 from the SP37 final-state tracker. The pre-existing ('POST', '/api/resubmit'): WRITE_ROLES entry only matched the exact path /api/resubmit (no children — the prefix-match logic requires the trailing slash for sub-paths). No route is registered at that path; the actual resubmit lives at /api/inbox/rejected/resubmit (which has its own entry). Removing the dead entry is fail-closed: any future request to /api/resubmit (or its descendants) returns None from allowed_roles, which means deny. Without this cleanup, the matrix silently granted WRITE_ROLES to a non-existent route. Tests (test_auth_permissions_matrix.py, 5 cases): * Exact path /api/resubmit not in PERMISSIONS * allowed_roles('POST', '/api/resubmit') is None * /api/resubmit/, /api/resubmit/anything all deny Pin the invariant so a future contributor can't re-add a dead prefix. TDD: tests written first, watched fail (5 fail with the dead entry), removed the line, tests pass (5 pass). Full SP37 chain (36 tests) passes with the change. --- backend/src/cyclone/auth/permissions.py | 1 - backend/tests/test_auth_permissions_matrix.py | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_auth_permissions_matrix.py diff --git a/backend/src/cyclone/auth/permissions.py b/backend/src/cyclone/auth/permissions.py index 0c2de12..45a7201 100644 --- a/backend/src/cyclone/auth/permissions.py +++ b/backend/src/cyclone/auth/permissions.py @@ -82,7 +82,6 @@ PERMISSIONS: dict[tuple[str, str], set[Role]] = { ("POST", "/api/inbox/payer-rejected"): WRITE_ROLES, ("POST", "/api/reconcile"): WRITE_ROLES, ("POST", "/api/reconciliation"): WRITE_ROLES, - ("POST", "/api/resubmit"): WRITE_ROLES, ("POST", "/api/acks"): WRITE_ROLES, ("POST", "/api/batches"): WRITE_ROLES, # /export-837 regenerates X12 from DB rows ("POST", "/api/eligibility"): WRITE_ROLES, diff --git a/backend/tests/test_auth_permissions_matrix.py b/backend/tests/test_auth_permissions_matrix.py new file mode 100644 index 0000000..1ae2b03 --- /dev/null +++ b/backend/tests/test_auth_permissions_matrix.py @@ -0,0 +1,50 @@ +"""SP37 follow-up #1: Fail-closed for unregistered paths. + +The permissions matrix is fail-closed — endpoints not listed are denied +(None). The pre-existing ``("POST", "/api/resubmit"): WRITE_ROLES`` +entry was a dead prefix that ONLY matched the exact path ``/api/resubmit`` +(which is not a registered route). The actual resubmit lives at +``/api/inbox/rejected/resubmit`` (already covered by its own entry). + +This test pins the fail-closed invariant: ``/api/resubmit`` MUST have +no granted permission because no route is registered there. Without +this test, a future contributor could re-add the dead entry (or any +similar dead prefix) and the auth system would silently grant access +to a non-existent path. +""" +from __future__ import annotations + +import pytest + +from cyclone.auth.permissions import PERMISSIONS, allowed_roles + + +def test_api_resubmit_exact_path_not_in_matrix(): + """``/api/resubmit`` (exact, no children) must NOT be in the matrix. + + The actual resubmit endpoint is ``/api/inbox/rejected/resubmit``, + which has its own entry. The pre-existing + ``("POST", "/api/resubmit"): WRITE_ROLES`` entry only matched the + exact path ``/api/resubmit`` and never any real route. + """ + assert ("POST", "/api/resubmit") not in PERMISSIONS + + +def test_api_resubmit_returns_none_via_allowed_roles(): + """Fail-closed: ``allowed_roles("POST", "/api/resubmit")`` is None. + + The matrix default is DENY. A registered entry that matches the + path would return a non-empty set of roles; the absence of any + entry should return None. + """ + assert allowed_roles("POST", "/api/resubmit") is None + + +@pytest.mark.parametrize("path", [ + "/api/resubmit", + "/api/resubmit/", + "/api/resubmit/anything", +]) +def test_api_resubmit_prefix_variants_all_deny(path): + """No path under ``/api/resubmit`` should match any permission entry.""" + assert allowed_roles("POST", path) is None \ No newline at end of file