Krótka odpowiedź
Większość implementacji permissions i ról w Django jest zła, bo modeluje strukturę firmy, a nie decyzję "kto może wykonać jaką operację na jakim zasobie w jakim kontekście".
Django daje solidne prymitywy (User, Group, Permission), ale nie rozwiązuje za Ciebie modelu policy i miejsca jej egzekucji.
Jeśli role są jedyną osią projektu, kończysz z role explosion, wyjątkami "na szybko" i wyciekami danych między tenantami.
Co jest źle w większości implementacji
Najczęstsze symptomy w projektach po 6-12 miesiącach:
- każda nowa funkcja dokłada kolejną rolę zamiast capability,
- autoryzacja jest sprawdzana w view, ale nie w queryset/service,
- endpoint zwraca dane, których użytkownik nie powinien widzieć,
- logika "owner albo admin" jest powielona w 20 miejscach,
- nikt nie ma jednej mapy "kto/co/może".
To nie jest problem składni Django. To problem architektury decyzji.
1) Anti-pattern: rola jako sztywny pakiet uprawnień
Gdy role są modelowane 1:1 z org chartem (np. sales_manager_eu, sales_manager_us, junior_support_night), liczba ról rośnie szybciej niż produkt.
Jak wygląda role explosion
- nowy wyjątek biznesowy -> nowa rola,
- tymczasowe uprawnienie -> "dopisane na stałe",
- brak dekompozycji na capability (
invoice.read,invoice.approve,refund.execute).
Po roku nie wiadomo już, która rola jest kanoniczna.
Lepszy wzorzec
Najpierw definiuj capability matrix, potem mapuj role na capabilities.
Role powinny być cienką warstwą agregacji, a nie jedynym nośnikiem semantyki autoryzacji.
Trade-off: więcej pracy koncepcyjnej na starcie, ale dużo niższy koszt zmian i audytu.
2) Anti-pattern: autoryzacja tylko w endpointach
has_perm w widoku to za mało, jeśli queryset zwraca zbyt szerokie dane.
W praktyce bezpieczeństwo łamie się nie na POST, tylko na "legalnym" GET bez ograniczenia scope.
Gdzie egzekwować policy
- warstwa wejścia (DRF
permission_classes) decyduje, czy akcja jest w ogóle dozwolona, - warstwa danych (
QuerySet, repo, service) decyduje, jakie rekordy są widoczne, - warstwa domeny decyduje, czy mutacja jest poprawna biznesowo.
# services/orders.py
from django.db.models import QuerySet
def visible_orders_for(user) -> QuerySet:
qs = Order.objects.all()
if user.is_superuser:
return qs
if user.has_perm("orders.view_all_orders"):
return qs.filter(company_id=user.company_id)
return qs.filter(owner_id=user.id)Jeśli policy nie filtruje danych, UI i endpointy będą tylko maskować problem.
3) Anti-pattern: "owner check" jako pełny model object permissions
obj.owner_id == request.user.id jest dobrym początkiem, ale nie wystarcza, gdy pojawia się delegacja, zespoły, zastępstwa i audytowalna odpowiedzialność.
Kiedy owner-based model wystarczy
- mały produkt,
- jeden typ zasobu,
- brak delegacji między użytkownikami,
- brak wymagań compliance.
Kiedy wejść w object-level permissions
- dostęp zależy od relacji użytkownik-zasób (np. reviewer, approver, proxy),
- uprawnienia są czasowe lub warunkowe,
- wymagany jest ślad "kto i dlaczego miał dostęp".
Wtedy rozważ django-guardian albo własny backend policy, ale nadal trzymaj jedną macierz decyzji.
4) Anti-pattern: brak centralnej mapy policy i testów
Najbardziej kosztowny błąd: autoryzacja rozlana po kodzie bez testów regresyjnych.
Dopóki zespół jest mały, "wszyscy wiedzą" jak działa dostęp. Przy skali wiedza znika.
Minimalny zestaw testów
- Test capability matrix: rola -> dozwolone operacje.
- Test data scope: użytkownik widzi tylko dopuszczalne rekordy.
- Test object rules:
has_object_permissiondla krytycznych akcji. - Test deny-by-default: brak jawnego allow oznacza odmowę.
Bez tych testów każda refaktoryzacja może otworzyć dane.
Jak dobrać architekturę autoryzacji w Django
Opcja A: Django Groups + model permissions
Dobra dla małych i średnich systemów, gdy:
- liczba ról jest stabilna,
- reguły są głównie globalne,
- object-level przypadki są rzadkie.
Opcja B: RBAC w DB + policy service
Dobra, gdy:
- role i capabilities często się zmieniają,
- masz wiele bounded contexts,
- potrzebujesz centralnej audytowalnej policy.
Opcja C: Hybryda RBAC + ABAC
Dobra, gdy decyzja zależy od kontekstu (tenant, region, pora, ownership, feature flag, status obiektu).
RBAC określa bazowy dostęp, ABAC doprecyzowuje warunki.
Trade-off: najwyższa elastyczność, ale też najwyższa złożoność testów i observability.
Plan naprawczy na 60 dni
- Spisz capability matrix dla 3-5 najważniejszych zasobów.
- Usuń role "tymczasowe" i zmapuj je do capability.
- Przenieś filtrowanie dostępu do queryset/service layer.
- Dodaj testy deny-by-default oraz testy data scope.
- Wydziel policy moduł (jedno miejsce decyzji).
- Włącz logowanie decyzji autoryzacyjnych dla akcji krytycznych.
- Zdefiniuj proces zmian ról: kto zatwierdza, jak testujesz, jak auditujesz.
- Dla wyjątków business-critical wdroż object-level permissions.
Finalny werdykt
Django nie jest problemem. Problemem jest traktowanie ról jako skrótu do całej autoryzacji.
Dobra implementacja zaczyna się od modelu decyzji i zakresu danych, a dopiero potem wybiera Group, custom backend lub hybrydę RBAC/ABAC.
Jeśli zrobisz to odwrotnie, aplikacja będzie działać, ale autoryzacja będzie stale pękać przy każdym nowym wymaganiu biznesowym.
