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