A review for revisionssicher logging asked that the trail answer who, when, what exactly and from which client IP. Edit asset logged only "Asset updated", asset and finding actions carried no IP (only logins did), a deleted user's rows turned into "System/Auto", a manual decommission was logged as a system action, and a settings change left no row at all. Every audit row with a user_id now gets the actor's username, client IP and user agent stamped on insert (before_insert listener, fed by an app-wide dependency that runs after ProxyHeadersMiddleware), so no writer can forget them. The username is a snapshot column (migration 065, backfilled), so it survives delete_user. Rows without a user (sync, scheduler) stay as before. Asset edits, single and bulk, record each changed field old -> new; a manual status change carries the user. A settings PUT writes CONFIG_CHANGE with the changed field names and old/new values whose secret subfields are masked for every key (ai_config is unprotected and holds api_token). An unchanged re-save writes nothing. The syslog message carries user=.
32 lines
943 B
Python
32 lines
943 B
Python
"""Add audit_logs.username
|
||
|
||
Revision ID: 065
|
||
Revises: 064
|
||
Create Date: 2026-10-26 10:00:00.000000
|
||
|
||
delete_user nulls audit_logs.user_id so the trail survives, but that turned
|
||
every row the user wrote into "System/Auto". The actor's name is now stamped
|
||
on insert (audit_log._stamp_actor); the backfill names every row whose user
|
||
still exists. Rows of users deleted before this revision stay unnamed.
|
||
Idempotent (IF NOT EXISTS), same shape as 053–064.
|
||
"""
|
||
from alembic import op
|
||
|
||
|
||
revision = "065"
|
||
down_revision = "064"
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
|
||
def upgrade() -> None:
|
||
op.execute("ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS username VARCHAR(50);")
|
||
op.execute(
|
||
"UPDATE audit_logs SET username = users.username FROM users "
|
||
"WHERE audit_logs.user_id = users.id AND audit_logs.username IS NULL;"
|
||
)
|
||
|
||
|
||
def downgrade() -> None:
|
||
op.execute("ALTER TABLE audit_logs DROP COLUMN IF EXISTS username;")
|