Django REST + custom permissions + Talend API / Postman = ❤️

Developing in real life: Django REST Framework + testing your custom permissions in an API application with Talend API Tester or Postman

Samuel Chan, Software Engineer

 

OK, so you definitely wouldn’t do this in production, but for the sake of this tutorial, let’s keep it simple.

— Every coding tutorial out there

I’ve surveyed numerous beginner’s guide to the Django REST Framework in trying to put together a learning path for my teammates, and came to the realization that a large majority of these tutorials are very focused on building toy projects (to-do list, project management app, another blog) yet fall short of how a real-life software would be architected.

This is not a criticism — it’s understandable and maybe even preferable that novices should be exposed to key foundational concepts at a pedagogically sound pace. But I get the frustration when a novice complains about her frustration that the guide completely omits critical knowledge in exchange of progressing the tutorial forward. The trade-off sounds something like “well make sure you don’t do this in production, but let’s move along for now.”

I thought it was an opportunity to compile some of the more common questions I get asked, and distill them into a series of “Developing in real life” articles. There isn’t a particularly narrow focus, beyond the fact that I will try to pick topics that I feel could bridge the “building toy projects from tutorials” to “building software applications for real” chasm. I’ll also try to cover topics I think existing materials (online videos, tutorials, blogs etc) don’t show enough love for.

Understanding Permissions with Django REST Framework

To start this series off, let’s talk about something that would be present in 100% of non-trivial API applications — permissions.

There are three mechanisms your Django REST app rely on to determine whether a request should be granted or denied access:

Authentication is what most beginners are taught, and hence are what most people are most familiar with. Authentication associates an incoming request with a set of identifying credentials, and the permissions and throttling policies use those credentials to determine if the request should be permitted. Because of this, authentication checks are performed first at the beginning of a request cycle, before the permission and throttling checks occur.

An important takeaway Django REST Framework wants you to know, is that authentication by itself does not permit or deny an incoming request, it merely associates the request with the credentials that the request was made with.

Once the authentication information is received, permissions checks are run before any other code is allowed to proceed. This is where the information (from request.user and request.auth properties) is used to actually determine if the incoming request should be permitted.

Throttling is similar to permissions in that it also determines if a request should be permitted, but throttles indicate a temporary state that puts (often temporary) constraints on the requests typically due to resources and costs.

Permissions example in Django REST

When building out your API, one would typically make use of the permission classes that REST framework provides out of the box. Two examples are:

  • IsAuthenticated class: grant full access (create, read, update, delete) to authenticated users, and deny access to unauthenticated users
  • IsAuthenticatedOrReadOnly class: grant full access (create, read, update, delete) to authenticated users, but allow read-only access to unauthenticated users

When the permissions checks fail either a “403 Forbidden” or a “401 Unauthorized” response will be returned.

Beginners are taught that while in development, it suffice to set a global permission policy using the DEFAULT_PERMISSION_CLASSES setting:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}

This is because during development, the only person that should really be using the API service is the developer himself. As long as the developer is signed in (authenticated), he or she should be given access to the full range of actions (create, read, update, delete) to speed up development and iterate through his ideas without necessarily worrying about complicated permission policies.

If the global setting is not specified, it defaults to allowing unrestricted access:

'DEFAULT_PERMISSION_CLASSES': [
   'rest_framework.permissions.AllowAny',
]

The authentication policy could also be set on a per-view, or per-viewset basis doing one of the following:

  1. import them from rest_framework.permissions before specifying a list of permission classes (yes, you can have multiple permissions in the list)
  2. use the @api_view decorator with function based views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        return Response('success!')

The permissions specified here overwrites the default setting in your settings.py file.

Subclassing from BasePermission

With Django REST framework, you can create sub-classes of  permissions.BasePermission, and then use bitwise operators to compose your permissions in permissions_classes:

from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS
from rest_framework.response import Response
from rest_framework.views import APIView

class ReadOnly(BasePermission):
    def has_permission(self, request, view):
        return request.method in SAFE_METHODS

class ExampleView(APIView):
    permission_classes = [IsAuthenticated|ReadOnly]

    def get(self, request, format=None):
        return Response('success!')

The code in ReadOnly returns a boolean depending on whether the request is one of the safe methods (GETHEAD or OPTIONS returns True).

A method that attempts to delete (DELETE) or modify (through PUT or PATCH) or add a model instance (POST) would return False.

What’s also nifty is how we use bitwise operators (|) to essentially say “user has full access if they are authenticated, they have read-only access otherwise”. This is clearly a lot more work, because you would use IsAuthenticatedOrReadOnly  instead but the option to subclass from BasePermission is instructive for what we’re about to cover next.

Scenario: IsCreatorOrReadOnly

While you’re developing software, these kind of built-in permission classes speed up development and since they’re provided ‘out-of-the-box’, it helps you write less code. Writing less code is good.

Every time you write new code, you should do so reluctantly, under duress, because you completely exhausted all your other options.” Robert Galanakis: “The fastest code is the code which does not run. The code easiest to maintain is the code that was never written.”

—  Jeff Atwood

But going from development to production, you are probably going to revisit some of these arguably simplistic permissions and here is where beginners often hit their first road-bump. Whether you’re creating a blog application, a book library, a newsletter, a portfolio tracking application, you will very likely need to write your own custom permissions. A typical pattern is what I call IsCreatorOrReadOnly:

  • If the model is created by the user (request.user), grant full access.
  • Otherwise, requests should pass the  SAFE_METHODS check (GETHEAD or OPTIONS returns True).

In effect, User A cannot delete a blog article that User B authored, but has access to read the article. User C can look at the stock portfolio of User D, but not modify the holdings in said portfolio. We implement this as a custom permission that subclasses permissions.BasePermission and, as is conventional, write this in permissions.py:

from rest_framework import permissions

class IsCreatorOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow creators of an object
to edit / delete it. Assumes the model instance has a
`creator` attribute.
"""
    def has_object_permission(self, request, view, obj):
        # Always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True
        # Instance must have an attribute named `creator`.
        return obj.creator == request.user

We then bring our permission class into our views.py and use it:

class SurveyViewSet(viewsets.ModelViewSet):
    queryset = Survey.objects.all()
    serializer_class = SurveySerializer
    permission_classes = [IsCreatorOrReadOnly]

    def get_queryset(self):
        # filter queryset based on whether it's published
        return self.queryset.filter(status='published')

 

Test our Permission Class

Django REST Framework reflects this new permission class that we implement in its Browsable API.

Left: On a model that the user isn’t a creator of, the red DELET button is hidden. The Raw data form for PUT and PATCH is also hidden. Right: Because user is the creator, DELETE, PUT and PATCH requests are all available.

The non-safe methods are hidden from the Browsable API, so how are we to really test that we’re getting a  PermissionDenied  if a user uses a different client to perform, say, a  DELETE method on an instance?

One solution is to  curl it, but a friendlier interface like Postman, Hoppscotch (formerly Postwoman) or Talend API Tester does the trick as well and makes you feel good using it.

Open up your favorite API testing client (doesn’t matter which, but I’d be using Talend API Tester, available as a Chrome Extension), and start by performing a GET request.

You should expect a very unevenetful 200 OK status code. Because of our permission, you will be successful at the GET request even as an unauthorized user (without logging in).

Now I’m going to add an authorization by signing in as tiatira and attempt to send a request with a DELETE method to a model instance that belongs to samuel. The expectation is that we should get a 403 Forbidden response and our server will print out Forbidden: /survey/api/surveys/19/

When the permissions checks fail either a “403 Forbidden” or a “401 Unauthorized” response will be returned, according to the following rules:

  • The request was successfully authenticated, but permission was denied. — An HTTP 403 Forbidden response will be returned.

— Django REST Framework API Guide

Depending on your API Client, there are numerous threads on some API Client mis-configuring the headers. In that case, you may run into the following error:

{"detail":"CSRF Failed: CSRF token missing or incorrect."}

The solution is to make sure you manually retrieve the CSRF token from your browser (it is stored as a cookie) and pass this token in using the X-CSRFToken header or csrftoken in Cookie — depending on the API client you choose.

Finally, remove the Authorization for the user and now authorize as the user who is the creator of that object. In my case, this means signing in as samuel. Perform the DELETE request again and you should now expect this method to successfully execute.

HTTP 204 simple means that  The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. If you try to make a GET request to this resource again, you’ll get a 404 Not found.

 

Closing Notes

In summary, we’ve seen how to make our API a little more secured and production-ready by:

  • Understanding that Django REST API uses a combination of Authentication, Throttling and Permissions to determine whether a request should be granted or denied access.
  • Authentication on its own merely associates the request with the credentials that the request was made with. It makes no effort on determining whether the request should be permitted or denied.
  • Permissions is. the mechanism that actually determine if the incoming request should be permitted.
  • Django REST Framework provides a few permission classes out of the box, and most tutorials recommend them to beginners with the caveat of “definitely change this in production”. These are easy, convenient and require no extra code.
  • Sub-classing from permissions.BasePermission is how we create our custom class.
  • Using permissions.SAFE_METHODS as an alias to read-only (GET, HEAD or OPTIONS) methods otherwise perform a obj.creator == request.user check.
  • Bring our custom permission class into the permissions_classes and check that the Browsable API has updated its view.
  • Retrieving csrftoken from your browser’s cookies to use it in your API testing client.
  • Checking for a 403 Forbidden when trying to perform a DELETE method as a user that isn’t the creator of the resource.
  • Confirm that, when authenticated as the creator of the resource, the same DELETE method was successful.

This concludes my quick introduction to custom permission in Django REST Framework today. I’ll see you in the next article and till then, you can connect with me on any of your favorite social media channels.

Supertype on Social Media

Exit mobile version