infrasynth-backend-kit/infrasynth/tenancy/mixins.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

71 lines
2.1 KiB
Python

"""Abstract model mixins for tenant-owned and global+override resources.
Using a mixin keeps the ``tenant`` field and managers consistent across every
app without repeating them. Concrete models still declare their own ``db_table``
and constraints. The mixin is the sanctioned way for another app to depend on
``infrasynth.tenancy`` (see the dependency graph in ``PLAN.md`` §3).
"""
from __future__ import annotations
from typing import Any
from django.db import models
from .context import get_current_tenant
from .managers import AllObjectsManager, GlobalOrTenantManager, TenantManager
__all__ = ["TenantOwnedModel", "GlobalOrTenantModel"]
class TenantOwnedModel(models.Model):
"""Base for every tenant-owned model (``TENANCY.md`` §4).
Provides a non-null ``tenant`` FK, the fail-closed default ``TenantManager``,
and the unscoped ``all_objects`` escape hatch. On save, an unset tenant is
filled from the bound context so writes inside a request/task land in the
right tenant; a write with neither is rejected by the database.
"""
tenant = models.ForeignKey(
"tenancy.Tenant",
on_delete=models.CASCADE,
related_name="+",
editable=False,
)
objects = TenantManager()
all_objects = AllObjectsManager()
class Meta:
abstract = True
def save(self, *args: Any, **kwargs: Any) -> None:
if self.tenant_id is None:
tenant = get_current_tenant()
if tenant is not None:
self.tenant = tenant
super().save(*args, **kwargs)
class GlobalOrTenantModel(models.Model):
"""Base for resources that exist globally and can be overridden per tenant.
``tenant IS NULL`` is the platform default; a non-null tenant is that
tenant's override. The default manager only ever exposes the global rows
plus the current tenant's rows, never another tenant's.
"""
tenant = models.ForeignKey(
"tenancy.Tenant",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="+",
)
objects = GlobalOrTenantManager()
all_objects = AllObjectsManager()
class Meta:
abstract = True