skip to content

Access Rights & Groups

contrib.auth authorization: per-model permission codenames, groups as roles, object-level backend hooks and the decorators and mixins that enforce them. Interviewers probe where checks really run.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

17

In Django, how do you restrict a function-based view to users holding a given permission, and what happens when they lack it?

level: juniorimportance: must knowfreq 62%

answer

  1. a decorator from contrib.auth
  2. app label, dot, codename
  3. default failure is not a 403
  4. one argument flips it to PermissionDenied

basics

~10 s

Decorate 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 s

Use `@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 lines
python
from 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.
open as a page

In Django's contrib.auth, which permissions does every model get automatically, and how do you check one with has_perm()?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Django creates add, change, delete and view permissions for every model when migrate runs. You check one with user.has_perm('app_label.codename'), for example has_perm('newsroom.change_article'), where the codename is the action plus the lowercase model name.

open as a page

In Django, how does a user get permissions through a Group, and how do group grants combine with user.user_permissions?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A Group holds a set of permissions, and every user in user.groups inherits them. ModelBackend takes the union of the group permissions and the direct user.user_permissions, so a permission from either source grants access and nothing can subtract one.

open as a page

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%

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.

open as a page

In a Django property-listings app, how do you make sure owners can edit only their own listings?

level: middleimportance: must knowfreq 60%

basics

~20 s

Fetch the listing through a queryset limited to the user, such as get_object_or_404(Listing, pk=pk, owner=request.user), so another owner's listing returns 404. Apply the same scoping to list, update and delete views, and never rely on change_listing alone.

open as a page

In a Django template, how do you show a Refund button only to finance staff, and why is hiding it not enough?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Wrap the button in {% if perms.payments.refund_payment %}; the perms variable from the auth context processor calls user.has_perm(). Hiding is cosmetic: anyone can still POST to the refund URL, so the view must enforce the same permission.

open as a page

In Django, if a user holds 'listings.change_listing', may they edit every listing, and what does has_perm() return with obj=listing?

level: juniorimportance: should knowfreq 44%

basics

~10 s

Yes: model permissions are global, so change_listing covers every listing. has_perm('listings.change_listing', listing) returns False with only ModelBackend, because it grants nothing when an object is passed; only an active superuser passes.

open as a page

In Django, when do you reach for user_passes_test or UserPassesTestMixin instead of permission_required, and what are their pitfalls?

level: middleimportance: should knowfreq 38%

basics

~20 s

Use them when access depends on a rule rather than one stored permission, such as an email domain or a profile flag. Pitfalls: the test also runs for AnonymousUser, the decorator can only redirect on failure, and UserPassesTestMixin cannot be stacked.

open as a page

In a Django newsroom app, how do you add a custom 'can publish' permission to an Article model, and what does declaring it enforce?

level: middleimportance: should knowfreq 48%

basics

~10 s

Declare Meta.permissions = [('publish_article', 'Can publish article')] on Article and run migrate, which creates the Permission row. The declaration enforces nothing: views must still check has_perm('newsroom.publish_article') before publishing.

open as a page

In Django, which permission-string mistakes make user.has_perm() silently return False, and how does Permission.user_perm_str in 6.1 help?

level: middleimportance: should knowfreq 30%

basics

~20 s

has_perm() only tests whether an 'app_label.codename' string is in the user's set, so a missing app label, a module path, a capitalised model name or a typo just returns False. Django 6.1's Permission.user_perm_str builds the correct string from a Permission row.

open as a page

When a custom Django user model extends AbstractBaseUser, what does adding PermissionsMixin provide, and what breaks without it?

level: middleimportance: should knowfreq 38%

basics

~20 s

PermissionsMixin adds is_superuser, the groups and user_permissions many-to-many fields, and has_perm(), has_perms(), has_module_perms() and the get_*_permissions() methods. Without it a user built on AbstractBaseUser has no permission API, and the admin and ModelBackend cannot authorize them.

open as a page

In Django, how do is_superuser and is_active change what has_perm() and get_all_permissions() return for a user?

level: middleimportance: should knowfreq 46%

basics

~20 s

An active superuser passes every has_perm() check without holding any grants, and ModelBackend reports every Permission row for them. An inactive user gets an empty set and False from ModelBackend, even if they are a superuser or belong to groups.

open as a page

A Django app hides its Refund button with {{ perms }} and guards the refund view, yet support agents still issue refunds; how do you find the gap?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Trace the request that actually performs the refund, not the page that shows the button. Usually another URL reaches the same code unguarded, a class-based view guards only get(), or the check is looser than the permission.

open as a page

In Django, a view grants an editor 'newsroom.publish_article' and then calls has_perm() on the same user object, which returns False — why, and how do you fix it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

ModelBackend caches a user's permission set on the user instance at the first check, and adding a grant does not invalidate it. Re-fetch the user with User.objects.get(pk=...) before checking again; refresh_from_db() does not clear the cache.

open as a page

How would you write a Django authorization backend so has_perm('listings.change_listing', listing) is True only for that listing's owner?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Subclass BaseBackend, override has_perm(user_obj, perm, obj) to return True when obj is a Listing whose owner_id equals the active user's pk, and add it to AUTHENTICATION_BACKENDS after ModelBackend. Callers must pass the listing to has_perm().

open as a page

In a Django clinic app, how do you seed Nurse, Doctor and Admin groups with permissions in every environment, and why does a data migration often fail on a fresh database?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Declare the roles in code and write them with an idempotent post_migrate receiver, a migration that creates its own rows, or a deploy command. A plain data migration fails on a fresh database because default permissions are created by post_migrate, after every migration has run.

open as a page

What does Django's User.objects.with_perm() return, and why does it return no users when you pass obj=listing?

level: middleimportance: nice to knowfreq 18%

basics

~10 s

User.objects.with_perm('listings.approve_listing') returns a queryset of active users holding the permission directly or through a group, plus superusers by default. With obj set, ModelBackend returns an empty queryset because core has no object permissions.

open as a page