Object permission checks decide whether the current principal may access a particular retrieved object, separately from whether the principal is authenticated.
Django REST Framework object permissions: check the retrieved record, not only login
Operation contract
The local receipt view first requires an authenticated principal, then explicitly calls check_object_permissions on its retrieved fixture record. The owner receives a success response, another authenticated principal is forbidden, and a missing record returns not found. APIRequestFactory and force_authenticate isolate permission behavior without claiming that a login or token exchange occurred.
Failure and ownership boundary
Force_authenticate bypasses real authentication and must stay in tests. List endpoints also need query filtering rather than assuming every returned record will receive an object check. Creation needs its own owner-assignment rule; a received owner_id must not grant access. The fixture uses no database or public listener. Django REST Framework serializers: reject coercion outside the wire contract and Django CSRF checks: require the token without confusing it with identity solve other request boundaries.
Tested environment
Dependency check: this program was executed on CPython 3.14.6 with Django==5.2.17, djangorestframework==3.18.1. Install these versions in a separate virtual environment. The download includes the recorded environment snapshot; no third-party package is part of the website runtime.
Working program
from types import SimpleNamespace
from django.conf import settings
settings.configure(INSTALLED_APPS=[], SECRET_KEY="owned-local-permission-fixture",
REST_FRAMEWORK={"DEFAULT_AUTHENTICATION_CLASSES": [], "UNAUTHENTICATED_USER": None})
import django
django.setup()
from django.http import Http404
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.test import APIRequestFactory, force_authenticate
RECEIPTS = {41: {"receipt_id": "R-0041", "owner_id": 7}}
class ReceiptOwner(BasePermission):
def has_object_permission(self, request, view, receipt):
return receipt["owner_id"] == request.user.pk
class ReceiptView(APIView):
permission_classes = [IsAuthenticated, ReceiptOwner]
def get(self, request, number):
receipt = RECEIPTS.get(number)
if receipt is None:
raise Http404
self.check_object_permissions(request, receipt)
return Response({"receipt_id": receipt["receipt_id"]})
def local_response(principal_id, number):
request = APIRequestFactory().get(f"/receipts/{number}")
if principal_id is not None:
force_authenticate(request, user=SimpleNamespace(pk=principal_id, is_authenticated=True))
return ReceiptView.as_view()(request, number=number).status_code
print("owner:", local_response(7, 41))
print("other user:", local_response(8, 41))
print("missing record:", local_response(7, 99))
print("anonymous:", local_response(None, 41))Output
owner: 200
other user: 403
missing record: 404
anonymous: 403Costs and limits
This fixture performs one dictionary retrieval and one ownership comparison. A real list/query path needs backend filters, indexes and paging budgets. Response timing and error shape may expose existence information, so a production policy must decide whether forbidden and missing should be distinguishable.
Common Mistakes
- Call object checks after retrieving the object in a custom view.
- Test authentication bypass is not a production identity provider.
Connected lessons
Django REST Framework serializers: reject coercion outside the wire contract, Django CSRF checks: require the token without confusing it with identity, Flask SQLite application: own the request connection and verify a clean reopen.
Check the next state boundary
Django REST Framework list visibility: filter the principal’s rows before paging, Django REST Framework creation ownership: assign the owner from the principal.
