skip to content

In Django, how does PermissionRequiredMixin enforce a permission on a class-based view, and how do you customise what a denied user gets?

level: middleimportance: must knowfreq 58%

answer

  1. the check happens in dispatch
  2. has_permission calls has_perms
  3. AccessMixin decides the denial
  4. logged-in users are treated differently

basics

~10 s

PermissionRequiredMixin checks request.user.has_perms() in dispatch(), before get() or post(). On failure handle_no_permission() raises PermissionDenied (403) for logged-in users and redirects anonymous ones to login unless raise_exception is True; override has_permission() or handle_no_permission() to customise.

solid answer

~40 s

`PermissionRequiredMixin` from `django.contrib.auth.mixins` overrides `dispatch()`: it calls `has_permission()`, which by default returns `request.user.has_perms(self.get_permission_required())`, before any handler such as `get()` or `post()` runs. `permission_required` may be one string or an iterable, and all are required. On failure it calls `handle_no_permission()` from `AccessMixin`: if `raise_exception` is true **or the user is authenticated**, it raises `PermissionDenied` (a 403); otherwise it redirects the anonymous user to `get_login_url()` with `next`. That differs from the `permission_required` decorator, which by default redirects logged-in users to login too. You customise with `login_url`, `redirect_field_name`, `permission_denied_message` and `raise_exception`, or by overriding `has_permission()` for any-of or extra rules and `handle_no_permission()` for a friendlier response. List the mixin first in the class bases.

code

python · 18 lines
python
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import redirect
from django.views.generic import DetailView

from payments.models import Payment


class RefundPaymentView(PermissionRequiredMixin, DetailView):
    model = Payment
    template_name = "payments/refund_confirm.html"
    permission_required = "payments.refund_payment"

    def handle_no_permission(self):
        if self.request.user.is_authenticated:
            messages.error(self.request, "Refunds are limited to the finance team.")
            return redirect("payments:detail", pk=self.kwargs["pk"])
        return super().handle_no_permission()

go deeper

for a junior

Recall the import from django.contrib.auth.mixins, the permission_required attribute with an app_label.codename string, and that the mixin goes first in the bases.

for a middle

Explain the dispatch-time check, has_permission calling has_perms, and handle_no_permission's split between 403 for logged-in users and a redirect for anonymous ones.

for a senior

Choose the right hook for each customisation, note that the mixin never checks the object, and keep denial responses consistent across views.

for a principal

Decide whether a shared base view or a project convention should fix the denial policy and message, so teams do not each invent their own.

## Where the check runs `PermissionRequiredMixin` lives in `django.contrib.auth.mixins` and is meant to be listed **first** in a class-based view's bases, for example `class RefundPaymentView(PermissionRequiredMixin, DetailView)`. It overrides `dispatch()`, the method every request passes through before Django picks the handler (`get()`, `post()` and so on). Its `dispatch()` does two things: 1. calls `self.has_permission()`; 2. if that is false, returns `self.handle_no_permission()`; otherwise continues to the normal `dispatch()` and the handler. Because the check sits in `dispatch()`, it covers **every HTTP method** of the view, and it runs before `get_object()`, `get_queryset()` or any form processing. ## The attributes and hooks | Name | Default | Role | |---|---|---| | `permission_required` | `None` | one `"app_label.codename"` string or an iterable; left at `None`, the view raises `ImproperlyConfigured` | | `get_permission_required()` | returns the attribute as a tuple | override to compute the permissions per request | | `has_permission()` | `request.user.has_perms(...)` | override to change the rule itself | | `login_url` | `None`, meaning `settings.LOGIN_URL` | where anonymous users are sent | | `redirect_field_name` | `"next"` | query parameter carrying the return path | | `raise_exception` | `False` | send anonymous users a 403 instead of a redirect | | `permission_denied_message` | `""` | message passed to `PermissionDenied` | | `handle_no_permission()` | 403 or redirect | override to change the denial response | Everything below `has_permission()` in that table comes from `AccessMixin`, the base class shared by `LoginRequiredMixin`, `PermissionRequiredMixin` and `UserPassesTestMixin`. ## What a denied user gets `AccessMixin.handle_no_permission()` raises `PermissionDenied` when `raise_exception` is true **or** the user is authenticated; only an anonymous user with `raise_exception = False` is redirected. | Requesting user | `raise_exception = False` | `raise_exception = True` | |---|---|---| | Anonymous | 302 to the login URL with `next` | 403 | | Logged in, lacks the permission | 403 | 403 | The 403 comes from Django's handling of `PermissionDenied`; the default handler renders a `403.html` template when one exists and passes the exception message to it as `exception`. ## Mixin versus decorator The function decorator `permission_required` is built on `user_passes_test`, which only knows how to redirect. By default it therefore sends a **logged-in** user who lacks the permission to the login page, where they are already logged in. The mixin avoids that by treating authenticated users as "you are known, you are not allowed" and answering with a 403. In an interview this difference is the detail that separates having read the source from having copied an example. ## Customising safely - **Any-of rules.** Override `has_permission()` and return `any(user.has_perm(p) for p in self.get_permission_required())`. - **Extra conditions.** Combine the permission with a flag, for example `super().has_permission() and self.request.user.is_active`; keep the rule in `has_permission()` so the denial path stays uniform. - **Friendlier denials.** Override `handle_no_permission()` to add a message and redirect a logged-in user back to the payment page, and call `super()` for everyone else so anonymous users still reach the login page. - **Dynamic permissions.** Override `get_permission_required()` when the permission depends on the request, such as a different codename per HTTP method; it must return an iterable. ## What it does not do - It never looks at the object. `has_permission()` calls `has_perms()` without an `obj`, before `get_object()` runs, so "may refund payments" is checked, not "may refund this payment". Object-level rules need a different mechanism. - It is one check per view. Stacking several access mixins is possible, but they share one `handle_no_permission()` and one set of `AccessMixin` attributes. - It does nothing for templates; hiding the Refund button is a separate `{{ perms }}` check. ## Combining it with LoginRequiredMixin With default attributes, `PermissionRequiredMixin` alone already behaves like the recommended decorator stack: anonymous visitors are redirected to log in, logged-in users without the permission get a 403. Adding `LoginRequiredMixin` in front changes nothing for those two cases. Be careful with `raise_exception = True` on a class that uses both: the attribute is shared through `AccessMixin`, so it makes **both** mixins answer 403, and anonymous visitors lose the login redirect too. If you need a different response per mixin, override `handle_no_permission()` rather than flipping the shared flag.

  • Why does PermissionRequiredMixin give a logged-in user a 403 while the permission_required decorator redirects them?
    The mixin's `handle_no_permission()` comes from `AccessMixin`, which raises `PermissionDenied` whenever `raise_exception` is true or the user is authenticated. The decorator is built on `user_passes_test`, whose only failure action is a redirect; it raises `PermissionDenied` only with `raise_exception=True`. So a logged-in user without the permission gets a 403 from the mixin but a login redirect from the default decorator.
  • Does PermissionRequiredMixin check the permission against the Payment that a DetailView loads?
    No. `has_permission()` calls `has_perms()` with no object, in `dispatch()`, before `get_object()` has run, and the default `ModelBackend` returns no permissions when an object is passed anyway. It answers whether the user may refund payments in general; restricting which payments they may touch needs an object-level check or a queryset scoped to the user.

saying these in an interview costs you the question

  • PermissionRequiredMixin sends logged-in users without the permission to the login page.
  • You must set raise_exception = True to get a 403 for logged-in users.
  • permission_required on the mixin must be a list; a single string fails.
  • The mixin checks the permission against the object DetailView loads.
  • Overriding test_func() changes what PermissionRequiredMixin checks.