Gestion du Contexte Tenant

Les context variables (contextvars) propagent le tenant, l’utilisateur et la membership courants à travers toute la pile d’appels, y compris les coroutines ASGI.

Module : src.share_kernel.infrastructure.context

Variables de contexte

src.share_kernel.infrastructure.context.current_tenant_var: ContextVar[Any]

Instance du tenant courant (ou None pour les routes publiques).

src.share_kernel.infrastructure.context.current_user_var: ContextVar[Any]

Instance de l’utilisateur authentifié courant.

src.share_kernel.infrastructure.context.current_membership_var: ContextVar[Any]

Instance du TenantMembership pour l’utilisateur dans le tenant actif.

src.share_kernel.infrastructure.context.correlation_id_var: ContextVar[str | None]

ID de corrélation pour le traçage distribué (HTTP → Celery).

src.share_kernel.infrastructure.context.cross_tenant_mode_var: ContextVar[bool]

Flag indiquant que le mode cross-tenant est actif. Les managers vérifient ce flag pour désactiver le filtrage automatique.

Accesseurs

src.share_kernel.infrastructure.context.get_current_tenant() Any

Retourne le tenant courant, ou None.

src.share_kernel.infrastructure.context.get_current_user() Any

Retourne l’utilisateur courant, ou None.

src.share_kernel.infrastructure.context.get_current_membership() Any

Retourne la membership courante, ou None.

src.share_kernel.infrastructure.context.get_correlation_id() str | None

Retourne l’ID de corrélation courant, ou None.

Context Manager : tenant_context

src.share_kernel.infrastructure.context.tenant_context(tenant_or_id, *, user=None, membership=None)

Context manager pour basculer vers un tenant spécifique.

Accepte un objet Tenant, un UUID, ou un slug (string).

Active le backend d’isolation et configure les contextvars pour la durée du bloc, puis restaure l’état précédent.

Parameters:
  • tenant_or_id – Tenant, UUID, ou slug

  • user – Utilisateur optionnel à associer

  • membership – Membership optionnelle à associer

Yields:

L’instance Tenant résolue

from src.share_kernel.infrastructure.context import tenant_context

# Par objet Tenant
with tenant_context(my_tenant) as tenant:
    products = Product.objects.all()  # Scopé au tenant

# Par slug
with tenant_context("acme-corp"):
    do_something()

# Par UUID
with tenant_context(uuid.UUID("...")) as tenant:
    orders = Order.objects.all()

# Avec utilisateur et membership
with tenant_context(tenant, user=admin_user, membership=admin_membership):
    # PIP peut résoudre les attributs de l'utilisateur
    policy_check()

Context Manager : async_tenant_context

src.share_kernel.infrastructure.context.async_tenant_context(tenant_or_id, *, user=None, membership=None)

Version asynchrone de tenant_context() pour les vues ASGI.

async with async_tenant_context("acme-corp") as tenant:
    products = await Product.objects.all().aall()

Context Manager : cross_tenant_context

src.share_kernel.infrastructure.context.cross_tenant_context()

Désactive temporairement l’isolation tenant pour les opérations cross-tenant (admin, reporting, migrations de données).

  • Désactive le backend d’isolation

  • Suspend RLS si applicable

  • Efface les contextvars tenant/user/membership

  • Active le flag cross_tenant_mode_var

Prérequis PostgreSQL (RLS) : le rôle DB doit avoir BYPASSRLS.

from src.share_kernel.infrastructure.context import cross_tenant_context
from django.db.models import Sum

with cross_tenant_context():
    # Pas de filtrage tenant — requêtes sur tous les tenants
    all_orders = Order.objects.all()
    total = all_orders.aggregate(Sum("amount"))
    print(f"Revenu total multi-tenant: {total}")

Warning

Utiliser avec précaution. Le code dans ce bloc a accès à toutes les données de tous les tenants.

Diagramme de flux

TenantMiddleware
    │
    ├── Résout le tenant (JWT/Header)
    ├── current_tenant_var.set(tenant)
    ├── current_user_var.set(user)
    ├── current_membership_var.set(membership)
    ├── correlation_id_var.set(uuid4())
    ├── backend.activate_tenant(tenant, connection)
    │
    ▼
Traitement de la requête
    │
    ├── TenantSpecificManager.get_queryset()
    │   └── Lit current_tenant_var pour filtrer
    │
    ├── PIP.resolve_attributes()
    │   └── Lit current_user_var, current_membership_var
    │
    ├── TenantContextFilter (logging)
    │   └── Lit tenant_id, user_id, correlation_id
    │
    └── tenant_cache_key()
        └── Lit current_tenant_var pour préfixer
    │
    ▼
Fin de requête
    │
    ├── backend.deactivate_tenant(connection)
    └── Reset de tous les contextvars