skip to content

Permission Classes

Permission classes such as IsAuthenticated and DjangoModelPermissions gate each view, while has_object_permission runs only through get_object(). Interviewers probe the AllowAny default and leaks.

part ofDjango REST Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django REST Framework, which permission applies to a view when none is configured, and how do you require authentication across the whole API?

level: juniorimportance: must knowfreq 68%

answer

  1. the REST_FRAMEWORK settings dictionary
  2. what DRF assumes when you say nothing
  3. DEFAULT_PERMISSION_CLASSES and IsAuthenticated
  4. a view's own list replaces the default

basics

~10 s

DRF's default permission is AllowAny, so an unconfigured view accepts anonymous requests. Set DEFAULT_PERMISSION_CLASSES to IsAuthenticated in REST_FRAMEWORK; a view's own permission_classes then replaces that list rather than adding to it.

solid answer

~40 s

Out of the box `REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES']` is `['rest_framework.permissions.AllowAny']`, so a view with no policy is open to anyone, anonymous clients included. To lock the API down, set that default to `IsAuthenticated` in `settings.py`; every `APIView`, generic view, viewset and `@api_view` function inherits it. A view that declares `permission_classes` (or a function view with the `@permission_classes` decorator) *replaces* the default list, so one convenient `[IsAuthenticatedOrReadOnly]` reopens anonymous reads on that view, and `permission_classes = []` removes the permission check entirely. `IsAuthenticated` only reads `request.user.is_authenticated`, which the authentication classes resolved just before it; when it fails for a request with no accepted credentials, DRF raises `NotAuthenticated`.

code

python · 9 lines
python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response


@api_view(["GET"])
@permission_classes([AllowAny])
def health(request):
    return Response({"status": "ok"})

go deeper

for a junior

Recall that DRF's default permission is AllowAny and that putting IsAuthenticated in DEFAULT_PERMISSION_CLASSES is the standard first fix.

for a middle

Explain that a view's permission_classes replaces the global list, that every listed class must pass, and that the check runs in initial() after authentication and before throttling.

for a senior

Show how the open default leaks in real code — empty lists, a convenience IsAuthenticatedOrReadOnly, per-action get_permissions branches — and how a URL-walking test catches them.

for a principal

Argue for secure-by-default settings with an explicit public allow-list, enforced by review and CI rather than by trusting every view author to remember.

## The shipped default is `AllowAny` Django REST Framework (DRF) reads its API policy from one dictionary in `settings.py` called `REST_FRAMEWORK`. One of its keys, `DEFAULT_PERMISSION_CLASSES`, lists the **permission classes** every view runs before its handler. When a project never sets that key, DRF falls back to its own default in `rest_framework/settings.py`: `['rest_framework.permissions.AllowAny']`. `AllowAny.has_permission()` simply returns `True`. The consequence is easy to miss: **a newly written view is public** — anonymous clients can read, create, update and delete through it — until someone adds a policy. Authentication classes still run and still populate `request.user`, but nothing *requires* the user to be anyone in particular. ## Requiring authentication everywhere The usual first hardening step is to flip the default: ```python # settings.py REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], } ``` `IsAuthenticated.has_permission()` returns `bool(request.user and request.user.is_authenticated)`. It does **not** log anyone in; it only reads the result of the authentication classes that ran just before it. Every class that inherits from `APIView` — generic views and viewsets included — picks this setting up, because `APIView.permission_classes` is initialised from it. A function view wrapped with `@api_view` gets it too: the decorator copies the function's own `permission_classes` attribute if one was set, otherwise the `APIView` default. ## A view's own list replaces the default Per-view policy is set with the `permission_classes` attribute on a class-based view, the `@permission_classes` decorator on a function view, or an overridden `get_permissions()`. The key rule: **the view's list replaces the setting; it is never merged with it**. | View declares | Effective policy | |---|---| | nothing | `DEFAULT_PERMISSION_CLASSES` from settings | | `permission_classes = [AllowAny]` | open to everyone, and says so explicitly | | `permission_classes = []` | open to everyone — an empty loop denies nothing | | `permission_classes = [IsAuthenticatedOrReadOnly]` | anonymous `GET`/`HEAD`/`OPTIONS` allowed, even under a global `IsAuthenticated` | | `permission_classes = [IsAuthenticated, IsOwner]` | every listed class must pass | When several classes are listed, **all of them must grant access**; the first one that returns `False` stops the request. ## Where the check runs On every request, `APIView.initial()` runs three policy steps in a fixed order before dispatching to the handler: 1. `perform_authentication()` — resolves `request.user` and `request.auth`. 2. `check_permissions()` — calls `has_permission(request, view)` on each instance returned by `get_permissions()`. 3. `check_throttles()` — applies rate limits. If a class says no, `permission_denied()` decides what to raise. When the view has authenticators and none of them accepted the request's credentials, it raises `NotAuthenticated`; otherwise it raises `PermissionDenied`, a 403 whose default detail is "You do not have permission to perform this action." and which picks up the failing class's `message` and `code` attributes when they exist. `NotAuthenticated` is sent as 401 only when the first authentication class supplies a `WWW-Authenticate` header; otherwise DRF coerces it to 403. ## How the open default leaks in practice - A project never sets `DEFAULT_PERMISSION_CLASSES`, and an internal endpoint ships world-writable. - A developer adds `permission_classes = [IsAuthenticatedOrReadOnly]` to one viewset for convenience and thereby reopens reads that the global `IsAuthenticated` was meant to block. - Someone writes `permission_classes = []` believing it means "inherit the default". - A custom `get_permissions()` returns different lists per `self.action`, and one branch returns an empty list or forgets a newly added action. - A mixin or base viewset sets `permission_classes`, and every subclass silently inherits that list instead of the project default. A cheap safeguard is a test that walks the project's URL patterns, finds every DRF view, and asserts that its permission list is not just `AllowAny` unless the view is on an explicit allow-list of public endpoints. ## Why DRF chose an open default The permissive default keeps the tutorial and a first `curl` working without configuration, and DRF cannot know which endpoints a project intends to be public. The price is that **access control is opt-in**. A production project should set the default to `IsAuthenticated` (or stricter) on day one and mark each public view with an explicit `AllowAny`, which documents intent far better than an empty list and is easy to grep for in review.

  • Why can a DRF view guarded by IsAuthenticated answer an anonymous client with 403 instead of 401?
    When the permission fails and no authenticator accepted credentials, DRF raises `NotAuthenticated`. `handle_exception()` then asks the first authentication class for a `WWW-Authenticate` header. `BasicAuthentication` and `TokenAuthentication` supply one, so the client gets 401; `SessionAuthentication` supplies none, so DRF coerces the status to 403.
  • With IsAuthenticated as the global default, how do you make one endpoint public, and why not with an empty list?
    Set `permission_classes = [AllowAny]` on that view (or `@permission_classes([AllowAny])` on a function view). An empty list behaves the same at runtime, but `AllowAny` states the intent, survives a reviewer's grep for public endpoints and cannot be mistaken for "inherit the default".
  • Does a function view decorated with @api_view get DEFAULT_PERMISSION_CLASSES?
    Yes. `@api_view` builds an `APIView` subclass and copies the function's `permission_classes` attribute if the `@permission_classes` decorator set one; otherwise it uses `APIView.permission_classes`, which comes from the setting. So function views are covered by the global default exactly like class-based ones.

saying these in an interview costs you the question

  • DRF requires authentication by default, so new endpoints start private.
  • A view's permission_classes is added to the global default list.
  • An empty permission_classes list denies every request.
  • IsAuthenticated logs the user in, so no authentication classes are needed.
  • The default permission class is IsAuthenticatedOrReadOnly.
open as a page

In Django REST Framework, how do has_permission and has_object_permission differ, and when does DRF call each of them?

level: middleimportance: must knowfreq 62%

basics

~10 s

has_permission runs on every request before the handler and sees only the request and view; has_object_permission sees a model instance and runs only when check_object_permissions() is called, which generic views do inside get_object().

open as a page

In Django REST Framework, what exactly do IsAdminUser, IsAuthenticatedOrReadOnly and DjangoModelPermissions check before letting a request through?

level: middleimportance: should knowfreq 44%

basics

~10 s

IsAdminUser requires user.is_staff; IsAuthenticatedOrReadOnly lets anyone use GET, HEAD and OPTIONS but requires login for writes; DjangoModelPermissions requires login plus the add, change or delete model permission matching the HTTP method.

open as a page

A DRF ModelViewSet guards documents with an owner-only has_object_permission, yet its list endpoint returns every user's documents — why, and how do you close the gap?

level: seniorimportance: should knowfreq 48%

basics

~10 s

DRF applies object permissions only through get_object(), which list and create never call. Scope get_queryset() to request.user so lists and lookups see only owned rows, and set the owner server-side in perform_create().

open as a page

In Django REST Framework, how do the &, | and ~ operators compose permission classes, and how can | quietly weaken an owner-only object check?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

DRF's &, | and ~ combine permission classes into one, applying the logic to both hooks. | weakens object checks when an operand inherits BasePermission's has_object_permission, which returns True: IsAuthenticated | IsOwner lets any signed-in user edit.

open as a page