from __future__ import annotations import json from quart import Blueprint, render_template, request, current_app from sqlalchemy import select from steward.ansible.inventory_gen import scope_labels from steward.audit.service import AREAS, action_text, detail_facts, entity_labels from steward.auth.middleware import require_role from steward.core.paging import keyset, next_page from steward.models.audit import AuditEvent from steward.models.users import UserRole audit_bp = Blueprint("audit", __name__, url_prefix="/audit") @audit_bp.get("/") @require_role(UserRole.admin) async def list_events(): """Who changed what, newest first, a page at a time as the reader scrolls; ?before= is the next page's rows alone (core.paging).""" action_filter = request.args.get("action", "").strip() async with current_app.db_sessionmaker() as db: stmt = select(AuditEvent) if action_filter: stmt = stmt.where(AuditEvent.action.like(f"{action_filter}%")) stmt = keyset(stmt, AuditEvent.timestamp, AuditEvent.id, request.args.get("before")) events, after = next_page((await db.execute(stmt)).scalars().all(), lambda e: (e.timestamp, e.id)) # Parse detail_json for display parsed = [] for e in events: detail = {} if e.detail_json: try: detail = json.loads(e.detail_json) except (ValueError, TypeError): detail = {"raw": e.detail_json} parsed.append({"event": e, "detail": detail}) labels = await entity_labels(db, parsed) scopes = await scope_labels(db, [p["detail"].get("scope") for p in parsed if isinstance(p["detail"].get("scope"), str)]) for item in parsed: e = item["event"] item["entity"] = labels.get((e.entity_type, str(e.entity_id))) if e.entity_id else None item["action"] = action_text(e.action) item["facts"] = detail_facts(item["detail"], scopes) ctx = {"events": parsed, "after": after, "action_filter": action_filter, "areas": AREAS} if request.args.get("before"): return await render_template("audit/_rows.html", **ctx) return await render_template("audit/list.html", **ctx)