"""Admin Security: alerts, the integrity result, the access log and its CSV, who is in, the stamp check.""" from __future__ import annotations import csv import io from datetime import UTC, datetime, timedelta from typing import Any from fakedir import FakeDirectory from fastapi.testclient import TestClient from pshelpers import PIN, app_conn, needs_pg, pin_signin, register_device, set_pin, sign_in from psycopg.types.json import Jsonb from packetstation.auth import ratelimit from packetstation.pdf import stamps from packetstation.settings import Settings pytestmark = needs_pg FORMULA = "=cmd|' /C calc'!A0" def test_access_log_filters_pages_and_csv( pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory ) -> None: assert sign_in(pg_client, "clerk.clara").status_code == 200 assert sign_in(pg_client, "clerk.clara", "wrong").status_code == 401 assert sign_in(pg_client, FORMULA, "wrong").status_code == 401 clara = str(fake_dir.user("clerk.clara").guid) with app_conn(pg_settings) as conn: ratelimit.record(conn, account="wh.wade", ip="10.1.2.3", kind="password", ok=False, reason="bad_password") sign_in(pg_client, "admin.ann") rows = pg_client.get("/api/admin/access-log").json()["rows"] sources = {r["source"] for r in rows} assert sources == {"attempt", "audit"} assert all( set(r) == {"id", "at", "source", "kind", "ok", "account", "user_id", "display_name", "ip", "detail"} for r in rows ) failed = pg_client.get("/api/admin/access-log", params={"ok": "false", "source": "x"}).json()["rows"] assert failed and all(r["ok"] is False for r in failed) assert {r["account"] for r in failed if r["source"] == "attempt"} == {"clerk.clara", FORMULA.lower(), "wh.wade"} mine = pg_client.get("/api/admin/access-log", params={"user_id": clara}).json()["rows"] assert mine and all(r["user_id"] == clara for r in mine) assert {r["kind"] for r in mine} >= {"password", "auth.signin"} q = pg_client.get("/api/admin/access-log", params={"q": "Clara"}).json()["rows"] assert q and all(r["display_name"] == "Clara Clerk" or r["account"] == "clerk.clara" for r in q) kind = pg_client.get("/api/admin/access-log", params={"kind": "auth.signin"}).json()["rows"] assert kind and all(r["kind"] == "auth.signin" and r["source"] == "audit" for r in kind) ip = pg_client.get("/api/admin/access-log", params={"ip": "10.1.2.0/24"}).json()["rows"] assert [r["account"] for r in ip] == ["wh.wade"] and ip[0]["ip"] == "10.1.2.3" assert pg_client.get("/api/admin/access-log", params={"ip": "nope"}).status_code == 422 future = (datetime.now(UTC) + timedelta(hours=1)).isoformat() assert pg_client.get("/api/admin/access-log", params={"since": future}).json()["rows"] == [] assert pg_client.get("/api/admin/access-log", params={"until": future}).json()["rows"] # Pages: no row twice, none missed (attempt and audit rows share a timestamp inside one transaction) every = pg_client.get("/api/admin/access-log", params={"limit": 1000}).json() assert every["next_before"] is None seen: list[str] = [] before = "" while True: page = pg_client.get("/api/admin/access-log", params={"limit": 3, "before": before}).json() seen += [r["id"] for r in page["rows"]] if not page["next_before"]: break before = page["next_before"] assert seen == [r["id"] for r in every["rows"]] assert pg_client.get("/api/admin/access-log", params={"before": "garbage"}).status_code == 422 # The CSV: the same rows, the formula guard, logged and audited r = pg_client.get("/api/admin/access-log.csv", params={"ok": "false"}) assert r.status_code == 200 and r.headers["content-type"].startswith("text/csv") assert "attachment" in r.headers["content-disposition"] table = list(csv.reader(io.StringIO(r.text))) assert table[0] == ["at", "source", "kind", "ok", "account", "display_name", "user_id", "ip", "detail"] accounts = [row[4] for row in table[1:]] assert "'" + FORMULA.lower() in accounts and FORMULA.lower() not in accounts assert all(row[3] == "no" for row in table[1:]) with app_conn(pg_settings) as conn: log = conn.execute("SELECT kind, what FROM print_log").fetchall() assert log == [("access-log-csv", f"Access log CSV ({len(table) - 1} rows)")] detail = conn.execute("SELECT detail FROM audit_events WHERE action = 'admin.access_log_exported'").fetchone()[ 0 ] assert detail["rows"] == len(table) - 1 and detail["filters"] == {"ok": "False"} def _print_row(settings: Settings, user_id: Any, *, sig_override: str | None = None) -> tuple[int, str, str]: who = {"id": str(user_id), "display_name": "Ann Admin", "emp_no": "1001"} now = datetime.now(UTC) text = stamps.controlled_stamp(who, now, mode="report") line, sig = stamps.signed_stamp(settings, text, packet_id=None, kind="report", seal_hash_hex=None, who=who, now=now) if sig_override is not None: line, sig = stamps.with_sig(text, sig_override), sig_override with app_conn(settings) as conn: pid = conn.execute( "INSERT INTO print_log (by, by_name, kind, what, pages, stamp, stamp_sig)" " VALUES (%s, 'Ann Admin', 'report-pdf', 'Production status report', 2, %s, %s) RETURNING id", (user_id, line, sig), ).fetchone()[0] return int(pid), line, sig def _pdf(text: str) -> bytes: from reportlab.pdfgen.canvas import Canvas buf = io.BytesIO() c = Canvas(buf) c.setFont("Helvetica", 6) c.drawString(20, 20, text) c.drawString(20, 60, "Some other text on the page") c.save() return buf.getvalue() def test_stamp_verify(pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory) -> None: sign_in(pg_client, "admin.ann") ann = fake_dir.user("admin.ann").guid pid, line, sig = _print_row(pg_settings, ann) def check(text: str) -> dict[str, Any]: r = pg_client.post("/api/admin/stamp-verify", json={"text": text}) assert r.status_code == 200, r.text out: dict[str, Any] = r.json() return out ok = check(line) assert ok["found"] is True and ok["valid"] is True, ok assert ok["print"] == { "id": pid, "at": ok["print"]["at"], "by_name": "Ann Admin", "kind": "report-pdf", "what": "Production status report", "pages": 2, "packet_id": None, "packet_label": None, } assert check(sig)["valid"] is True # the 16 hex alone assert check(sig.upper())["valid"] is True assert check(" " + line.replace(" - ", " - ") + "\n")["valid"] is True # spacing does not matter tail = line[line.index("printed by") :] assert check(tail)["valid"] is True # a partial copy from the end tampered = check(line.replace("Ann Admin", "Bob Admin")) assert tampered["found"] is True and tampered["valid"] is False and "differs" in tampered["reason"] other_sig = ("0" if sig[0] != "0" else "1") + sig[1:] unknown = check(line.replace(sig, other_sig)) assert unknown["found"] is False and unknown["valid"] is False and unknown["print"] is None nothing = check("no stamp in here") assert nothing["found"] is False and nothing["reason"] == "No stamp signature in the text." # A print_log row whose signature was never made with the seal key _, forged_line, _ = _print_row(pg_settings, ann, sig_override="deadbeefdeadbeef") forged = check(forged_line) assert forged["found"] is True and forged["valid"] is False and "does not match" in forged["reason"] # From a PDF r = pg_client.post("/api/admin/stamp-verify/pdf", files={"file": ("copy.pdf", _pdf(line), "application/pdf")}) assert r.status_code == 200, r.text found = r.json()["stamps"] assert len(found) == 1 and found[0]["valid"] is True and found[0]["print"]["id"] == pid r = pg_client.post( "/api/admin/stamp-verify/pdf", files={"file": ("copy.pdf", _pdf(line.replace("Ann Admin", "Bob Admin")), "application/pdf")}, ) assert r.json()["stamps"][0]["valid"] is False r = pg_client.post("/api/admin/stamp-verify/pdf", files={"file": ("x.pdf", b"not a pdf", "application/pdf")}) assert r.status_code == 422 and r.json()["code"] == "not_pdf" with app_conn(pg_settings) as conn: n = conn.execute("SELECT count(*) FROM audit_events WHERE action = 'admin.stamp_verified'").fetchone()[0] assert n == 11 def test_alerts_integrity_and_presence(pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory) -> None: # Three wrong PINs at sign-in, then a session ended after three wrong unlock PINs register_device(pg_client) set_pin(pg_client, "weld.andy") for _ in range(3): assert pin_signin(pg_client, "3003", "000000").status_code == 401 with app_conn(pg_settings) as conn: conn.execute("UPDATE user_pins SET failed_count = 0") assert pin_signin(pg_client, "3003", PIN).status_code == 200 pg_client.post("/api/auth/lock") for _ in range(3): pg_client.post("/api/auth/unlock", json={"factor": "pin", "secret": "111111"}) # A denial sign_in(pg_client, "clerk.clara") assert pg_client.get("/api/admin/users").status_code == 403 with app_conn(pg_settings) as conn: run = conn.execute( "INSERT INTO job_runs (job, trigger, finished_at, status, counts) VALUES ('integrity', 'schedule', now()," " 'degraded', %s) RETURNING id", (Jsonb({"seal": 1, "asset_hash": 0}),), ).fetchone()[0] conn.execute( "INSERT INTO integrity_findings (run_id, kind, subject, detail) VALUES (%s, 'seal', 'packet:7', %s)", (run, Jsonb({"why": "hash differs"})), ) conn.execute("INSERT INTO job_runs (job, trigger, status) VALUES ('integrity', 'schedule', 'running')") sign_in(pg_client, "admin.ann") alerts = pg_client.get("/api/admin/security/alerts", params={"hours": 1}).json() kinds = {a["kind"] for a in alerts} assert {"pin_failures", "stepup_wrong_pin", "denied", "integrity"} <= kinds, kinds pins = next(a for a in alerts if a["kind"] == "pin_failures") assert pins["display_name"] == "Andy Welder" and pins["detail"]["wrong_pins"] >= 3 steps = [a for a in alerts if a["kind"] == "stepup_wrong_pin"] assert len(steps) == 3 and steps[0]["detail"]["where"] == "unlock" denied = next(a for a in alerts if a["kind"] == "denied") assert denied["display_name"] == "Clara Clerk" and denied["detail"]["path"] == "/api/admin/users" assert [a["at"] for a in alerts] == sorted((a["at"] for a in alerts), reverse=True) integ = pg_client.get("/api/admin/security/integrity").json() assert integ["run"]["id"] == run and integ["run"]["status"] == "degraded" assert integ["run"]["counts"] == {"seal": 1, "asset_hash": 0} assert integ["findings"][0]["kind"] == "seal" and integ["findings"][0]["subject"] == "packet:7" assert integ["findings"][0]["detail"] == {"why": "hash differs"} who = pg_client.get("/api/admin/presence").json() assert who["at"] names = {s["display_name"]: s for s in who["sessions"]} assert "Ann Admin" in names and names["Ann Admin"]["kind"] == "password" and names["Ann Admin"]["locked"] is False assert "Andy Welder" not in names # that session ended with app_conn(pg_settings) as conn: conn.execute( "UPDATE sessions SET last_seen_at = now() - interval '20 minutes' WHERE user_id = %s", (fake_dir.user("clerk.clara").guid,), ) # Clara is a desk role (60 minutes), so she is still in; a shop worker idle 20 minutes is not assert "Clara Clerk" in {s["display_name"] for s in pg_client.get("/api/admin/presence").json()["sessions"]}