This article looks at how permissions work in Django REST Framework (DRF).
--
Django REST Framework Permissions Series:
- Permissions in Django REST Framework (this article!)
- Built-in Permission Classes in Django REST Framework
- Custom Permission Classes in Django REST Framework
Contents
Objectives
By the end of this article, you should be able to explain:
- How DRF permissions work
- The similarities and differences between
has_permission
andhas_object_permission
- When to use
has_permission
andhas_object_permission
DRF Permissions
In DRF, permissions, along with authentication and throttling, are used to grant or deny access for different classes of users to different parts of an API.
Authentication and authorization work hand in hand. Authentication is always executed before authorization.
While authentication is the process of checking a user's identity (the user the request came from, the token that it was signed with), authorization is a process of checking if the request user has the necessary permissions for executing the request (are they a super user, are they the creators of the object).
The authorization process in DRF is covered by permissions.
View Permissions
APIView has two methods that check for permissions:
check_permissions
checks if the request should be permitted based on request datacheck_object_permissions
checks if the request should be permitted based on the combination of the request and object data
# rest_framework/views.py
class APIView(View):
# other methods
def check_permissions(self, request):
"""
Check if the request should be permitted.
Raises an appropriate exception if the request is not permitted.
"""
for permission in self.get_permissions():
if not permission.has_permission(request, self):
self.permission_denied(
request,
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
def check_object_permissions(self, request, obj):
"""
Check if the request should be permitted for a given object.
Raises an appropriate exception if the request is not permitted.
"""
for permission in self.get_permissions():
if not permission.has_object_permission(request, self, obj):
self.permission_denied(
request,
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
When the request comes in, authentication is performed. If the authentication isn't successful, a NotAuthenticated
error is raised. After that, the permissions get checked in a loop, and if any of them fail, a PermissionDenied
error is raised. Finally, a throttling check is performed against the request.
check_permissions
is called before the view handler is executed while check_object_permissions
is not executed unless you explicitly call it. For example:
class MessageSingleAPI(APIView):
def get(self, request, pk):
message = get_object_or_404(Message.objects.all(), pk=pk)
self.check_object_permissions(request, message) # explicitly called
serializer = MessageSerializer(message)
return Response(serializer.data)
With ViewSets and Generic Views, check_object_permissions
is called after the object is retrieved from the database for all detail views.
# rest_framework/generics.py
class GenericAPIView(views.APIView):
# other methods
def get_object(self):
"""
Returns the object the view is displaying.
You may want to override this if you need to provide non-standard
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
queryset = self.filter_queryset(self.get_queryset())
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
)
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj) # HERE
return obj
Permission is checked for all permissions and if any one of them return False
, a PermissionDenied
error is raised.
Permission classes
Permissions in DRF are defined as a list of permission classes. You can either create your own or use one of the seven built-in classes. All permission classes, either custom or built-in, extend from the BasePermission
class:
class BasePermission(metaclass=BasePermissionMetaclass):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return True
As you can see, BasePermission
has two methods, has_permission
and has_object_permission
, that both return True
. The permission classes override one or both of the methods to conditionally return True
.
Turn back to the check_permissions
and check_object_permissions
methods from the beginning of the article:
check_permissions
callshas_permission
for each of the permissionscheck_object_permissions
callshas_object_permission
for each of the permissions as well
has_permission
has_permission
is used to decide whether a request and a user are allowed to access a specific view
For example:
- Is the request method allowed?
- Is the user authenticated?
- Is the user an admin or super user?
has_permission
possesses knowledge about the request, but not about the object of the request.
As explained at the beginning, has_permission
(called by check_permissions
) gets executed before the view handler is executed, without explicitly calling it.
has_object_permission
has_object_permission
is used to decide whether a specific user is allowed to interact with a specific object
For example:
- Who created the object?
- When was it created?
- In which group does the object belong to?
Besides the knowledge of the request, has_object_permission
also possesses data about the object of the request. The method executes after the object is retrieved from the database.
Unlike has_permission
, has_object_permission
isn't always executed by default:
- With an
APIView
, you must explicitly callcheck_object_permission
to executehas_object_permission
for all permission classes. - With
ViewSets
(likeModelViewSet
) or Generic Views (likeRetrieveAPIView
),has_object_permission
is executed viacheck_object_permission
inside aget_object
method out of the box. has_object_permission
is never executed for list views (regardless of the view you're extending from) or when the request method isPOST
(since the object doesn't exist yet).- When any
has_permission
returnsFalse
, thehas_object_permission
doesn't get checked. The request is immediately rejected.
has_permission vs has_object_permission
What's the difference between has_permission
and has_object_permission
in Django REST Framework?
Again, for:
- List views, only
has_permission
is executed and the request is either granted or refused access. If access is refused, the objects never get retrieved. - Detail views,
has_permission
is executed and then only if permission is granted,has_object_permission
is executed after the object is retrieved.
Built-in DRF Permission Classes
With regard to the built-in DRF permission classes, all of them override has_permission
while only DjangoObjectPermissions
overrides has_object_permission
:
Permission class | has_permission | has_object_permission |
---|---|---|
AllowAny | ✓ | ✗ |
IsAuthenticated | ✓ | ✗ |
IsAuthenticatedOrReadOnly | ✓ | ✗ |
IsAdminUser | ✓ | ✗ |
DjangoModelPermissions | ✓ | ✗ |
DjangoModelPermissionsOrAnonReadOnly | ✓ | ✗ |
DjangoObjectPermissions | by extending DjangoModelPermissions |
✓ |
For more on the built-in permission classes, be sure to check out the second article in this series, Built-in Permission Classes in Django REST Framework.
Custom Permission Classes
For custom permission classes, you can override one or both of the methods. If you override only one of them you need to be careful, especially if the permissions you're using are complicated or you're combining multiple permissions. Both has_permission
and has_object_permission
defaults to True
, so if you don't set one of them explicitly, the denial of the request is dependent on the one you've explicitly set.
For more on custom permission classes, be sure to check out the second article in this series, Custom Permission Classes in Django REST Framework.
Correct Usage
Let's look at a quick example:
from rest_framework import permissions
class AuthorOrReadOnly(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
This permission class allows only the author of the object access to it:
- In
has_permission
, we refuse permission only to the unauthenticated users. At this point we don't have access to the object so we don't know if the user making the request is the author of the desired object. - If the user is authenticated, after the object is retrieved,
has_object_permission
is called where we check if the object's author is the same as the user.
Results:
List view | Detail view | |
---|---|---|
has_permission | Grants permission to the authenticated user | Grants permission to the authenticated user |
has_object_permission | Has no impact | Grants permission to the author of the object |
Result | Access granted for the authenticated users | Access granted to the owner of the object if they are authenticated |
Incorrect Usage
Let's look at a permission that won't do what we want, to better understand what's going on:
from rest_framework import permissions
class AuthenticatedOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.user.is_authenticated:
return True
return False
This permission denies access to the unauthenticated user, but the check is done in has_object_permission
instead of has_permission
.
Detail view for unauthenticated user:
Even though the auto-generated browsable API shows the delete button, the unauthenticated user can't delete the message.
And the list view for unauthenticated user:
What's going on?
- The list view only checks
has_permission
. So, since the custom class doesn't have one, it checkshas_permission
from theBasePermission
, which unconditionally returnsTrue
. - The detail view first checks the
has_permission
(again, alwaysTrue
). Then it checkshas_object_permission
, which denies access to unauthenticated users.
That's why in this example unauthenticated requests don't have access to detail views, but they do have access to list views.
List view | Detail view | |
---|---|---|
has_permission | Uses the default function that grants permission without any condition | Uses the default function that grants permission without any condition |
has_object_permission | Has no impact | Grants permission to the authenticated user |
Result | Permission is always granted | Permission is granted for the authenticated users |
This permission class was created only to show how the two methods work. You should use the built-in class
IsAuthenticated
class rather than creating your own.
Conclusion
All permissions, either custom or built-in, in Django REST Framework leverage either has_permission
or has_object_permission
or both to restrict access to API endpoints.
While has_permission
has no restrictions as to when it can be used, it doesn't have access to the desired object. Because of this, it's more of a "generic" permission check to ensure that the request and user can access the view. On the other hand, since has_object_permission
has access to the object, the criteria can be much more specific, but it has many limitations as to when it can be used.
Keep in mind, that if you don't override the methods, they will always return True
, granting unlimited access. Only has_permission
impacts the access to list views while they both impact the access to detail view.
Knowing and understanding how both of these methods work is especially important when creating custom permission classes.
--
Django REST Framework Permissions Series: