Interaction between global permission classes and object-level BasePermission logic
0 reputation · 06 Jan 2021, 13:11 UTC
0 reputation · 06 Jan 2021, 13:11 UTC
In Django Rest Framework, the security lifecycle executes check_permission on the view before invoking has_object_permission. While setting a global DEFAULT_PERMISSION_CLASSES like IsAuthenticated provides a baseline, complex scenarios arise when custom permission classes are implemented to handle object-level ownership.
Specifically, if a custom permission class relies on the state of a model instance to determine access, there is a risk that the global view-level check may pass or fail before the object-specific logic is even evaluated. This becomes particularly problematic when using global filterbackends that modify querysets, as the permission logic might not align with the filtered data set if not strictly atomic.
How does the execution order between global default permissions and custom object-level permission checks affect security when a view depends on dynamic data? Is there a reliable way to ensure that object-level restrictions are not bypassed if the view-level check returns a positive result prematurely?
34775 reputation · 06 Jan 2021, 14:20 UTC
Global permission classes (from permission_classes on the view or settings.DEFAULT_PERMISSION_CLASSES) are always evaluated first via their has_permission method. If any of those returns false or raises PermissionDenied, the request is rejected and no object‑level check is performed.
has_permission calls before retrieving an object.has_object_permission, DRF falls back to calling its has_permission again for object‑level decisions.After the global checks succeed, DRF proceeds to the view’s get_object (or similar) to fetch a single instance. For each permission class that implements has_object_permission, DRF then calls that method with the retrieved object, the view, and the request. List views do not trigger has_object_permission automatically unless you manually iterate over the queryset and call get_object on each item.
has_object_permission. If it does not, add it; otherwise DRF will never invoke object‑specific logic.has_permission and has_object_permission to confirm the call order during a test request.get_object (e.g., RetrieveAPIView, UpdateAPIView, DestroyAPIView) for the actions where object‑level checks are required. If you rely on a custom filter_queryset or manually built queryset, you must either call get_object yourself or enforce the permission in the view logic.filter_queryset to apply the same object‑level checks before returning the queryset.Missing diagnostic detail that changes the recommendation: Does your custom permission class implement has_object_permission? If the answer is “no”, you must add that method; if “yes”, the steps above are sufficient.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 06 Jan 2021, 22:32 UTC
A practical detail often missed: filter backends run in filter_queryset() before get_object(). If a backend (or a custom get_queryset() override) removes an instance the user owns, get_object() raises Http404 and has_object_permission is never invoked for that instance. The request fails with 404 instead of 403, which can leak existence information and bypass audit logic in your permission class.
To keep object-level permissions as the authoritative gate, either:
has_object_permission enforce ownership, orcheck_object_permissions(request, obj) after a manual get_object_or_404 on the unfiltered queryset.This ensures the permission class always evaluates the instance, regardless of upstream filtering.