"""Tenant-scoped managers (``TENANCY.md`` §4). * :class:`TenantManager` — default manager of every tenant-owned model; fails closed (empty queryset) when no tenant is bound. * :class:`AllObjectsManager` — the ``all_objects`` escape hatch for migrations, admin, and platform staff. Never called from a view. * :class:`GlobalOrTenantManager` — for resources that exist globally and can be overridden per tenant (roles, pipelines, templates, feature flags). """ from __future__ import annotations from typing import TYPE_CHECKING, Any from django.db import models from django.db.models import Q from .context import get_current_tenant if TYPE_CHECKING: from django.db.models import QuerySet __all__ = ["TenantManager", "AllObjectsManager", "GlobalOrTenantManager"] class TenantManager(models.Manager): """Default manager on tenant-owned models. Scopes to the current tenant.""" def get_queryset(self) -> QuerySet[Any]: tenant = get_current_tenant() if tenant is None: # Fail closed: no tenant context must never mean "all tenants". return super().get_queryset().none() return super().get_queryset().filter(tenant_id=tenant.pk) def unsafe_all(self) -> QuerySet[Any]: """Explicit, greppable escape hatch for system code. Never from a view.""" return super().get_queryset() class AllObjectsManager(models.Manager): """Unscoped manager exposed as ``all_objects`` (admin/migrations/staff).""" class GlobalOrTenantManager(models.Manager): """Global rows (``tenant IS NULL``) plus the current tenant's overrides. Never exposes another tenant's rows. Use :meth:`resolve` to apply the documented precedence: tenant override → global default → ``None``. """ def get_queryset(self) -> QuerySet[Any]: tenant = get_current_tenant() qs = super().get_queryset() if tenant is None: return qs.filter(tenant__isnull=True) return qs.filter(Q(tenant_id=tenant.pk) | Q(tenant__isnull=True)) def resolve(self, **filters: Any) -> Any: """Returns the tenant override for ``filters``, else the global row.""" tenant = get_current_tenant() if tenant is not None: override = self.get_queryset().filter(tenant_id=tenant.pk, **filters).first() if override is not None: return override return self.get_queryset().filter(tenant__isnull=True, **filters).first() def unsafe_all(self) -> QuerySet[Any]: return super().get_queryset()