This article looks at how to build custom permission classes in Django REST Framework (DRF).
Django REST Framework Permissions Series:
- Permissions in Django REST Framework
- Built-in Permission Classes in Django REST Framework
- Custom Permission Classes in Django REST Framework (this article!)
By the end of this article, you should be able to:
- Create custom permission classes
- Explain when to use
has_object_permissionin your custom permission classes
- Return a custom error message when a permission is denied
- Combine and exclude permission classes using AND, OR, and NOT operators
Custom Permission Classes
If your application has some special requirements and the built-in permission classes don't meet those requirements, it's time to start building your own custom permissions.
Creating custom permissions allows you to set permissions based on whether the user is authenticated or not, the request method, the group that the user belongs to, object attributes, the IP address... or any of their combinations.
All permission classes, either custom or built-in, extend from the
class BasePermission(metaclass=BasePermissionMetaclass): """ A base class from which all permission classes should inherit. """ def has_permission(self, request, view): """ Return `True` if permission is granted, `False` otherwise. """ return True def has_object_permission(self, request, view, obj): """ Return `True` if permission is granted, `False` otherwise. """ return True
BasePermission has two methods,
has_object_permission, that both return
True. Permission classes override one or both of those methods to conditionally return
True. If you don't override the methods, they will always return
True, granting unlimited access.
For more on
has_object_permission, be sure to check out the first article in this series, Permissions in Django REST Framework.
By convention, you should put custom permissions in a permissions.py file. This is just a convention so you don't have to do this if you need to organize your permissions differently.
As with the built-in permissions, if any of the permission classes used in a view returns
False from either
PermissionDenied exception is raised. To change the error message associated with the exception, you can set a message attribute directly on your custom permission class.
With that, let's look at some examples.
Custom Permission Examples
You may want to give different levels of access to different users, based on their properties -- i.e., are they the creators of the object or are they a staff member?
Let's say you don't want staff members to be able to edit objects. Here's how a custom permission class for that case might look like:
# permissions.py from rest_framework import permissions class AuthorAllStaffAllButEditOrReadOnly(permissions.BasePermission): edit_methods = ("PUT", "PATCH") def has_permission(self, request, view): if request.user.is_authenticated: return True def has_object_permission(self, request, view, obj): if request.user.is_superuser: return True if request.method in permissions.SAFE_METHODS: return True if obj.author == request.user: return True if request.user.is_staff and request.method not in self.edit_methods: return True return False
AuthorAllStaffAllButEditOrReadOnly class extends
BasePermission and overrides both
has_permission only one thing gets checked: If the user is authenticated. If not, the
NotAuthenticated exception is raised and access is denied.
Since you should never limit the superuser's access, the first check --
request.user.is_superuser -- grants access to the superuser.
Next, we check if the request method is one of the "safe" ones --
request.method in permissions.SAFE_METHODS. Safe methods are defined in rest_framework/permissions.py:
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
These methods have no impact on the object; they can only read it.
At first glance, it may seem like the
SAFE_METHODS check should be in the
has_permission method. If you're only checking the request method then that's the place it should be. But in this case, other checks wouldn't be executed:
Since we want to grant access when the method is one of the safe ones or when the user is the author of the object or when the user is a staff member, we need to check that on the same level. In other words, since we can't check for the owner on the
has_permission level, we need to check everything on the
The last possibility is that the user is a staff member: They are allowed all methods but the ones we defined as
Finally, turn back to the class name:
AuthorAllStaffAllButEditOrReadOnly. You should always try to name the permission class as informative as possible.
Keep in mind that
has_object_permissionis never executed for list views (regardless of the view you're extending from) or when the request method is
POST(since the object doesn't exist yet).
You use the custom permission class the same way as the built-in one:
# views.py from rest_framework import viewsets from .models import Message from .permissions import AuthorAllStaffAllButEditOrReadOnly from .serializers import MessageSerializer class MessageViewSet(viewsets.ModelViewSet): permission_classes = [AuthorAllStaffAllButEditOrReadOnly] # Custom permission class used queryset = Message.objects.all() serializer_class = MessageSerializer def perform_create(self, serializer): serializer.save(author=self.request.user)
The author of the object has full access to it. A staff member, meanwhile, can delete the object, but can't edit it:
And an authenticated user can view the object, but can't edit or delete it:
Although we briefly touched on the object's property in the previous example, the emphasis was much more on the user's properties (e.g., the object's author). In this example, we'll focus on the object's properties.
How can one or more of the object's properties have an impact on the permissions?
- As in the previous example, you can limit the access only to the owner of the object. You can also limit access to the group the owner belongs to.
- Objects may have an expiration date, so you can limit the access to objects older than n to only some users.
- You can have DELETE implemented as a flag (so that it's not actually removed from the database). You can then prevent access to objects with a delete flag.
Let's say you want to restrict access to objects older than 10 minutes for everyone except superusers:
# permissions.py from datetime import datetime, timedelta from django.utils import timezone from rest_framework import permissions class ExpiredObjectSuperuserOnly(permissions.BasePermission): def object_expired(self, obj): expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10)) return obj.created < expired_on def has_object_permission(self, request, view, obj): if self.object_expired(obj) and not request.user.is_superuser: return False else: return True
In this permission class, the
has_permission method is not overridden -- so it will always return
Since the only important property is the object's creation time, the check happens in
has_object_permission (since we don't have access to an object's properties in
So, if a user wants to access the expired object, the exception
PermissionDenied is raised:
Again, as with the previous example, we could check if the user is a superuser in
has_permission, but if they're not, the object's property would never get checked.
Take note of the error message. It's not very informative. The user has no idea why their access was denied. We can create a custom error message by adding a
message attribute to our permission class:
class ExpiredObjectSuperuserOnly(permissions.BasePermission): message = "This object is expired." # custom error message def object_expired(self, obj): expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10)) return obj.created < expired_on def has_object_permission(self, request, view, obj): if self.object_expired(obj) and not request.user.is_superuser: return False else: return True
Now the user sees exactly why the permission was denied:
Combining and Excluding Permission Classes
Typically, when using more than one permission class, you'd define them in the view like so:
permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]
This approach combines them so that permission is granted only if all of the classes return
Since DRF version 3.9.0, you can also combine multiple classes using the AND (
&) or OR (
|) logical operators. Also, since 3.9.2, the NOT (
~) operator is supported.
These operators are not limited to custom permission classes. They can also be used with the built-in ones.
Instead of creating many complicated permission classes that are similar to each other, you could create simpler classes and combine them with the aforementioned operators.
For example, you may have different permissions for different combinations of groups. Let's say you want the following permissions:
- Permission for groups A or B
- Permission for groups B or C
- Permission for members of both B and C
- Permission for all groups but A
While four permission classes doesn't seem like much this won't scale well. What if you had eight different groups -- A, B, C, D, E, F, G? It would quickly ballon to a point where it's impossible to understand and maintain.
You could simplify it and combine them with operators by first creating permission classes for groups A, B, and C. Then, you can implement them like so:
permission_classes = [PermGroupA | PermGroupB]
permission_classes = [PermGroupB | PermGroupC]
permission_classes = [PermGroupB & PermGroupC]
permission_classes = [~PermGroupA]
When OR (
|) is involved, things can get a little more complicated. Errors can often fall through the cracks. For more, review the discussion on the permissions: Allow permissions to be composed pull request.
AND is the default behavior of permission classes, achieved by using
permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]
It can also be written with
permission_classes = [IsAuthenticated & IsStaff & SomeCustomPermissionClass]
With the OR (
|), when any of the permission classes return
True, the permission is granted. You can use the OR operator to offer multiple possibilities in which the user gets granted permission.
Let's look at an example where either the owner of the object or a staff member can edit or delete the object.
We'll need two classes:
Trueif the user
Trueif the user is the same as the
class IsStaff(permissions.BasePermission): def has_permission(self, request, view): if request.user.is_staff: return True return False def has_object_permission(self, request, view, obj): if request.user.is_staff: return True return False class IsOwner(permissions.BasePermission): def has_permission(self, request, view): if request.user.is_authenticated: return True return False def has_object_permission(self, request, view, obj): if obj.author == request.user: return True return False
There's quite a bit of redundancy here, but it's necessary.
For covering list views
Again, the list view doesn't check for
has_object_permission. However, each of the created permissions needs to be standalone. You shouldn't create a permission class that needs to be combined with another permission class to cover the list view.
IsOwnerlimits access to authenticated users in
has_permission-- so that if the
IsOwneris the only class used, access to API is still controlled.
Both methods by default return
When using OR, if you don't provide the
has_object_permissionmethod, the user will have access to the object, even though they shouldn't.
if you omit
IsOwnerclass, anyone will be able to see or create on the list.
if you omit
IsStaffand combine it with
or, either one or the other will return
True. That way, a registered user that's neither an owner nor a staff member, would be able to change the content.
Now, when we have our permission classes well designed, it's easy to combine them:
from rest_framework import viewsets from .models import Message from .permissions import IsStaff, IsOwner from .serializers import MessageSerializer class MessageViewSet(viewsets.ModelViewSet): permission_classes = [IsStaff | IsOwner] # or operator used queryset = Message.objects.all() serializer_class = MessageSerializer
Here, we allow either a staff member or the owner of the object to change or delete it.
The only requirement
IsOwner has for the list views, is that the user is authenticated. That means that the authenticated user that's not a staff member, will be able to create objects.
The NOT operator results in the exact opposite to the defined permission class. In other words, permission is granted to all users except the ones from the permission class.
Let's say you have three groups of users:
Each of these groups should have access to API endpoints meant only for their specific group.
Here's a permission class that grants access to only members of the Finances group:
class IsFinancesMember(permissions.BasePermission): def has_permission(self, request, view): if request.user.groups.filter(name="Finances").exists(): return True
Now, say you have a new view that's meant for all users who are not part of the Finances group. You can use the NOT operator to implement this:
from rest_framework import viewsets from .models import Message from .permissions import IsFinancesMember from .serializers import MessageSerializer class MessageViewSet(viewsets.ModelViewSet): permission_classes = [~IsFinancesMember] # using not operator queryset = Message.objects.all() serializer_class = MessageSerializer
So, only members of the Finances group won't have access.
Be careful! If you only use the NOT operator, everybody else will be allowed access, including unauthenticated users! If that's not what you meant to do, you can fix that by adding another class like so:permission_classes = [~IsFinancesMember & IsAuthenticated]
permission_classes you can also use parentheses (
()) to control which expression gets resolved first.
class MessageViewSet(viewsets.ModelViewSet): permission_classes = [(IsFinancesMember | IsTechMember) & IsOwner] # using parentheses queryset = Message.objects.all() serializer_class = MessageSerializer
In this example,
(IsFinancesMember | IsTechMember) will be resolved first. Then, the result of that will be used with
& IsOwner -- e.g.,
ResultsFromFinancesOrTech & IsOwner. This means that a user that's either a member of the Tech OR Finances groups AND is the owner of the object will be granted access.
Despite a wide range of built-in permission classes, there will be situations where they won't fit your needs. That's when custom permission classes come in handy.
With custom permission classes you must override one or both of the following methods:
If permission isn't granted in the
has_permission method, it doesn't matter what's written in
has_object_permission -- the permission is denied. If you don't override one (or both) of them, you need to take into account that by default, the method will always return
You can combine and exclude permission classes with the AND, OR, and NOT operators. You can even decide the order in which permissions resolve in with parenthesis.
Django REST Framework Permissions Series: