- 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
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 |
|
|
|
|
|
|
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 includeuser_handleandactor_handlefields. 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 joiningUsertwice (asUserSubjectandUserActorviaaliased()) to resolve handles. Returns(AuditLog, user_handle, actor_handle)row tuples.
Updated endpoints:
GET /api/admin/audit-log—user_id: Optional[uuid.UUID]replaced withuser_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 includesuser_handleandactor_handlecolumns.
New endpoints (registered before existing ones to ensure route priority):
GET /api/admin/audit-log/daily-exports— lists MinIOaudit-logsbucket viaasyncio.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 againstre.fullmatch(r"\d{4}-\d{2}-\d{2}", date)before constructingf"audit-logs/{date}.csv"key (T-06.2-04-01 path traversal prevention). Streams CSV viaasyncio.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:
test_audit_log_includes_user_handle— seeds entry, assertsuser_handleandactor_handlekeys present, handle matches admin_user fixturetest_audit_log_filter_by_handle— seeds two users, asserts filtering by handle returns only matching entriestest_audit_log_filter_unknown_handle— asserts 200 +items==[]+total==0for unknown handletest_daily_exports_list— mocksget_storage_backendwith mock MinIO client, asserts sorteditemsreturnedtest_daily_export_download— mocksget_object, assertstext/csvContent-Type,2026-05-30in 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)— rawfetch()with Authorization Bearer header,res.text()(notres.json()), Blob +<a>click download pattern (D-13, T-06.2-04-03)adminListDailyExports()— rawfetch()+res.json()for the JSON-returning listing endpointadminDownloadDailyExport(date)— rawfetch()with Bearer header, Blob download asaudit-{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_id→filters.user_handle;fetchLog()passesuser_handleparam exportCsv()replaced with async function callingapi.adminExportAuditLogCsv(); loading stateexportingCsvref; 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 inonMounted()alongsidefetchLog()
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 usedpatch("api.audit.get_storage_backend", ...), the attribute did not exist on the module (the import had not yet executed), causingAttributeError. - Fix: Moved
from storage import get_storage_backendandfrom storage.minio_backend import MinIOBackendto 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_headerintest_audit_log_export_csvto includeuser_handle,actor_handlecolumns. 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()+Blobpattern 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: