From b80e40e7e96b9a693523b842cb69b10393a31c3c Mon Sep 17 00:00:00 2001 From: tyler Date: Wed, 24 Jun 2026 17:51:44 -0600 Subject: [PATCH] fix(permissions): expose GET /api/acks, /api/ta1-acks, /api/277ca-acks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PERMISSIONS matrix only listed the POST /api/acks entry (parse-999 ingest). The GET list + detail endpoints for 999 / TA1 / 277CA acks were missing, so the matrix_gate (default-deny) returned 403 to every authenticated role, breaking the 999 ACKs / TA1 / 277CA inbox pages. Add the three GET entries as ALL_ROLES — they're read-only metadata surfaces that every authenticated operator needs to see. Add a regression test that logs in as admin via the public route and exercises the matrix; the existing test_existing_endpoints_require_auth suite only checks the unauthenticated 401 path, which is why this slipped through. Fixes the live-verification 'Couldn't load ACKs from the backend / forbidden' error reported against the UI. --- backend/src/cyclone/auth/permissions.py | 3 + .../tests/test_ack_endpoints_authenticated.py | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 backend/tests/test_ack_endpoints_authenticated.py diff --git a/backend/src/cyclone/auth/permissions.py b/backend/src/cyclone/auth/permissions.py index 7eecc80..8a9a13c 100644 --- a/backend/src/cyclone/auth/permissions.py +++ b/backend/src/cyclone/auth/permissions.py @@ -44,6 +44,9 @@ PERMISSIONS: dict[tuple[str, str], set[Role]] = { ("GET", "/api/inbox/export.csv"): ALL_ROLES, ("GET", "/api/reconcile"): ALL_ROLES, ("GET", "/api/reconciliation"): ALL_ROLES, + ("GET", "/api/acks"): ALL_ROLES, + ("GET", "/api/ta1-acks"): ALL_ROLES, + ("GET", "/api/277ca-acks"): ALL_ROLES, ("GET", "/api/audit-log"): ADMIN_ONLY, # Write endpoints (admin + user, no viewer). diff --git a/backend/tests/test_ack_endpoints_authenticated.py b/backend/tests/test_ack_endpoints_authenticated.py new file mode 100644 index 0000000..8cc7c23 --- /dev/null +++ b/backend/tests/test_ack_endpoints_authenticated.py @@ -0,0 +1,71 @@ +"""Regression: GET /api/acks, /api/ta1-acks, /api/277ca-acks must be reachable +for an authenticated admin. The PERMISSIONS matrix only listed the +``POST /api/acks`` (parse-999) entry, so the GET list/detail endpoints +returned 403 to admins and broke the Inbox / 999 ACKs pages on the UI. + +This test exercises the matrix via the public login route — not by +flipping ``AUTH_DISABLED`` — so a missing ``("GET", "/api/-acks"): +ALL_ROLES`` entry would surface as 403. +""" + +from __future__ import annotations + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import delete + +from cyclone.api import app +from cyclone.auth import users +from cyclone.db import Session as DbSession +from cyclone.db import SessionLocal, User + + +@pytest.fixture(autouse=True) +def _clear(monkeypatch): + monkeypatch.setattr("cyclone.auth.deps.AUTH_DISABLED", False) + with SessionLocal()() as db: + db.execute(delete(DbSession)) + db.execute(delete(User)) + db.commit() + yield + with SessionLocal()() as db: + db.execute(delete(DbSession)) + db.execute(delete(User)) + db.commit() + + +@pytest.fixture +def client(): + return TestClient(app) + + +@pytest.fixture +def admin_client(client): + with SessionLocal()() as db: + users.create(db, username="admin", password="adminpassword1", role="admin") + resp = client.post( + "/api/auth/login", + json={"username": "admin", "password": "adminpassword1"}, + ) + assert resp.status_code == 200 + return client + + +@pytest.mark.parametrize("path", [ + "/api/acks", + "/api/acks/0", + "/api/ta1-acks", + "/api/ta1-acks/0", + "/api/277ca-acks", + "/api/277ca-acks/0", +]) +def test_admin_can_list_and_detail_all_ack_kinds(admin_client, path): + resp = admin_client.get(path, headers={"Accept": "application/json"}) + # 200 for the list endpoint (empty rows is fine), 404 for the detail + # of a non-existent id (the gate must let the request through). + assert resp.status_code in (200, 404), ( + f"{path} returned {resp.status_code}: {resp.text}" + ) + assert resp.status_code != 403, ( + f"{path} returned 403 — PERMISSIONS matrix is missing a GET entry" + ) \ No newline at end of file