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:
@@ -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
|
(per-tenant data key) and stored in encrypted_details. The plaintext details
|
||||||
column is set to '{}' for column compatibility. If Transit encryption fails
|
column is set to '{}' for column compatibility. If Transit encryption fails
|
||||||
(e.g., OpenBao unavailable), details are stored in plaintext as a fallback.
|
(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
|
import uuid
|
||||||
@@ -23,7 +28,7 @@ logger = structlog.get_logger("audit")
|
|||||||
|
|
||||||
|
|
||||||
async def log_action(
|
async def log_action(
|
||||||
db: AsyncSession,
|
db: AsyncSession, # kept for backward compat — not used internally
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
action: str,
|
action: str,
|
||||||
@@ -33,9 +38,14 @@ async def log_action(
|
|||||||
details: Optional[dict[str, Any]] = None,
|
details: Optional[dict[str, Any]] = None,
|
||||||
ip_address: Optional[str] = None,
|
ip_address: Optional[str] = None,
|
||||||
) -> 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:
|
try:
|
||||||
import json as _json
|
import json as _json
|
||||||
|
from app.database import AdminAsyncSessionLocal
|
||||||
|
|
||||||
details_dict = details or {}
|
details_dict = details or {}
|
||||||
details_json = _json.dumps(details_dict)
|
details_json = _json.dumps(details_dict)
|
||||||
@@ -59,30 +69,34 @@ async def log_action(
|
|||||||
tenant_id=str(tenant_id),
|
tenant_id=str(tenant_id),
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
# Keep details_json as-is (plaintext fallback)
|
|
||||||
encrypted_details = None
|
encrypted_details = None
|
||||||
|
|
||||||
await db.execute(
|
# Use a dedicated session so this commit is independent of the
|
||||||
text(
|
# caller's transaction (fixes dropped audit logs in routers that
|
||||||
"INSERT INTO audit_logs "
|
# call log_action after their own db.commit()).
|
||||||
"(tenant_id, user_id, action, resource_type, resource_id, "
|
async with AdminAsyncSessionLocal() as audit_db:
|
||||||
"device_id, details, encrypted_details, ip_address) "
|
await audit_db.execute(
|
||||||
"VALUES (:tenant_id, :user_id, :action, :resource_type, "
|
text(
|
||||||
":resource_id, :device_id, CAST(:details AS jsonb), "
|
"INSERT INTO audit_logs "
|
||||||
":encrypted_details, :ip_address)"
|
"(tenant_id, user_id, action, resource_type, resource_id, "
|
||||||
),
|
"device_id, details, encrypted_details, ip_address) "
|
||||||
{
|
"VALUES (:tenant_id, :user_id, :action, :resource_type, "
|
||||||
"tenant_id": str(tenant_id),
|
":resource_id, :device_id, CAST(:details AS jsonb), "
|
||||||
"user_id": str(user_id),
|
":encrypted_details, :ip_address)"
|
||||||
"action": action,
|
),
|
||||||
"resource_type": resource_type,
|
{
|
||||||
"resource_id": resource_id,
|
"tenant_id": str(tenant_id),
|
||||||
"device_id": str(device_id) if device_id else None,
|
"user_id": str(user_id),
|
||||||
"details": details_json,
|
"action": action,
|
||||||
"encrypted_details": encrypted_details,
|
"resource_type": resource_type,
|
||||||
"ip_address": ip_address,
|
"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:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"audit_log_insert_failed",
|
"audit_log_insert_failed",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./docker-data/git-store:/data/git-store
|
- ./docker-data/git-store:/data/git-store
|
||||||
|
- ./docker-data/firmware-cache:/data/firmware-cache
|
||||||
- ./docker-data/wireguard:/data/wireguard
|
- ./docker-data/wireguard:/data/wireguard
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
Reference in New Issue
Block a user