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