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
90 lines
3 KiB
Python
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
|