You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
75 lines
2.4 KiB
75 lines
2.4 KiB
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
|
|
from flask import current_app, has_request_context, request
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from app.extensions import db
|
|
from app.models import Account, AuditLog
|
|
|
|
ANONYMOUS_ACTOR_USERNAME = "anonymous"
|
|
|
|
|
|
def account_snapshot(account: Account | None) -> dict[str, object] | None:
|
|
if account is None:
|
|
return None
|
|
|
|
return {
|
|
"id": account.id,
|
|
"username": account.username,
|
|
"display_name": account.display_name,
|
|
"role": account.role,
|
|
"status": account.status,
|
|
}
|
|
|
|
|
|
def write_audit_log(
|
|
*,
|
|
action_type: str,
|
|
target_type: str,
|
|
actor: Account | None = None,
|
|
actor_username: str | None = None,
|
|
actor_display_name: str | None = None,
|
|
target_id: int | None = None,
|
|
target_display_name: str | None = None,
|
|
before_data: Mapping[str, object] | list[object] | None = None,
|
|
after_data: Mapping[str, object] | list[object] | None = None,
|
|
commit: bool = True,
|
|
) -> AuditLog | None:
|
|
log = AuditLog(
|
|
actor_user_id=actor.id if actor is not None else None,
|
|
actor_username=actor.username if actor is not None else (actor_username or ANONYMOUS_ACTOR_USERNAME),
|
|
actor_display_name=actor.display_name if actor is not None else actor_display_name,
|
|
action_type=action_type,
|
|
target_type=target_type,
|
|
target_id=target_id,
|
|
target_display_name=target_display_name,
|
|
before_data_json=dict(before_data) if isinstance(before_data, Mapping) else before_data,
|
|
after_data_json=dict(after_data) if isinstance(after_data, Mapping) else after_data,
|
|
)
|
|
_attach_request_metadata(log)
|
|
db.session.add(log)
|
|
|
|
if not commit:
|
|
return log
|
|
|
|
try:
|
|
db.session.commit()
|
|
except SQLAlchemyError:
|
|
db.session.rollback()
|
|
current_app.logger.exception("Failed to persist audit log for action '%s'.", action_type)
|
|
return None
|
|
|
|
return log
|
|
|
|
|
|
def _attach_request_metadata(log: AuditLog) -> None:
|
|
if not has_request_context():
|
|
return
|
|
|
|
log.request_path = request.path
|
|
log.request_method = request.method
|
|
log.ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
|
|
user_agent = request.user_agent.string if request.user_agent is not None else None
|
|
log.user_agent = user_agent[:512] if user_agent else None
|
|
|