infrasynth-backend-kit/infrasynth/audit/models.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

83 lines
3 KiB
Python

from django.conf import settings
from django.db import models
from infrasynth.tenancy.managers import AllObjectsManager
class ModelChangeLog(models.Model):
tenant = models.ForeignKey(
"tenancy.Tenant",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="+",
help_text="Null = platform action; set for tenant-scoped actions.",
)
model_label = models.CharField(max_length=200, db_index=True)
object_id = models.CharField(max_length=200, db_index=True)
action = models.CharField(
max_length=10,
choices=[("create", "create"), ("update", "update"), ("delete", "delete")],
)
changes = models.JSONField(help_text="Dict with {field_name: [old_value, new_value]}")
actor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
request_id = models.CharField(max_length=64, help_text="UUID for request correlation")
class Meta:
db_table = "audit_model_change_log"
indexes = [
models.Index(fields=["tenant_id", "model_label", "object_id"]),
models.Index(fields=["tenant_id", "timestamp"]),
]
objects = AllObjectsManager()
class APIInteractionLog(models.Model):
tenant = models.ForeignKey(
"tenancy.Tenant",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="+",
)
method = models.CharField(max_length=10, db_index=True)
path = models.CharField(max_length=500, db_index=True)
status_code = models.PositiveSmallIntegerField(db_index=True)
request_body = models.JSONField(null=True, blank=True)
response_body = models.JSONField(null=True, blank=True)
ip_address = models.GenericIPAddressField(null=True)
actor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
duration_ms = models.PositiveIntegerField()
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
request_id = models.CharField(max_length=64, db_index=True)
user_agent = models.TextField(blank=True, default="")
class Meta:
db_table = "audit_api_interaction_log"
indexes = [models.Index(fields=["tenant_id", "timestamp"])]
objects = AllObjectsManager()
class SecurityEvent(models.Model):
tenant = models.ForeignKey(
"tenancy.Tenant",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="+",
)
event_type = models.CharField(max_length=50, db_index=True)
actor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
ip_address = models.GenericIPAddressField(null=True)
metadata = models.JSONField(default=dict)
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
request_id = models.CharField(max_length=64)
class Meta:
db_table = "audit_security_event"
indexes = [models.Index(fields=["tenant_id", "event_type"])]
objects = AllObjectsManager()