fix(permissions): expose GET /api/acks, /api/ta1-acks, /api/277ca-acks
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.
This commit is contained in:
@@ -44,6 +44,9 @@ PERMISSIONS: dict[tuple[str, str], set[Role]] = {
|
|||||||
("GET", "/api/inbox/export.csv"): ALL_ROLES,
|
("GET", "/api/inbox/export.csv"): ALL_ROLES,
|
||||||
("GET", "/api/reconcile"): ALL_ROLES,
|
("GET", "/api/reconcile"): ALL_ROLES,
|
||||||
("GET", "/api/reconciliation"): 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,
|
("GET", "/api/audit-log"): ADMIN_ONLY,
|
||||||
|
|
||||||
# Write endpoints (admin + user, no viewer).
|
# Write endpoints (admin + user, no viewer).
|
||||||
|
|||||||
@@ -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/<kind>-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"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user