Il problema che Django non risolve da solo
Django include un sistema di permessi robusto. Puoi definire permessi a livello di modello — add_progetto, change_progetto, delete_progetto, view_progetto — e assegnarli a utenti o gruppi. Per la maggior parte dei casi è più che sufficiente.
Il problema me lo sono trovato davanti quando ho dovuto gestire un permesso che non riguardava "può modificare i progetti" in generale, ma "può modificare solo i progetti del suo ente". O, per le commissioni di selezione, "può vedere solo le candidature a cui è stato assegnato". Una differenza piccola sulla carta, enorme nel codice.
Django, nativamente, questo non lo sa fare. L'alternativa è filtrare ogni query a mano — Progetto.objects.filter(ente=request.user.responsabile.ente) — e funziona, fino a quando non dimentichi di farlo in una view, o un serializzatore DRF lo salta, o aggiungi un endpoint nuovo senza ricordarti del filtro. Basta una distrazione per esporre dati che nessuno doveva vedere.
Come abbiamo visto nell'articolo su OWASP Top 10 applicato a Django, il Broken Access Control è la vulnerabilità più diffusa nelle applicazioni web — e nasce quasi sempre da esattamente questo: permessi non verificati a livello di oggetto.
django-guardian risolve il problema alla radice.

Cos'è django-guardian e come funziona
django-guardian è una libreria che estende il sistema di autenticazione di Django aggiungendo un backend per i permessi a livello di oggetto. Invece di chiedere "questo utente ha il permesso view_progetto?" puoi chiedere "questo utente ha il permesso view_progetto su questo specifico Progetto?".
Internamente, django-guardian crea due tabelle nel database — UserObjectPermission e GroupObjectPermission — che associano un utente o un gruppo a un permesso su una specifica istanza di un modello. Usa i ContentTypes di Django per riferirsi ai modelli in modo generico.
Installazione:
pip install django-guardiansettings.py:
INSTALLED_APPS = [
"guardian",
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
]
ANONYMOUS_USER_NAME = NoneANONYMOUS_USER_NAME = None disabilita il supporto per gli utenti anonimi — nella maggior parte delle applicazioni aziendali non serve e aggiunge complessità inutile.
Dopo migrate, le tabelle di django-guardian vengono create automaticamente.
Il caso reale: Skara e i permessi tra enti
Skara è un gestionale per la gestione di progetti di servizio civile. Ogni progetto appartiene a un ente — un'organizzazione che ha aderito al programma. I responsabili di un ente devono poter vedere e modificare solo i progetti del loro ente, non quelli degli altri.
Con il sistema di permessi standard di Django, avrei dovuto filtrare ogni queryset a mano:
def get_queryset(self):
return Progetto.objects.filter(ente=self.request.user.responsabile.ente)Ci ho provato per un po'. Poi ho iniziato ad aggiungere view nuove e mi sono reso conto che stavo riscrivendo lo stesso filtro ovunque, con il rischio concreto di scordarmelo da qualche parte — una viewset DRF generica, un endpoint aggiunto in fretta prima di una demo, qualcosa del genere.
Con django-guardian il permesso vive direttamente sull'oggetto, e Django lo verifica da solo quando uso i meccanismi standard — niente da ricordare ogni volta.
Assegnare il permesso quando si crea un progetto:
from guardian.shortcuts import assign_perm
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Progetto
@receiver(post_save, sender=Progetto)
def assegna_permessi_progetto(sender, instance, created, **kwargs):
if created:
ente = instance.ente
for responsabile in ente.responsabili.all():
assign_perm("view_progetto", responsabile.user, instance)
assign_perm("change_progetto", responsabile.user, instance)Ogni volta che viene creato un nuovo progetto, il signal assegna automaticamente i permessi a tutti i responsabili dell'ente. Se aggiungo un nuovo responsabile all'ente in un secondo momento, devo assegnare i permessi anche sui progetti esistenti — lo gestisco con un signal separato sul modello Responsabile.
Verificare il permesso in una view:
from guardian.shortcuts import get_objects_for_user
class ProgettoViewSet(viewsets.ModelViewSet):
serializer_class = ProgettoSerializer
def get_queryset(self):
return get_objects_for_user(
self.request.user,
"myapp.view_progetto",
klass=Progetto
)get_objects_for_user restituisce tutti gli oggetti su cui l'utente ha quel permesso specifico. Non devo filtrare per ente — django-guardian lo sa già. Se domani un responsabile cambia ente, revoco i vecchi permessi e assegno i nuovi, e automaticamente vede solo i progetti giusti.
assign_perm e remove_perm: le funzioni principali
Le due funzioni che usi più spesso sono assign_perm e remove_perm.
Assegnare un permesso a un utente su un oggetto:
from guardian.shortcuts import assign_perm
assign_perm("view_progetto", user, progetto)
assign_perm("change_progetto", user, progetto)Assegnare un permesso a un gruppo su un oggetto:
from guardian.shortcuts import assign_perm
from django.contrib.auth.models import Group
commissione = Group.objects.get(name="Commissione Alpha")
assign_perm("view_candidatura", commissione, candidatura)In Skara uso i gruppi per le commissioni di selezione — ogni commissione è un Group Django, e i membri hanno accesso solo alle candidature della loro commissione.
Revocare un permesso:
from guardian.shortcuts import remove_perm
remove_perm("change_progetto", user, progetto)Verificare se un utente ha un permesso su un oggetto:
user.has_perm("myapp.view_progetto", progetto)Questo funziona attraverso il backend di autenticazione configurato in settings.py — Django chiama automaticamente ObjectPermissionBackend di guardian quando viene passato un oggetto come secondo argomento.
Integrazione con Django REST Framework
Con DRF, puoi usare DjangoObjectPermissions come permission class — verifica automaticamente i permessi sull'oggetto quando viene chiamato get_object():
from rest_framework.permissions import DjangoObjectPermissions
class ProgettoViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, DjangoObjectPermissions]
serializer_class = ProgettoSerializer
def get_queryset(self):
return get_objects_for_user(
self.request.user,
"myapp.view_progetto",
klass=Progetto
)DjangoObjectPermissions gestisce automaticamente il mapping tra i metodi HTTP e i permessi Django:
- GET, HEAD, OPTIONS → view_
- POST → add_
- PUT, PATCH → change_
- DELETE → delete_
Per i permessi di visualizzazione (GET) su oggetti singoli, aggiungi anche djangorestframework-guardian:
pip install djangorestframework-guardianfrom rest_framework_guardian.filters import ObjectPermissionsFilter
class ProgettoViewSet(viewsets.ModelViewSet):
filter_backends = [ObjectPermissionsFilter]ObjectPermissionsFilter filtra automaticamente il queryset restituendo solo gli oggetti su cui l'utente ha il permesso view_
Testing dei permessi con django-guardian
Nei test, django-guardian funziona normalmente senza configurazione aggiuntiva. L'unica accortezza è usare il database nei test che verificano i permessi:
import pytest
from guardian.shortcuts import assign_perm
from myapp.tests.factories import ProgettoFactory, UserFactory
@pytest.mark.django_db
def test_responsabile_vede_solo_propri_progetti(client):
user_a = UserFactory()
user_b = UserFactory()
progetto_a = ProgettoFactory()
progetto_b = ProgettoFactory()
assign_perm("view_progetto", user_a, progetto_a)
assign_perm("view_progetto", user_b, progetto_b)
client.force_login(user_a)
response = client.get("/api/progetti/")
assert response.status_code == 200
ids = [p["id"] for p in response.json()]
assert str(progetto_a.id) in ids
assert str(progetto_b.id) not in idsQuesto test verifica esattamente il requisito di business: user_a vede solo i progetti su cui ha il permesso, non quelli degli altri.
Performance: il punto critico
django-guardian ha un costo in query. Ogni chiamata a get_objects_for_user genera query sulle tabelle UserObjectPermission e GroupObjectPermission. Su modelli con molte istanze e molti utenti, questo può diventare un problema.
Le ottimizzazioni principali:
Usare permessi diretti invece di generic foreign key — django-guardian supporta modelli custom per i permessi che usano ForeignKey dirette invece di GenericForeignKey, molto più veloci:
from guardian.models import UserObjectPermissionBase
class ProgettoUserPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(
Progetto,
on_delete=models.CASCADE
)
class Meta(UserObjectPermissionBase.Meta):
abstract = FalsePrefetch dei permessi — per evitare N+1 nelle view che mostrano liste con informazioni sui permessi:
from guardian.core import ObjectPermissionChecker
checker = ObjectPermissionChecker(request.user)
checker.prefetch_perms(queryset)
for obj in queryset:
if checker.has_perm("change_progetto", obj):
...In Skara uso i modelli custom per i permessi su tutti i modelli principali — la differenza di performance su dataset reali è significativa.
Quando usare django-guardian e quando no
Lo installerei di nuovo se:
- i permessi cambiano istanza per istanza dello stesso modello
- il criterio di accesso non si riduce a un filtro semplice su un campo
- serve che il permesso sia persistente e modificabile senza toccare il codice
- utenti diversi devono vedere sottoinsiemi diversi degli stessi dati
Lo eviterei se invece il criterio è sempre lo stesso — "l'utente vede solo i suoi oggetti" si risolve benissimo con owner=request.user, senza bisogno di una libreria in più. O se gli oggetti e gli utenti in gioco sono pochi: a quel punto l'overhead di guardian non si giustifica, e il sistema di permessi nativo di Django basta e avanza.
In Skara la scelta ha senso perché i permessi cambiano nel tempo — un responsabile passa a un altro ente, un membro lascia una commissione, un progetto viene trasferito ad altra competenza — e gestire tutto questo con filtri sparsi nelle view, sinceramente, non lo rifarei una seconda volta.