infrasynth-backend-kit/infrasynth/audit/middleware.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

90 lines
3 KiB
Python

import time
import uuid
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from .models import APIInteractionLog
class AuditAPIMiddleware(MiddlewareMixin):
def process_request(self, request):
if not getattr(request, "request_id", None):
request.request_id = str(uuid.uuid4())
request._audit_start_time = time.time()
def process_response(self, request, response):
config = getattr(settings, "INFRASYNTH_AUDIT", {})
if not config.get("ENABLE_API_LOGGING", True):
return response
path = request.path
if path.startswith("/admin/"):
return response
if hasattr(request, "_audit_start_time"):
duration_ms = int((time.time() - request._audit_start_time) * 1000)
else:
duration_ms = 0
max_body = config.get("MAX_BODY_SIZE_BYTES", 5000)
request_body = None
response_body = None
sensitive_keys = config.get("SENSITIVE_KEYS", [])
try:
raw_body = getattr(request, "body", b"")
if raw_body and len(raw_body) <= max_body:
body = raw_body.decode("utf-8", errors="replace")
if not any(k in body.lower() for k in sensitive_keys):
import json
try:
request_body = json.loads(body)
except (json.JSONDecodeError, ValueError):
request_body = {"_truncated": True}
except Exception:
pass
try:
if hasattr(response, "data") and response.data:
import json as _json
try:
raw = _json.dumps(response.data)
if len(raw) <= max_body:
response_body = response.data
except (TypeError, ValueError):
pass
except Exception:
pass
actor = getattr(request, "user", None)
if actor and not actor.is_authenticated:
actor = None
if actor is not None and not hasattr(actor, "_meta"):
actor = None
from infrasynth.tenancy.context import get_current_tenant
tenant = get_current_tenant()
try:
APIInteractionLog.objects.create(
tenant=tenant,
method=request.method,
path=path,
status_code=response.status_code,
request_body=request_body,
response_body=response_body,
ip_address=request.META.get("REMOTE_ADDR"),
actor=actor,
duration_ms=duration_ms,
request_id=getattr(request, "request_id", ""),
user_agent=request.META.get("HTTP_USER_AGENT", ""),
)
except Exception: # noqa: BLE001 - audit must never break the response
import logging
logging.getLogger(__name__).exception("Failed to persist APIInteractionLog")
return response