58 lines
2.3 KiB
Python
58 lines
2.3 KiB
Python
"""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
|