Custom Permissions & Authentication
Go beyond login_required: object-level permissions, custom permission classes, and swapping in your own authentication backend.
Django ships two layers of access control, and knowing which to use where is a senior skill.
The built-in permission system
Every model automatically gets add, change, delete and view permissions. Check them with user.has_perm("blog.change_article"), or declare your own domain permissions in Meta.permissions (e.g. can_publish). These are model-level — they say "this user may edit articles", not "this article".
Object-level checks
"Can this user edit this specific article?" is object-level and Django's default backend does not answer it — you enforce it yourself, typically in get_queryset() (so users only ever see their own rows) or with UserPassesTestMixin. This is the more common real-world need.
Custom authentication backends
Need to log people in by email instead of username, or authenticate against an SSO / API token? Write an authentication backend — a class with authenticate() and get_user() — and add it to AUTHENTICATION_BACKENDS. Django tries each backend in order.
Whichever mechanism you choose, enforce access on the queryset, not just by hiding a button in the template. A hidden link is UX; a filtered queryset is security. If a user cannot see a row, they cannot POST to edit it either.
Example
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import UserPassesTestMixin
from django.views.generic import UpdateView
from .models import Article
User = get_user_model()
# --- Custom model permission, declared on the model's Meta ---
# class Article(models.Model):
# class Meta:
# permissions = [("can_publish", "Can publish articles")]
# ...then check: if request.user.has_perm("blog.can_publish"): ...
# --- Object-level: a user may only edit an article they authored ---
class ArticleEdit(UserPassesTestMixin, UpdateView):
model = Article
fields = ["title", "body"]
def test_func(self):
return self.get_object().author == self.request.user
# Even stronger: never let a foreign object be fetched at all -> 404, not 403.
def get_queryset(self):
return super().get_queryset().filter(author=self.request.user)
# --- Custom authentication backend: log in by email + password ---
class EmailBackend(BaseBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(email__iexact=username)
except User.DoesNotExist:
return None
return user if user.check_password(password) else None
def get_user(self, user_id):
return User.objects.filter(pk=user_id).first()
# settings.py:
# AUTHENTICATION_BACKENDS = [
# "accounts.backends.EmailBackend",
# "django.contrib.auth.backends.ModelBackend",
# ]When to use it
- A developer uses django-guardian to grant per-object permissions so each user only edits their own documents.
- A team creates a custom DRF IsAdminOrOwner permission class to give admins full access and owners limited access.
- A developer checks object-level permission in a DRF ViewSet using get_object() which calls check_object_permissions.
More examples
DRF IsAdminOrReadOnly permission
Allows GET/HEAD/OPTIONS to everyone but restricts data mutation to staff users.
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return request.user and request.user.is_staffObject-level permission in ViewSet
Explicitly calls check_object_permissions after fetching the object to enforce per-object access rules.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
def get_object(self):
obj = super().get_object()
self.check_object_permissions(self.request, obj)
return objGroup-based permission check
Restricts access to users who belong to the 'Editors' group, checked against the auth_group table.
from rest_framework.permissions import BasePermission
class IsEditor(BasePermission):
def has_permission(self, request, view):
return (
request.user.is_authenticated
and request.user.groups.filter(name="Editors").exists()
)
Discussion