PythonDjangoArchitektura

Django permissions i role - dlaczego większość implementacji jest zła

Dlaczego role w Django często kończą się role explosion i wyciekami danych? Praktyczny model RBAC + object-level checks, który skaluje się w produkcji.

Django permissions i role - dlaczego większość implementacji jest zła

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.
python
# 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

  1. Test capability matrix: rola -> dozwolone operacje.
  2. Test data scope: użytkownik widzi tylko dopuszczalne rekordy.
  3. Test object rules: has_object_permission dla krytycznych akcji.
  4. 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

  1. Spisz capability matrix dla 3-5 najważniejszych zasobów.
  2. Usuń role "tymczasowe" i zmapuj je do capability.
  3. Przenieś filtrowanie dostępu do queryset/service layer.
  4. Dodaj testy deny-by-default oraz testy data scope.
  5. Wydziel policy moduł (jedno miejsce decyzji).
  6. Włącz logowanie decyzji autoryzacyjnych dla akcji krytycznych.
  7. Zdefiniuj proces zmian ról: kto zatwierdza, jak testujesz, jak auditujesz.
  8. 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.