Nota 04 / Separazione dei tenant
Django: accesso ai dati vincolato al tenant
Un ID di documento non è un permesso. Verifica l’appartenenza, limita le query e prova gli ID di altri tenant.
Il problema: un oggetto valido, ma altrui
In un’applicazione Django condivisa lo stesso utente può appartenere a più tenant. Un endpoint riceve l’ID di un documento e il tenant selezionato. Se cerca solo per chiave primaria, un ID valido può aprire il documento sbagliato. Anche una UUID difficile da indovinare non sostituisce il controllo dei permessi.
Tratta il tenant ricevuto da URL, cookie o form come un input. Il database deve confermare che l’utente autenticato possa accedervi. Solo dopo esegui la query sul documento.
Due controlli nello stesso punto
L’esempio usa un modello di permessi semplice: ogni appartenenza consente di leggere tutti i documenti di quel tenant. Il servizio verifica l’appartenenza e cerca l’ID solo nel QuerySet limitato. Senza appartenenza solleva PermissionDenied; per un documento non visibile solleva Http404. La view deve chiamare il servizio dopo l’autenticazione.
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import models
from django.shortcuts import get_object_or_404
class Tenant(models.Model):
name = models.CharField(max_length=120)
class Meta:
app_label = "notes_demo"
class Membership(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
class Meta:
app_label = "notes_demo"
constraints = [models.UniqueConstraint(
fields=["tenant", "user"], name="demo_member_unique")]
class Document(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
title = models.CharField(max_length=240)
class Meta:
app_label = "notes_demo"
def read_document(user, tenant_id, document_id):
# Verifica nel database l’appartenenza al tenant.
if not user.is_authenticated or not Membership.objects.filter(
user=user, tenant_id=tenant_id
).exists():
raise PermissionDenied
# Cerca l’ID del documento nel tenant autorizzato.
return get_object_or_404(
Document.objects.filter(tenant_id=tenant_id),
pk=document_id,
)
Prova l’accesso vietato
Il test locale crea due tenant sintetici. Un utente appartiene soltanto a uno. Il test legge il documento consentito, richiede un ID altrui nel tenant autorizzato e poi prova a selezionare il tenant altrui. Entrambi i percorsi vietati devono fallire. Anche l’utente anonimo viene respinto.
Eseguito con Python 3.12 e Django 5.2 su SQLite locale. Verifica l’accesso ORM di questo esempio. Non verifica lock specifici di PostgreSQL, Row-Level Security o concorrenza.
Limiti: il servizio non è tutta l’applicazione
Document.objects senza filtri resta accessibile. Admin, job, esportazioni e nuove view possono aggirare il servizio. Applica la stessa regola in tutti i percorsi e cerca le query non limitate durante la revisione. Le chiavi della cache devono includere il tenant: una cache basata solo sull’ID del documento può vanificare il controllo.
Per le scritture questo esempio di lettura non basta: devi verificare anche chiavi esterne e ruoli. Se l’appartenenza può essere revocata durante la richiesta, serve una strategia transazionale esplicita. PostgreSQL Row-Level Security può aggiungere una difesa; devi considerare proprietario della tabella, ruoli BYPASSRLS e contesto dei pool di connessioni.