Customize permissions to your API’s like a PRO
By: Juli Colombo
Tags: Web Development, Programming, Backend, Django, Python

Permissions check

Permissions can be applied in views so that they are checked before calling the actual functions (get, post, put, delete, etc). Those permissions objects must define a has_permission method. For example, the IsAuthenticated permission is defined like this:

class IsAuthenticated(BasePermission):

"""

Allows access only to authenticated users.

""" def has_permission(self, request, view):

return bool(request.user and request.user.is_authenticated)

In case we are using the Model View Set, we have to know that we have preset methods implemented to perform CRUD operations for a certain model. These methods are:

list: Returns a collection of model instances.

retrieve: Returns a single model instance.

create: Creates a model instance.

update: Updates a single model instance.

destroy: Deletes a single model instance.

Knowing that, we have to notice that there is also a Django Model Permission. This permission uses the queryset attribute in the viewset, so that it finds the access rule that the user must have in order to operate on a model. The list and create methods need model permissions, whereas retrieve, update and destroy methods need object permissions. What does that mean? Well, you have to provide the model instance’s id in the url to get it, modify it or delete it. Depending on the operation we want to perform on a model, we have to define:

has_permission : To check if the user has model permissions.

: To check if the user has model permissions. has_object_permission: To check if the user has object permissions.

Custom permission

Assume that we have to check if the user trying to access to a specific resource is actually its owner, or the user associated with it. To do that, we have to create a custom permission implementing this logic.

In order to know if the resource is accessible by the current user, we can check three things:

If the user is a superuser, we let them access.

If the user is not an administrator, and the object is an instance of User, both users have to be the same.

If the user is not an administrator, and the object is not an instance of User but it has an owner, the user and the owner have to be the same.

Imagine that we have a Model View Set for a resource called “Message” that provides the CRUD actions. Then we simply include the custom permission in our view like this:

class MessageViewSet(viewsets.ModelViewSet):

serializer_class = MessageSerializer

permission_classes = [IsAuthenticated, OwnerOrModelPermission]

queryset = Message.objects.all()

Custom error response

Suppose that the user can perform some actions to the messages instances, but he or she has to pay monthly to access to those features.

By default, DRF’s determination is to return a 403 or a 401 status code response if the user has not the correct permissions or if he or she is not logged in. But as we mentioned at the beginning of this article, status codes are a good way of communicating events.

There is the 402 status code which means “Payment Required”. Wouldn’t it be great to use it to deny the access to users that haven’t paid yet?

Payment Required Permission

First of all, as we did before, we have to define a new permission that checks if the user who is requesting access to a certain resource has effectively paid for that.

class PaymentRequiredPermission(ModelPermissions):

def can_operate(self, request):

return request.user.has_paid() def has_permission(self, request, view):

return self.can_operate(request) def has_object_permission(self, request, view, obj):

return self.can_operate(request)

Notice that we have to define a has_paid method in User that implements some logic to check if the user is up to date with the payments, but I don’t want to go into that since it depends a lot on the project.

Payment Required Mixin

In order to run the validations in the correct view, we will create a mixin that can be used in any view or viewset and checks that the user has already paid. Let’s create the mixin!

Some things to bear in mind is that the mixin will first check for the usual permissions: Is the user logged in? Do they have permission to access to the resource? If the check fails, the view will return a 401 or a 403 status code respectively. If the validations pass, it will check the payment status, returning the 402 status code if the user hasn’t paid.

Let’s see how our view will look like, using our mixin:

class MessageViewSet(PaymentRequiredMixin, viewsets.ModelViewSet):

serializer_class = MessageSerializer

permission_classes = [IsAuthenticated, OwnerOrModelPermission]

queryset = Message.objects.all()

Testing

Now that we have all the new permissions and mixin, let’s write some tests to check that everything works as we want.