In Django REST Framework, how do has_permission and has_object_permission differ, and when does DRF call each of them?
answer
- two hooks on BasePermission
- one runs before the handler, always
- the other needs a loaded instance
- get_object() calls check_object_permissions()
basics
~10 shas_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().
solid answer
~40 sBoth are methods of `BasePermission` and both return `True` unless overridden. `has_permission(request, view)` is the view-level gate: `APIView.initial()` calls it on every listed class for every request, after authentication and before the handler, so it can only look at the user, the method, the view and, on a viewset, `view.action`. `has_object_permission(request, view, obj)` is the instance-level gate and runs only when the view calls `check_object_permissions(request, obj)`. `GenericAPIView.get_object()` does that after fetching the instance, so on a `ModelViewSet` it covers `retrieve`, `update`, `partial_update` and `destroy`, but not `list` or `create`. A custom `APIView` or function view that loads the object itself must call the check explicitly. For "only the owner may edit", the owner test belongs in `has_object_permission`.
code
python · 14 linesfrom rest_framework import permissions, viewsets
from .models import Document
from .permissions import IsOwnerOrReadOnly
from .serializers import DocumentSerializer
class DocumentViewSet(viewsets.ModelViewSet):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
permission_classes = [permissions.IsAuthenticated, IsOwnerOrReadOnly]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)go deeper
Remember that has_permission gates the whole view and has_object_permission gates a single instance, and that both default to True in BasePermission.
Walk through initial() and get_object(): which actions reach the object hook, why list and create do not, and why a plain APIView must call check_object_permissions() itself.
Point out the paths that silently skip object checks — overridden get_object(), detail actions that query directly, function views — and how you would test for them.
Weigh object-level checks against queryset scoping as the primary control, and decide which one a team standardises on so reviewers know where to look.
## Two hooks on one class Every Django REST Framework (DRF) permission class inherits from `BasePermission`, which defines two methods. Both return `True` by default, so a subclass that overrides one of them grants access on the other: - `has_permission(self, request, view)` — the **view-level** question: may this request reach this view at all? - `has_object_permission(self, request, view, obj)` — the **object-level** question: may this request act on this particular model instance? The names look symmetrical, but DRF calls them from completely different places. ## When DRF calls `has_permission` For every request, `APIView.initial()` runs: 1. `perform_authentication()`, which resolves `request.user`; 2. `check_permissions()`, which instantiates each class from `get_permissions()` and calls `has_permission()`; 3. `check_throttles()`. This runs for **every method and every action** — list, create, retrieve, update, destroy — before the handler executes. No object has been loaded yet, so the method can only use what the request and view carry: `request.user`, `request.method`, URL kwargs, and on a viewset `view.action`, which `initialize_request()` sets before `initial()` runs. ## When DRF calls `has_object_permission` Only when something calls `view.check_object_permissions(request, obj)`. In the generic views that something is `GenericAPIView.get_object()`, which: 1. takes `self.filter_queryset(self.get_queryset())`; 2. fetches the instance with `get_object_or_404()` using `lookup_field` and the URL kwarg; 3. calls `self.check_object_permissions(self.request, obj)` and returns the object. So on a `ModelViewSet` the object hook covers `retrieve`, `update`, `partial_update`, `destroy` and any custom detail action that calls `get_object()`. It does **not** run for `list` (no single object is fetched) or `create` (the object does not exist yet). Because `check_permissions()` has already run every class's view-level hook, the object hook is reached only after the view-level checks passed. Outside the generic views nothing is automatic. An `APIView` that does its own `Document.objects.get(...)`, an overridden `get_object()` that forgets the call, and an `@api_view` function all skip object permissions unless the code calls `self.check_object_permissions(request, obj)` itself. | | `has_permission` | `has_object_permission` | |---|---|---| | Arguments | `request, view` | `request, view, obj` | | Called from | `check_permissions()` in `initial()` | `check_object_permissions()`, normally in `get_object()` | | Runs on `list` and `create` | yes | no | | Runs on `retrieve`/`update`/`destroy` | yes | yes, when `get_object()` is used | | `BasePermission` default | `True` | `True` | ## The owner-only document The common requirement "anyone signed in may read a document, only its owner may change it" splits naturally across the two hooks. `IsAuthenticated` supplies the view-level gate; a custom class supplies the object-level one and lets reads through with `SAFE_METHODS` (`GET`, `HEAD`, `OPTIONS`): ```python from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): message = "Only the document's owner may change it." def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True return obj.owner == request.user ``` Listed as `permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]`, anonymous requests fail at the view level, while `PATCH` or `DELETE` on someone else's document fails at the object level with a 403 carrying the custom `message`. ## What a failure looks like - Either hook returning `False` sends the view to `permission_denied()`. - If no authenticator accepted credentials, that raises `NotAuthenticated`; otherwise `PermissionDenied` (403). - A class can set `message` and `code` attributes to customise the error body. - Returning `False` never produces a 404 by itself; a class that wants to hide existence must raise `Http404` (as `DjangoObjectPermissions` does) or the view must filter its queryset.
- In a DRF viewset, can has_permission apply a stricter rule to destroy than to list?Yes. On viewsets `initialize_request()` sets `view.action` before `initial()` runs, so `has_permission()` can branch on `view.action == "destroy"`. It still has no object, so a rule that depends on the instance's owner has to live in `has_object_permission()`.
- In DRF, when an object-level check fails on PATCH, does the user get 403 or 404?A custom `has_object_permission()` returning `False` leads to `permission_denied()`, so an authenticated user gets 403 with the class's `message`. A 404 happens only if the object was never found — for example because `get_queryset()` filtered it out — or if the class raises `Http404` itself, as `DjangoObjectPermissions` does for users who cannot even read the object.
saying these in an interview costs you the question
- has_object_permission is called for every object in a list response.
- has_object_permission runs even after has_permission has already denied the request.
- DRF calls has_object_permission automatically in any APIView that loads a model.
- A class that overrides only has_object_permission denies everything at view level.
- Returning False from a custom has_object_permission makes DRF answer 404.