gmao/app_new/core/audit.py

59 lines
2.3 KiB
Python
Raw Normal View History

"""Capture centralisée et expurgée des écritures HTTP."""
import json
import re
from flask import current_app, request
from flask_login import current_user
from ..extensions import db
from .models.audit import AuditLog
SENSITIVE_MARKERS = ("password", "mot_de_passe", "secret", "token", "api_key", "apikey", "pin")
def _redacted_payload():
data = request.get_json(silent=True) if request.is_json else request.form.to_dict(flat=False)
if not isinstance(data, dict):
return None
clean = {}
for key, value in data.items():
clean[key] = "[MASQUÉ]" if any(marker in key.lower() for marker in SENSITIVE_MARKERS) else value
encoded = json.dumps(clean, ensure_ascii=False, default=str)
return encoded[:8000]
def _entity_from_path():
endpoint = request.endpoint or ""
blueprint = endpoint.split(".", 1)[0] or None
match = re.search(r"/(\d+)(?:/|$)", request.path)
return blueprint, match.group(1) if match else None
def record_mutation(response):
if request.method in {"GET", "HEAD", "OPTIONS"} or response.status_code >= 500:
return response
entity_type, entity_id = _entity_from_path()
values = {
"user_id": current_user.id if current_user.is_authenticated else None,
"username": current_user.username if current_user.is_authenticated else "anonyme",
"action": request.method,
"endpoint": request.endpoint,
"path": request.path[:500],
"entity_type": entity_type,
"entity_id": entity_id,
"payload": _redacted_payload(),
"status_code": response.status_code,
"ip_address": (request.headers.get("X-Forwarded-For", request.remote_addr or "").split(",")[0].strip())[:64],
}
try:
# Transaction séparée : le journal ne valide jamais par accident des
# changements laissés en attente par une vue métier.
with db.engine.begin() as connection:
connection.execute(AuditLog.__table__.insert().values(**values))
except Exception:
# Une panne du journal ne doit pas transformer une réponse métier déjà
# produite en erreur 500 ; l'erreur reste visible dans les logs serveur.
current_app.logger.exception("Impossible d'enregistrer l'opération dans le journal d'audit")
return response