Files
TimeTracker/scripts/verify_audit_setup.py
Dries Peeters 350d7105a2 feat: Add comprehensive audit trail/history tracking system
Implement a complete audit logging system to track all changes made to
tracked entities, providing full compliance and accountability capabilities.

Features:
- Automatic tracking of create, update, and delete operations on 25+ models
- Detailed field-level change tracking with old/new value comparison
- User attribution with IP address, user agent, and request path logging
- Web UI for viewing and filtering audit logs with pagination
- REST API endpoints for programmatic access
- Entity-specific history views
- Comprehensive test coverage (unit, model, route, and smoke tests)

Core Components:
- AuditLog model with JSON-encoded value storage and decoding helpers
- SQLAlchemy event listeners for automatic change detection
- Audit utility module with defensive programming for table existence checks
- Blueprint routes for audit log viewing and API access
- Jinja2 templates for audit log list, detail, and entity history views
- Database migration (044) creating audit_logs table with proper indexes

Technical Implementation:
- Uses SQLAlchemy 'after_flush' event listener to capture changes
- Tracks 25+ models including Projects, Tasks, TimeEntries, Invoices, Clients, Users, etc.
- Excludes sensitive fields (passwords) and system fields (id, timestamps)
- Implements lazy import pattern to avoid circular dependencies
- Graceful error handling to prevent audit logging from breaking core functionality
- Transaction-safe logging that integrates with main application transactions

Fixes:
- Resolved login errors caused by premature transaction commits
- Fixed circular import issues with lazy model loading
- Added table existence checks to prevent errors before migrations
- Improved error handling with debug-level logging for non-critical failures

UI/UX:
- Added "Audit Logs" link to admin dropdown menu
- Organized admin menu into logical sections for better usability
- Filterable audit log views by entity type, user, action, and date range
- Color-coded action badges and side-by-side old/new value display
- Pagination support for large audit log datasets

Documentation:
- Added comprehensive feature documentation
- Included troubleshooting guide and data examples
- Created diagnostic scripts for verifying audit log setup

Testing:
- Unit tests for AuditLog model and value encoding/decoding
- Route tests for all audit log endpoints
- Integration tests for audit logging functionality
- Smoke tests for end-to-end audit trail verification

This implementation provides a robust foundation for compliance tracking
and change accountability without impacting application performance or
requiring code changes in existing routes/models.
2025-11-13 08:08:48 +01:00

128 lines
4.6 KiB
Python

#!/usr/bin/env python
"""Verify audit logs setup - check routes, table, and imports"""
import sys
import os
# Add the parent directory to the path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
print("=" * 70)
print("Audit Logs Setup Verification")
print("=" * 70)
# Test 1: Check if modules can be imported
print("\n1. Testing imports...")
try:
from app.models.audit_log import AuditLog
print(" ✓ AuditLog model imported successfully")
except Exception as e:
print(f" ✗ Failed to import AuditLog: {e}")
sys.exit(1)
try:
from app.utils.audit import check_audit_table_exists, reset_audit_table_cache
print(" ✓ Audit utility imported successfully")
except Exception as e:
print(f" ✗ Failed to import audit utility: {e}")
sys.exit(1)
try:
from app.routes.audit_logs import audit_logs_bp
print(" ✓ Audit logs blueprint imported successfully")
print(f" Blueprint name: {audit_logs_bp.name}")
except Exception as e:
print(f" ✗ Failed to import audit_logs blueprint: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Test 2: Check routes in blueprint
print("\n2. Checking blueprint routes...")
routes = []
for rule in audit_logs_bp.url_map.iter_rules() if hasattr(audit_logs_bp, 'url_map') else []:
routes.append(rule.rule)
# Check deferred functions (routes not yet registered)
if hasattr(audit_logs_bp, 'deferred_functions'):
print(f" Found {len(audit_logs_bp.deferred_functions)} deferred route functions")
for func in audit_logs_bp.deferred_functions:
if hasattr(func, '__name__'):
print(f" - {func.__name__}")
# Test 3: Create app and check registered routes
print("\n3. Creating app and checking registered routes...")
try:
from app import create_app
app = create_app()
# Find all audit-related routes
audit_routes = []
for rule in app.url_map.iter_rules():
if 'audit' in rule.rule.lower():
audit_routes.append({
'rule': rule.rule,
'endpoint': rule.endpoint,
'methods': sorted([m for m in rule.methods if m not in ['HEAD', 'OPTIONS']])
})
if audit_routes:
print(f" ✓ Found {len(audit_routes)} registered audit log route(s):")
for route in audit_routes:
print(f" {route['rule']} -> {route['endpoint']} [{', '.join(route['methods'])}]")
else:
print(" ✗ No audit log routes found in app!")
print(" This means the blueprint was not registered properly.")
print("\n Checking app initialization...")
# Check if blueprint is in the app
blueprint_names = [bp.name for bp in app.blueprints.values()]
if 'audit_logs' in blueprint_names:
print(" ✓ Blueprint is registered in app")
else:
print(" ✗ Blueprint 'audit_logs' NOT found in app blueprints")
print(f" Available blueprints: {', '.join(sorted(blueprint_names))}")
# Test 4: Check database table
print("\n4. Checking database table...")
with app.app_context():
reset_audit_table_cache()
table_exists = check_audit_table_exists(force_check=True)
if table_exists:
print(" ✓ audit_logs table exists")
try:
count = AuditLog.query.count()
print(f" Current log count: {count}")
except Exception as e:
print(f" ⚠ Could not query table: {e}")
else:
print(" ✗ audit_logs table does NOT exist")
print(" Run migration: flask db upgrade")
# Show available tables
try:
from sqlalchemy import inspect as sqlalchemy_inspect
inspector = sqlalchemy_inspect(app.extensions['sqlalchemy'].db.engine)
tables = inspector.get_table_names()
print(f"\n Available tables ({len(tables)}):")
for table in sorted(tables)[:20]: # Show first 20
print(f" - {table}")
if len(tables) > 20:
print(f" ... and {len(tables) - 20} more")
except Exception as e:
print(f" Could not list tables: {e}")
except Exception as e:
print(f" ✗ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
print("\n" + "=" * 70)
print("Verification Complete")
print("=" * 70)
print("\nIf routes are missing, restart your Flask application.")
print("If table is missing, run: flask db upgrade")