"""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 **any** of the view's ``required_permissions`` (set ``require_all = True`` to demand all of them). * A view with no ``required_permissions`` falls back to the model-derived codename (see :class:`AutoPermission`). * Declared gates (``infrasynth_gates``) run first for everyone, including owners and superusers; use :class:`infrasynth.gates.PermissionGate` when even the owner must hold a codename. 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 infrasynth.gates import evaluate_gates from infrasynth.shared.settings_utils import get_setting 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): """Authenticated, then permission-checked, with an owner/superuser bypass. A view declares ``required_permissions`` (any-of by default; set ``require_all = True`` for all-of). With none declared, the model-derived codename is enforced via :func:`evaluate_auto_permission`. """ def has_permission(self, request, view): user = getattr(request, "user", None) if not user or not getattr(user, "is_authenticated", False): return False # Declared gates apply to everyone, including owners and superusers. evaluate_gates(request, view) if getattr(user, "is_superuser", False): return True if is_tenant_owner(user): return True required = getattr(view, "required_permissions", []) or [] if not required: # No explicit permission: fall back to the model-derived codename. return evaluate_auto_permission(request, view) authz = AuthorizationService() if getattr(view, "require_all", False): return authz.has_all_permissions(user, list(required)) return authz.has_any_permission(user, list(required)) class AutoPermission(BasePermission): """Derives and enforces ``{app}.{action}_{model}`` permissions automatically. Only applies to model-backed DRF viewsets (and only when ``AUTO_PERMISSIONS`` is enabled); it abstains on plain APIViews so it is safe in ``DEFAULT_PERMISSION_CLASSES``. Tenant owners and superusers bypass, matching ``HybridPermission``. """ message = "You do not have permission to perform this action." 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 return evaluate_auto_permission(request, view) # Backwards-compatible alias: ``IsAuthenticatedAndPermitted`` is the documented # idiom name for kit views but is exactly ``HybridPermission``. IsAuthenticatedAndPermitted = HybridPermission def resolve_view_model(view) -> Any: """Best-effort concrete model behind a DRF view, or ``None``.""" model = getattr(getattr(view, "queryset", None), "model", None) if model is not None: return model get_queryset = getattr(view, "get_queryset", None) if callable(get_queryset): try: return getattr(get_queryset(), "model", None) except Exception: # noqa: BLE001 - not every queryset is safe to build return None return None def automatic_permissions(view) -> list[str]: """The codenames a view requires, explicitly declared or auto-derived. Priority: ``required_permissions`` (explicit) → ``action_permissions`` for the current action → derived ``{app}.{verb}_{model}`` → ``[]``. """ explicit = getattr(view, "required_permissions", None) if explicit: return list(explicit) action = getattr(view, "action", None) if not action: return [] action_map = getattr(view, "action_permissions", None) or {} if action in action_map: return [action_map[action]] model = resolve_view_model(view) if model is None: return [] from .catalog import permission_for return [permission_for(model, action)] def auto_permissions_enabled(view) -> bool: """Whether model-derived enforcement applies to ``view``.""" mode = get_setting("INFRASYNTH_SECURITY", "AUTO_PERMISSIONS", "global") if mode in (False, None, "off", "disabled"): return False if getattr(view, "auto_permissions", None) is False: return False if mode == "opt_in": return bool(getattr(view, "auto_permissions", False)) return True def evaluate_auto_permission(request, view) -> bool: """Runs the auto-permission check, abstaining when nothing is derivable.""" if not auto_permissions_enabled(view): return True codenames = automatic_permissions(view) if not codenames: return True 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 return AuthorizationService().has_any_permission(user, codenames)