In Django REST Framework, which permission applies to a view when none is configured, and how do you require authentication across the whole API?
answer
- the REST_FRAMEWORK settings dictionary
- what DRF assumes when you say nothing
- DEFAULT_PERMISSION_CLASSES and IsAuthenticated
- a view's own list replaces the default
basics
~10 sDRF'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 sOut 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 linesfrom 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
Recall that DRF's default permission is AllowAny and that putting IsAuthenticated in DEFAULT_PERMISSION_CLASSES is the standard first fix.
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.
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.
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.