Files
curo1305 893da5b9ba docs(06.2-04): complete ADMIN-06 audit enrichment + daily exports — 10 tests pass
- Handle-enriched audit log (user_handle, actor_handle via aliased double-JOIN)
- user_handle filter with handle-to-UUID resolution, empty result for unknown handles
- fetch+Blob CSV export replacing window.location.href (T-06.2-04-03)
- GET /audit-log/daily-exports and /daily-exports/{date} with date regex validation
- Daily exports section in AuditLogTab with date dropdown + Download button
- Full audit test suite: 10 passed; backend suite: 337 passed, 1 pre-existing failure
2026-05-31 15:22:46 +02:00

8.6 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
06.2 04 admin-audit-log
audit-log
handle-enrichment
csv-export
daily-exports
admin
security
requires provides affects
06.2-02
06.2-03
ADMIN-06 complete
handle-enriched audit log
fixed CSV export
daily export UI
backend/api/audit.py
frontend/src/components/admin/AuditLogTab.vue
frontend/src/api/client.js
added patterns
SQLAlchemy aliased double-JOIN for handle enrichment
Handle-to-UUID resolution with empty-result fallback for unknown handles
asyncio.to_thread wrapping synchronous MinIO SDK calls
fetch+Blob URL pattern for authenticated CSV download
Date path parameter regex validation before MinIO key construction
created modified
backend/api/audit.py
backend/tests/test_audit.py
frontend/src/api/client.js
frontend/src/components/admin/AuditLogTab.vue
Module-level import of get_storage_backend and MinIOBackend in audit.py to enable testable patch targets
Separate count query (no JOIN) from data query (with JOIN) to avoid COUNT subquery ambiguity on multi-column selects (Pitfall 4)
export_audit_log uses same _audit_to_dict_with_handles() as list_audit_log to prevent UUID-only CSV export regression (Pitfall 7)
Updated test_audit_log_export_csv expected CSV header to include user_handle and actor_handle columns
duration completed_date tasks_completed files_modified
~25 minutes 2026-05-31 2 4

Phase 06.2 Plan 04: ADMIN-06 audit enrichment + CSV + daily exports Summary

One-liner: Handle-enriched audit log (aliased double-JOIN), user_handle filter with handle→UUID resolution, fixed CSV export via fetch+Blob, and new daily-export listing + streaming download endpoints with MinIO integration.

What Was Built

Backend (backend/api/audit.py)

New helper functions:

  • _audit_to_dict_with_handles(entry, user_handle, actor_handle) — extends the existing _audit_to_dict() to include user_handle and actor_handle fields. Used by both the JSON viewer and CSV export (Pitfall 7 compliance).
  • _build_filtered_query_with_handles(start, end, user_uuid, event_type) — builds a multi-column select joining User twice (as UserSubject and UserActor via aliased()) to resolve handles. Returns (AuditLog, user_handle, actor_handle) row tuples.

Updated endpoints:

  • GET /api/admin/audit-loguser_id: Optional[uuid.UUID] replaced with user_handle: Optional[str]. Handle resolved to UUID via preliminary SELECT; unknown handles return empty results (not 422). Data query uses enriched JOIN; count query uses plain query to avoid subquery ambiguity (Pitfall 4).
  • GET /api/admin/audit-log/export — same user_handle change; uses _audit_to_dict_with_handles() so CSV includes user_handle and actor_handle columns.

New endpoints (registered before existing ones to ensure route priority):

  • GET /api/admin/audit-log/daily-exports — lists MinIO audit-logs bucket via asyncio.to_thread(_list). Returns {items: [{date, key}]} sorted descending by date. Returns {items: []} if backend is not MinIOBackend.
  • GET /api/admin/audit-log/daily-exports/{date} — validates date against re.fullmatch(r"\d{4}-\d{2}-\d{2}", date) before constructing f"audit-logs/{date}.csv" key (T-06.2-04-01 path traversal prevention). Streams CSV via asyncio.to_thread(_get). Returns 404 on invalid date or missing file.

Both new endpoints use Depends(get_current_admin) (T-06.2-04-02).

Backend Tests (backend/tests/test_audit.py)

Five xfail stubs promoted to full integration tests:

  1. test_audit_log_includes_user_handle — seeds entry, asserts user_handle and actor_handle keys present, handle matches admin_user fixture
  2. test_audit_log_filter_by_handle — seeds two users, asserts filtering by handle returns only matching entries
  3. test_audit_log_filter_unknown_handle — asserts 200 + items==[] + total==0 for unknown handle
  4. test_daily_exports_list — mocks get_storage_backend with mock MinIO client, asserts sorted items returned
  5. test_daily_export_download — mocks get_object, asserts text/csv Content-Type, 2026-05-30 in Content-Disposition, 404 for invalid date

Also updated test_audit_log_export_csv expected CSV header to include user_handle,actor_handle columns — regression caused by enriched export; correct per Pitfall 7.

Final test counts: 10 tests pass (4 pre-existing + 6 updated/promoted), full suite 337 passed / 1 pre-existing failure (test_extract_docx, ModuleNotFoundError — unrelated).

Frontend (frontend/src/api/client.js)

Three new exported functions:

  • adminExportAuditLogCsv(params) — raw fetch() with Authorization Bearer header, res.text() (not res.json()), Blob + <a> click download pattern (D-13, T-06.2-04-03)
  • adminListDailyExports() — raw fetch() + res.json() for the JSON-returning listing endpoint
  • adminDownloadDailyExport(date) — raw fetch() with Bearer header, Blob download as audit-{date}.csv

Updated adminListAuditLog() — parameter renamed from user_id to user_handle to match backend API change.

Frontend (frontend/src/components/admin/AuditLogTab.vue)

  • Label "User" → "User handle"; filters.user_idfilters.user_handle; fetchLog() passes user_handle param
  • exportCsv() replaced with async function calling api.adminExportAuditLogCsv(); loading state exportingCsv ref; error display
  • New "Daily exports" section below pagination: loading/empty/populated states, date <select> dropdown, Download button with spinner, error display
  • All reactive state initialized in <script setup>; loadDailyExports() called in onMounted() alongside fetchLog()

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Moved get_storage_backend import to module level for testability

  • Found during: Task 1 - daily exports tests
  • Issue: The plan specified lazy imports inside handler bodies (from storage import get_storage_backend). When tests used patch("api.audit.get_storage_backend", ...), the attribute did not exist on the module (the import had not yet executed), causing AttributeError.
  • Fix: Moved from storage import get_storage_backend and from storage.minio_backend import MinIOBackend to module-level imports. This is consistent with how other modules import these — the lazy import pattern was only needed for the cloud backend classes (to avoid circular imports) not for the top-level factory.
  • Files modified: backend/api/audit.py
  • Commit: 839bfe0

2. [Rule 1 - Bug] Updated test_audit_log_export_csv header assertion

  • Found during: Task 1 - running full test suite after GREEN phase
  • Issue: The existing CSV export test asserted the old header line (without user_handle,actor_handle). After enriching the export endpoint per Pitfall 7, the test failed with a header mismatch.
  • Fix: Updated expected_header in test_audit_log_export_csv to include user_handle,actor_handle columns. This is the correct behavior — the test was correct for the old API, and the new assertion is correct for the enriched API.
  • Files modified: backend/tests/test_audit.py
  • Commit: 839bfe0

Security Compliance

All threat model mitigations implemented and verified:

  • T-06.2-04-01 (date path traversal): re.fullmatch(r"\d{4}-\d{2}-\d{2}", date) gates key construction
  • T-06.2-04-02 (unauthenticated access): both new endpoints use Depends(get_current_admin)
  • T-06.2-04-03 (CSV token bypass via window.location.href): replaced with fetch()+Blob pattern carrying Bearer header
  • T-06.2-04-05 (event loop blocking): asyncio.to_thread() wraps all synchronous MinIO SDK calls

Known Stubs

None — all functionality is fully wired.

Self-Check: PASSED

Files verified present:

  • backend/api/audit.py — contains _audit_to_dict_with_handles, _build_filtered_query_with_handles, /audit-log/daily-exports, /audit-log/daily-exports/{date}, re.fullmatch
  • backend/tests/test_audit.py — all 10 tests pass
  • frontend/src/api/client.js — contains adminExportAuditLogCsv, adminListDailyExports, adminDownloadDailyExport
  • frontend/src/components/admin/AuditLogTab.vue — contains "Daily exports", "User handle", no window.location.href

Commits verified:

  • d7cfc5c — test(06.2-04): add failing tests for handle enrichment, user_handle filter, daily exports
  • 839bfe0 — feat(06.2-04): backend — handle enrichment, user_handle filter, two daily-export endpoints
  • 0647e6e — feat(06.2-04): frontend — user_handle filter, fetch+Blob export, daily-export section