In Django, how do you restrict a function-based view to users holding a given permission, and what happens when they lack it?
answer
- a decorator from contrib.auth
- app label, dot, codename
- default failure is not a 403
- one argument flips it to PermissionDenied
basics
~10 sDecorate the view with permission_required("app_label.codename") from django.contrib.auth.decorators. Any user lacking it, logged in or not, is redirected to LOGIN_URL with a next parameter; raise_exception=True raises PermissionDenied and yields a 403 instead.
solid answer
~40 sUse `@permission_required("payments.refund_payment")` from `django.contrib.auth.decorators`. Before the view body runs it calls `request.user.has_perms(...)`; the argument may be one string or an iterable, and an iterable means **all** of them are required. By default a failing user, anonymous **or logged in**, is redirected to `login_url` or `settings.LOGIN_URL` with `?next=` set, because the decorator is built on `user_passes_test`. With `raise_exception=True` it raises `PermissionDenied`, which Django turns into a 403. The usual stack is `@login_required` above `@permission_required(..., raise_exception=True)`: anonymous visitors get the login page, logged-in users without the right get a 403. Active superusers pass every check, inactive users fail every check, and since Django 5.1 the decorator also wraps `async def` views.
code
python · 14 linesfrom django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST
from payments.models import Payment
@require_POST
@login_required
@permission_required("payments.refund_payment", raise_exception=True)
def refund_payment(request, pk):
payment = get_object_or_404(Payment, pk=pk)
payment.refund(issued_by=request.user)
return redirect("payments:detail", pk=pk)go deeper
Recall the import, the app_label.codename string, and that by default a failing user is redirected to LOGIN_URL rather than shown a 403.
Explain that the decorator is built on user_passes_test, why that sends logged-in users back to the login page, and how login_required plus raise_exception=True separates the two cases.
Show where the decorator falls short: views reachable by more than one route, class-based views that need method_decorator on dispatch, and superusers passing any string so typos survive manual testing.
Argue for one project-wide enforcement style and one denial policy, 403 versus redirect, so a reviewer can spot an unguarded view at a glance.
## What the decorator is `permission_required` lives in `django.contrib.auth.decorators` and wraps a **function-based view**. Its signature is `permission_required(perm, login_url=None, raise_exception=False)`. Before the view body runs, it asks the current user whether they hold the permission, and only if the answer is yes does the view execute. Internally it is a thin layer over another decorator, `user_passes_test`. It builds a small check function that calls `user.has_perms(perms)` and hands that function to `user_passes_test`. That detail explains its default failure behaviour, covered below. ## The permission string - A permission is named `"<app_label>.<codename>"`: for a custom permission `refund_payment` declared on a model in the `payments` app, that is `"payments.refund_payment"`; the default ones look like `"payments.change_payment"`. - The argument may be **one string** or an **iterable of strings**. With an iterable the user must hold **all** of them, because the check is `has_perms()`, which is an all-of test. - A misspelled string does not raise anything. `has_perm()` simply finds no such permission and returns `False`, so the view is closed to everyone, with one exception: an **active superuser** gets `True` from `has_perm()` for any string, so a developer testing as a superuser never notices the typo. - **Inactive** users and `AnonymousUser` get `False` from the default `ModelBackend`, whatever permissions are stored for them. ## What a denied user gets The decorator follows three steps: 1. If `has_perms()` is true, the view runs. 2. Otherwise, if `raise_exception=True`, it raises `django.core.exceptions.PermissionDenied`; Django's exception handling converts that into a **403 Forbidden** response (rendered by the 403 handler, which uses a `403.html` template if you provide one). 3. Otherwise it returns `False` to `user_passes_test`, which **redirects** to `login_url` (default `settings.LOGIN_URL`, whose own default is `"/accounts/login/"`) with the current path in the `next` query parameter. | Requesting user | `raise_exception=False` (default) | `raise_exception=True` | |---|---|---| | Anonymous | 302 to the login URL with `next` | 403 | | Logged in, lacks the permission | 302 to the login URL with `next` | 403 | | Holds the permission, or active superuser | view runs | view runs | The middle row surprises people: a logged-in user without the permission is sent to the **login page**, where they are already logged in. If `LoginView` is configured with `redirect_authenticated_user=True`, it sends them straight back to `next`, which redirects them to login again: a redirect loop. ## The idiomatic stack The Django documentation recommends combining two decorators so each kind of user gets the right answer: - `@login_required` on the **outside** sends anonymous visitors to log in. - `@permission_required(..., raise_exception=True)` on the **inside** gives logged-in users without the permission a 403. Decorators apply bottom-up and run top-down, so the outermost one checks first. Adding `@require_POST` above both makes a state-changing view such as a refund refuse `GET` with a 405 before any auth work happens. ## Class-based views and URLconfs The decorator is written for view functions, but it can still guard a class-based view: - wrap the result of `as_view()` in the URLconf, for example `path("refunds/<int:pk>/", permission_required("payments.refund_payment")(RefundView.as_view()))`; - or decorate the class with `method_decorator(permission_required(...), name="dispatch")`, so every HTTP method passes through the check. The class-based counterpart is `PermissionRequiredMixin`, which reads more naturally on a class and differs in one important way: it answers logged-in users with a 403 even without `raise_exception`. ## Async views Since Django 5.1, `login_required`, `permission_required` and `user_passes_test` detect an `async def` view. For such views the check awaits `request.auser()` and `user.ahas_perms()`, so you can decorate an async view directly instead of checking permissions by hand inside it. ## Common mistakes - **Expecting a 403 by default.** Without `raise_exception=True`, a logged-in user who lacks the permission is bounced to the login page, which looks like a broken session rather than a refusal. - **Writing only the codename.** `"refund_payment"` without the app label never matches, so the view is closed to everyone except active superusers. - **Guarding the wrong view.** Decorating the page that shows a refund form does nothing for the separate view the form posts to; the view that performs the change needs the check. - **Assuming a list means any-of.** An iterable is all-of; an any-of rule needs a custom test. - **Checking in the view body instead.** A hand-written `if not request.user.has_perm(...)` works, but it is easy to forget on the next view and invisible to anyone scanning the decorators.
- Why is @login_required usually stacked above @permission_required(..., raise_exception=True)?`raise_exception` applies to everyone who fails the check, so without `login_required` an anonymous visitor gets a bare 403 instead of a chance to log in. With `login_required` outermost, anonymous users are redirected to `LOGIN_URL` first and only logged-in users lacking the permission reach the 403. It also avoids the redirect loop that appears when `LoginView` has `redirect_authenticated_user=True`.
- How would you require any one of several permissions instead of all of them?`permission_required` always demands all of them, because it calls `has_perms()`. For an any-of rule use `user_passes_test` with a callable such as `lambda u: any(u.has_perm(p) for p in perms)`, or on a class-based view override `PermissionRequiredMixin.has_permission()`. Often the cleaner fix is one dedicated permission granted to every group that needs the access.
- How do you put permission_required on a class-based view?Either wrap the view in the URLconf, `permission_required("payments.refund_payment")(RefundView.as_view())`, or decorate the class with `method_decorator(permission_required(...), name="dispatch")` so every HTTP method is covered. Most code uses `PermissionRequiredMixin` instead, which also returns 403 to logged-in users by default.
saying these in an interview costs you the question
- permission_required returns a 403 by default when a logged-in user lacks the permission.
- The permission string is just the codename, like refund_payment, without the app label.
- Passing a list of permissions lets in users who hold any one of them.
- raise_exception=True only changes what anonymous users receive.
- permission_required cannot decorate async views, so they must call has_perm by hand.