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/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).
|
||||
|
||||
@@ -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