2025, Dec 17 21:00
How to combine Django REST Framework permission classes with OR for admin, authenticated, and anonymous users
Learn to implement granular Django REST Framework permissions by OR-composing classes: admins full CRUD, authenticated users read + delete, anonymous read-only.
Granular API permissions in Django REST framework often seem straightforward until you mix different user types and HTTP methods. A common need is to give admins full CRUD, allow authenticated users to read and delete, and let anonymous users read only. The tricky part is how DRF evaluates multiple permission classes: if you simply list them, they will be combined as an implicit AND, which blocks legitimate access for non-admins. The right approach is to compose permissions with an OR.
Problem setup
Consider a ModelViewSet registered through DefaultRouter. The goal is simple: admins (is_staff) can do everything, authenticated users can read and delete, and anonymous users can read. The first instinct is to stack multiple permission classes in the permission_classes list, expecting DRF to treat them as alternatives.
from rest_framework import permissions
class AuthUserCanViewAndDelete(permissions.BasePermission):
def has_permission(self, request, view):
allowed = ("GET", "HEAD", "OPTIONS", "DELETE")
return bool(
request.method in allowed and
request.user and
request.user.is_authenticated
)
class ReadOnlyOnly(permissions.BasePermission):
def has_permission(self, request, view):
return bool(request.method in permissions.SAFE_METHODS)
class FeatureApi(viewsets.ModelViewSet):
# ...
permission_classes = [
permissions.IsAdminUser,
AuthUserCanViewAndDelete,
ReadOnlyOnly,
]
# ...
What actually goes wrong
DRF treats a list of permission classes as an AND operation. That means the request must simultaneously satisfy IsAdminUser, AuthUserCanViewAndDelete, and ReadOnlyOnly. For example, a DELETE request from an authenticated non-admin fails because ReadOnlyOnly rejects non-safe methods. A GET request from an anonymous user fails because AuthUserCanViewAndDelete requires authentication. The net effect is that only admins will pass all checks for most operations, which contradicts the intended policy.
Fix: compose permissions with OR
The solution is to combine permission classes using OR so that passing any of them grants access. Some of what you need is already supported out of the box (IsAdminUser), and the rest can be expressed via simple custom permissions. The composition below follows this idea directly.
from rest_framework import permissions
class AuthOnlyReadAndDelete(permissions.BasePermission):
"""
Authenticated users can GET, HEAD, OPTIONS, and DELETE.
"""
def has_permission(self, request, view):
user_ops = ("GET", "HEAD", "OPTIONS", "DELETE")
return bool(
request.method in user_ops and
request.user and
request.user.is_authenticated
)
class JustRead(permissions.BasePermission):
"""
Anonymous or any user can perform read-only requests.
"""
def has_permission(self, request, view):
return bool(request.method in permissions.SAFE_METHODS)
class FeatureEndpoint(viewsets.ModelViewSet):
# ...
permission_classes = [
permissions.IsAdminUser | AuthOnlyReadAndDelete | JustRead
]
# ...
This composition captures the policy succinctly. If the requester is an admin, all operations are allowed. If the requester is authenticated, only safe methods plus DELETE are permitted. If the requester is anonymous, only safe methods are permitted.
Why this matters
Relying on the default list behavior can silently over-restrict your API or produce confusing access patterns. When policies differ by user type and HTTP method, it is essential to express them as alternatives, not cumulative constraints. Using OR makes the intent explicit, keeps viewsets readable, and aligns the permission checks with the actual access model.
Practical takeaways
When you need different access tiers within one endpoint, model them as discrete permission classes and combine them with OR. Keep read-only semantics in a simple rule so anonymous access remains clear, and scope additional allowances for authenticated users narrowly to the needed methods. With admins covered by IsAdminUser, this structure remains compact and easy to audit.
The end result is predictable behavior: admins manage everything, authenticated users can read and delete, anonymous users can read—exactly as intended.