diff --git a/backend/app/services/audit_service.py b/backend/app/services/audit_service.py index ae6a65d..05c9f26 100644 --- a/backend/app/services/audit_service.py +++ b/backend/app/services/audit_service.py @@ -10,6 +10,11 @@ Phase 30: When details are non-empty, they are encrypted via OpenBao Transit (per-tenant data key) and stored in encrypted_details. The plaintext details column is set to '{}' for column compatibility. If Transit encryption fails (e.g., OpenBao unavailable), details are stored in plaintext as a fallback. + +IMPORTANT: log_action always opens its own AdminAsyncSessionLocal session and +commits internally. This guarantees the audit INSERT is persisted regardless of +whether the caller's DB session has already been committed or rolled back. The +``db`` parameter is kept for backward compatibility but is intentionally unused. """ import uuid @@ -23,7 +28,7 @@ logger = structlog.get_logger("audit") async def log_action( - db: AsyncSession, + db: AsyncSession, # kept for backward compat — not used internally tenant_id: uuid.UUID, user_id: uuid.UUID, action: str, @@ -33,9 +38,14 @@ async def log_action( details: Optional[dict[str, Any]] = None, ip_address: Optional[str] = None, ) -> None: - """Insert a row into audit_logs. Swallows all exceptions on failure.""" + """Insert a row into audit_logs using a dedicated session. + + Always self-commits so the INSERT is never dependent on the caller's + transaction lifecycle. Swallows all exceptions on failure. + """ try: import json as _json + from app.database import AdminAsyncSessionLocal details_dict = details or {} details_json = _json.dumps(details_dict) @@ -59,30 +69,34 @@ async def log_action( tenant_id=str(tenant_id), exc_info=True, ) - # Keep details_json as-is (plaintext fallback) encrypted_details = None - await db.execute( - text( - "INSERT INTO audit_logs " - "(tenant_id, user_id, action, resource_type, resource_id, " - "device_id, details, encrypted_details, ip_address) " - "VALUES (:tenant_id, :user_id, :action, :resource_type, " - ":resource_id, :device_id, CAST(:details AS jsonb), " - ":encrypted_details, :ip_address)" - ), - { - "tenant_id": str(tenant_id), - "user_id": str(user_id), - "action": action, - "resource_type": resource_type, - "resource_id": resource_id, - "device_id": str(device_id) if device_id else None, - "details": details_json, - "encrypted_details": encrypted_details, - "ip_address": ip_address, - }, - ) + # Use a dedicated session so this commit is independent of the + # caller's transaction (fixes dropped audit logs in routers that + # call log_action after their own db.commit()). + async with AdminAsyncSessionLocal() as audit_db: + await audit_db.execute( + text( + "INSERT INTO audit_logs " + "(tenant_id, user_id, action, resource_type, resource_id, " + "device_id, details, encrypted_details, ip_address) " + "VALUES (:tenant_id, :user_id, :action, :resource_type, " + ":resource_id, :device_id, CAST(:details AS jsonb), " + ":encrypted_details, :ip_address)" + ), + { + "tenant_id": str(tenant_id), + "user_id": str(user_id), + "action": action, + "resource_type": resource_type, + "resource_id": resource_id, + "device_id": str(device_id) if device_id else None, + "details": details_json, + "encrypted_details": encrypted_details, + "ip_address": ip_address, + }, + ) + await audit_db.commit() except Exception: logger.warning( "audit_log_insert_failed", diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 023141e..16934fa 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -47,6 +47,7 @@ services: volumes: - ./backend:/app - ./docker-data/git-store:/data/git-store + - ./docker-data/firmware-cache:/data/firmware-cache - ./docker-data/wireguard:/data/wireguard depends_on: postgres: