- infrasynth.configs: typed multi-tenant config store (registry, service, secrets, cache) + public config_changed/config_reset signals and API - emit the declared-but-dead signals (features flags/overrides, scheduler task_completed/task_failed, tenancy tenant_updated, audit model_changed) and per-model audit field exclusions - security: permission catalog (security_permission), Django-style model-derived AutoPermission, PermissionRegistry, RoleAssignment, global-or-tenant Grant/Revoke, catalog API - consolidate the permission surface: PermissionRegistry only (drop the settings dict), IsAuthenticatedAndPermitted aliases HybridPermission, require_permission replaced by required_permissions + require_all - packaging: add [build-system]; add Forgejo publish workflow (.forgejo)
91 lines
2.9 KiB
Python
91 lines
2.9 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", "ContextGlobalOrTenantModel"]
|
|
|
|
|
|
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
|
|
|
|
|
|
class ContextGlobalOrTenantModel(GlobalOrTenantModel):
|
|
"""A global-or-tenant model that fills an unset tenant from context on save.
|
|
|
|
The global row still exists (``force_global=True`` on :meth:`save`), but a
|
|
normal write inside a request/task lands in the current tenant instead of
|
|
silently becoming a platform-wide row. Used for per-user overrides
|
|
(``Grant``/``Revoke``) where a tenant-scoped write is the safe default.
|
|
"""
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def save(self, *args: Any, force_global: bool = False, **kwargs: Any) -> None:
|
|
if self.tenant_id is None and not force_global:
|
|
tenant = get_current_tenant()
|
|
if tenant is not None:
|
|
self.tenant = tenant
|
|
super().save(*args, **kwargs)
|