infrasynth-backend-kit/infrasynth/security/permissions.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

86 lines
3 KiB
Python

"""DRF permission classes built on :class:`AuthorizationService`.
Enforcement model
-----------------
* A superuser is always allowed.
* A **tenant owner** (``TenantMembership.is_owner`` for the resolved tenant) is
allowed — ownership is a capability, not a permission row.
* Otherwise the request user must hold at least one of the view's
``required_permissions`` (``HybridPermission``) or all of them
(``require_permission``).
* A view with no ``required_permissions`` only needs authentication.
The underlying :class:`AuthorizationService` is deliberately strict (owners are
not implicitly granted every codename) so it stays a pure permission resolver;
ownership is handled at the HTTP boundary here.
"""
from __future__ import annotations
from typing import Any
from rest_framework.permissions import BasePermission
from .services import AuthorizationService
def is_tenant_owner(user: Any) -> bool:
"""True when ``user`` owns the currently bound tenant."""
if not user or not getattr(user, "is_authenticated", False):
return False
# System users (API keys) are not database-backed memberships.
if not hasattr(user, "_meta") or getattr(user, "pk", None) is None:
return False
from infrasynth.tenancy.context import get_current_tenant
from infrasynth.tenancy.models import TenantMembership
tenant = get_current_tenant()
if tenant is None:
return False
return TenantMembership.objects.filter(
tenant=tenant,
user=user,
is_active=True,
is_owner=True,
).exists()
class HybridPermission(BasePermission):
"""Allows when the user is an owner or holds any ``required_permissions``."""
def has_permission(self, request, view):
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return False
if getattr(user, "is_superuser", False):
return True
if is_tenant_owner(user):
return True
required = getattr(view, "required_permissions", []) or []
if not required:
return True
return AuthorizationService().has_any_permission(user, required)
class IsAuthenticatedAndPermitted(HybridPermission):
"""The idiom for kit views: authenticated, then permission-checked."""
def has_permission(self, request, view):
if not getattr(getattr(request, "user", None), "is_authenticated", False):
return False
return super().has_permission(request, view)
def require_permission(*codenames: str):
"""View (or view-decorator) requiring *all* listed permissions."""
class PermissionRequired(IsAuthenticatedAndPermitted):
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
user = request.user
if getattr(user, "is_superuser", False) or is_tenant_owner(user):
return True
return AuthorizationService().has_all_permissions(user, list(codenames))
return PermissionRequired