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