02.06.2021       Выпуск 389 (31.05.2021 - 06.06.2021)       Статьи

Permissions in Django Rest Framework

This article looks at how permissions work in Django REST Framework.

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

This article looks at how permissions work in Django REST Framework (DRF).

Contents

Objectives

By the end of this article, you should be able to explain:

  1. How DRF permissions work
  2. The similarities and differences between has_permission and has_object_permission
  3. When to use has_permission and has_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:

  1. check_permissions checks if the request should be permitted based on request data
  2. check_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 calls has_permission for each of the permissions
  • check_object_permissions calls has_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 call check_object_permission to execute has_object_permission for all permission classes.
  • With ViewSets (like ModelViewSet) or Generic Views (like RetrieveAPIView), has_object_permission is executed via check_object_permission inside a get_object method out of the box.
  • has_object_permission is never executed for list views (regardless of the that view you're extending from) or when the request method is POST (since the object doesn't exist yet).
  • When any has_permission returns False, the has_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?

DRF Permissions Execution

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 classhas_permissionhas_object_permission
AllowAny
IsAuthenticated
IsAdminUser
IsAuthenticatedOrReadOnly
DjangoModelPermissions
DjangoModelPermissionsOrAnonReadOnly
DjangoObjectPermissionsby extending DjangoModelPermissions

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.

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:

  1. 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.
  2. 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 viewDetail view
has_permissionGrants permission to the authenticated userGrants permission to the authenticated user
has_object_permissionHas no impactGrants permission to the author of the object
ResultAccess granted for the authenticated usersAccess 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:

DRF Browsable API Response detail view

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:

DRF Browsable API Response list view

What's going on?

  1. The list view only checks has_permission. So, since the custom class doesn't have one, it checks has_permission from the BasePermission, which unconditionally returns True.
  2. The detail view first checks the has_permission (again, always True). Then it checks has_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 viewDetail view
has_permissionUses the default function that grants permission without any conditionUses the default function that grants permission without any condition
has_object_permissionHas no impactGrants permission to the authenticated user
ResultPermission is always grantedPermission 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.






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus