fix: audit logs never persisted + firmware-cache permission denied

Two bugs fixed:

1. audit_service.py: log_action() inserted into audit_logs using the
   caller's DB session but never committed. Any router that called
   db.commit() before log_action() (firmware, devices, config_editor,
   alerts, certificates) had its audit rows silently rolled back when
   the request session closed.
   Fix: log_action now opens its own AdminAsyncSessionLocal and self-
   commits, making audit persistence independent of the caller's
   transaction. The 'db' parameter is kept for backward compat but
   unused. Affects 5 routers (firmware, devices, config_editor,
   alerts, certificates).

2. docker-compose.override.yml: /data/firmware-cache had no volume
   mount so the directory didn't exist in the container, causing
   firmware downloads to fail with Permission denied.
   Fix: bind-mount docker-data/firmware-cache:/data/firmware-cache
   so firmware images survive container restarts.
This commit is contained in:
Cog
2026-03-11 23:02:36 -05:00
committed by Jason Staack
parent 21b8ce029f
commit 6b22741f54
2 changed files with 39 additions and 24 deletions

View File

@@ -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",

View File

@@ -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: