M24: implement privacy governance
This commit is contained in:
@@ -1,23 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_operations_manager
|
||||
from app.core.config import get_settings
|
||||
from app.models.audit import AuditEvent
|
||||
from app.models.booking import Booking
|
||||
from app.models.customer import Customer
|
||||
from app.models.data_quality import DataQualityIssue
|
||||
from app.models.vehicle import Vehicle
|
||||
from app.schemas import AuditEventOut, AuditEventPageOut, CurrentUser
|
||||
from app.services.audit import record_audit_event
|
||||
|
||||
router = APIRouter(prefix="/api/v1/audit", tags=["audit"])
|
||||
settings = get_settings()
|
||||
|
||||
# Only entity types with a stable public reference and (optionally) a real frontend route
|
||||
# are resolved here. Types like "system", "knowledge" or "mcp_tool" carry no linkable
|
||||
@@ -37,6 +44,74 @@ _ROUTE_TEMPLATES: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
@router.get("/export.csv")
|
||||
def export_audit_csv(
|
||||
occurred_from: datetime | None = Query(default=None),
|
||||
occurred_to: datetime | None = Query(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
actor: CurrentUser = Depends(require_operations_manager),
|
||||
) -> Response:
|
||||
end = occurred_to or datetime.now(UTC)
|
||||
start = occurred_from or end - timedelta(days=30)
|
||||
if end <= start or end - start > timedelta(days=90):
|
||||
raise HTTPException(status_code=422, detail="Audit export range must be 1 to 90 days")
|
||||
events = db.scalars(
|
||||
select(AuditEvent)
|
||||
.where(AuditEvent.occurred_at >= start, AuditEvent.occurred_at <= end)
|
||||
.order_by(AuditEvent.occurred_at)
|
||||
.limit(settings.privacy_audit_export_max_rows + 1)
|
||||
).all()
|
||||
if len(events) > settings.privacy_audit_export_max_rows:
|
||||
raise HTTPException(status_code=413, detail="Audit export exceeds configured row limit")
|
||||
output = io.StringIO(newline="")
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(
|
||||
(
|
||||
"id",
|
||||
"occurred_at",
|
||||
"actor_type",
|
||||
"actor_label",
|
||||
"action",
|
||||
"entity_type",
|
||||
"entity_id",
|
||||
"correlation_id",
|
||||
"before",
|
||||
"after",
|
||||
"metadata",
|
||||
)
|
||||
)
|
||||
for event in events:
|
||||
writer.writerow(
|
||||
(
|
||||
event.id,
|
||||
event.occurred_at.isoformat(),
|
||||
event.actor_type,
|
||||
event.actor_label,
|
||||
event.action,
|
||||
event.entity_type,
|
||||
event.entity_id or "",
|
||||
event.correlation_id,
|
||||
json.dumps(event.before_json, separators=(",", ":"), default=str),
|
||||
json.dumps(event.after_json, separators=(",", ":"), default=str),
|
||||
json.dumps(event.metadata_json, separators=(",", ":"), default=str),
|
||||
)
|
||||
)
|
||||
record_audit_event(
|
||||
db,
|
||||
actor_type="user",
|
||||
actor_label=actor.display_name,
|
||||
action="audit_exported",
|
||||
entity_type="audit",
|
||||
metadata={"from": start.isoformat(), "to": end.isoformat(), "rows": len(events)},
|
||||
)
|
||||
db.commit()
|
||||
return Response(
|
||||
output.getvalue(),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": 'attachment; filename="mobilityops-audit.csv"'},
|
||||
)
|
||||
|
||||
|
||||
def _resolve_entity_refs(db: Session, events: Sequence[AuditEvent]) -> dict[uuid.UUID, str]:
|
||||
ids_by_type: dict[str, set[uuid.UUID]] = {}
|
||||
for event in events:
|
||||
|
||||
Reference in New Issue
Block a user