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
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
---
|
||||
phase: "06.2"
|
||||
plan: "04"
|
||||
subsystem: "admin-audit-log"
|
||||
tags: [audit-log, handle-enrichment, csv-export, daily-exports, admin, security]
|
||||
dependency_graph:
|
||||
requires: ["06.2-02", "06.2-03"]
|
||||
provides: ["ADMIN-06 complete", "handle-enriched audit log", "fixed CSV export", "daily export UI"]
|
||||
affects: ["backend/api/audit.py", "frontend/src/components/admin/AuditLogTab.vue", "frontend/src/api/client.js"]
|
||||
tech_stack:
|
||||
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"
|
||||
key_files:
|
||||
created: []
|
||||
modified:
|
||||
- backend/api/audit.py
|
||||
- backend/tests/test_audit.py
|
||||
- frontend/src/api/client.js
|
||||
- frontend/src/components/admin/AuditLogTab.vue
|
||||
decisions:
|
||||
- "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"
|
||||
metrics:
|
||||
duration: "~25 minutes"
|
||||
completed_date: "2026-05-31"
|
||||
tasks_completed: 2
|
||||
files_modified: 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-log` — `user_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_id` → `filters.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
|
||||
Reference in New Issue
Block a user