Django REST Framework Tutorial : Permissions

Welcome back to the 2nd series of my Django Rest framework tutorial. This post would be a continuation of my previous post, please click on any link below to view any post you missed.

The code to this tutorial is hosted on Github

OUTLINE OF DJANGO REST FRAMEWORK SERIES

At the end of this post, you will learn the following

  • How to set up different types of permission levels which are view, project, and object level.
  • How to grant or deny access to different users to different parts of the API.

 

Currently, our API doesn’t have any restrictions on who can edit or delete blog content.

We’d like to modify it now:

  • Blog notes should always be associated with a creator.
  • Only authenticated users may create blog notes.
  • Only the creator of a blog may update or delete it.
  • Unauthenticated requests should have full read-only access.

 

Adding a login to the Browsable API

We can add a login view for use with the browsable API, by editing the URL conf in our project-level urls.py file.

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("blog_api.urls")),  
    path('api-auth/', include('rest_framework.urls')),
]

The 'api-auth/' part of the pattern can actually be whatever URL you want to use.

Open this URL   http://localhost:8000/api/v1/post/

I am logged in with my superuser account   Click on the link and a dropdown menu with “Log out” appears. Click on it and logout.

Now if you open up the browser again and refresh the page you’ll see a ‘Login’ link in the top right of the page. If you log in as one of the users you created earlier, you’ll be able to create blog content again.

Currently, any anonymous non-authorized user can access our post list endpoint.

We know this because even though we are not logged-in, you still have full access to create, edit, update or delete a post.  This is because our default permission is AllowAnyin oursettings.py file.

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.AllowAny",
    ]
}

permissions

Permissions are used to grant or deny access for different classes of users to different parts of the API.

Django REST Framework ships with a number of built-in level permissions settings.

  • AllowAny  any user (authenticated or not) including anonymous users have full access.
  •  IsAuthenticated only registered & authenticated users have access.
  •   IsAdminUseronly admins/superusers have access
  • IsAuthenticatedOrReadOnlyunauthorized users can view any page, but only authenticated users have write, edit, or delete privileges.

There are multiple places we can add permissions to our blog API.

  • project-level
  • view-level
  • object level

 

We’d like all our blog contents to be visible to anyone, but also make sure that only the user that created a blog is able to update or delete it.

I will show you how to achieve this using the project and view level type permission types.

View-Level Permissions

This permission is set at the view level, the simplest style of permission would be to allow access to any authenticated user and deny access to any unauthenticated user using  the IsAuthenticatedOrReadOnly class in the REST framework.

In your blog_api/views.py file, add the permission_classes field to each view.

from rest_framework import generics , permissions
from.models import Post, Category
from.serializers import PostSerializer , CategorySerializer


class PostListView(generics.ListCreateAPIView):
    permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    queryset=Post.objects.all()
    serializer_class= PostSerializer
  


class CategoryListView(generics.ListCreateAPIView):
    permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    queryset=Category.objects.all()
    serializer_class= CategorySerializer


class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    queryset=Post.objects.all()
    serializer_class= PostSerializer

class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    queryset=Category.objects.all()
    serializer_class= CategorySerializer

 

Refresh the browsable API at http://127.0.0.1:8000/api/v1/post/

Refresh the browsable API to view our third post at http://127.0.0.1:8000/api/v1/post/data-science/

Since we are not logged in, there are no forms in the browsable API to edit the data since we don’t have permission.

Therefore at this point, only logged-in users can view, create, update, and delete our API. If you log back in with either your superuser details, the API endpoints will be accessible.

Project-Level Permissions

This permission is set at the project level, an example of the permission level set at project-level settings.py file.

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.AllowAny",
    ]
}

We edit the code above and change it to this below

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticatedOrReadOnly",
    ]
}

Try visiting http://127.0.0.1:8000/api/v1/post/,there are no forms in the browsable API to edit the data since we don’t have permission.

 

OBJECT-Level Permission

Object-level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.

Django REST Framework relies on a BasePermission class (here is the source code which is available on Github) from which all other permission classes inherit.

To implement custom permission, we need to override BasePermission class and implement either, or both, of the following methods,

  • .has_permission(self, request, view)
  • .has_object_permission(self, request, view, obj)

The methods should return True if the request should be granted access, and False otherwise.

Object-level permission has limitations, for performance reasons the DRF generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.

To ensure that users only have visibility onto instances that they are permitted to view, we need to set  the DEFAULT_PERMISSION_CLASSES  globally.

We are going to change DEFAULT_PERMISSION_CLASSES to IsAuthenticated

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

Create our custom permission

In the blog_api app, create a new file, permissions.py

from rest_framework import permissions


class IsAuthorOrReadOnly(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the author of a post
        return obj.author == request.user

If order to test if a request is a read operation or a write operation, we should check the request method against the constant SAFE_METHODS, which is a tuple containing 'GET', 'OPTIONS' and 'HEAD'

 

Back in the views.py file, we should import our new permission class IsAuthorOrReadOnly for PostDetail and CategoryDetail

from rest_framework import generics , permissions
from.models import Post, Category
from.serializers import PostSerializer , CategorySerializer
from .permissions import IsAuthorOrReadOnly

class PostListView(generics.ListCreateAPIView):
    # permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    permission_classes=(IsAuthorOrReadOnly,)
    queryset=Post.objects.all()
    serializer_class= PostSerializer
  


class CategoryListView(generics.ListCreateAPIView):
    # permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    permission_classes=(IsAuthorOrReadOnly,)
    queryset=Category.objects.all()
    serializer_class= CategorySerializer


class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    # permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    permission_classes=(IsAuthorOrReadOnly,)
    queryset=Post.objects.all()
    serializer_class= PostSerializer

class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
    # permission_classes=(permissions.IsAuthenticatedOrReadOnly,)
    permission_classes=(IsAuthorOrReadOnly,)
    queryset=Category.objects.all()
    serializer_class= CategorySerializer

Try visiting http://127.0.0.1:8000/api/v1/post/,there are no forms in the browsable API to edit the data since we don’t have permission.

Summary

We’ve now got permissions on our Web API that allows only authenticated users to create, edit and delete category/post page.

A good process when setting up permission is to set a strict project-level permissions policy (IsAuthenticated)such that only authenticated users can view the API. Then make view-level or custom permissions more accessible as needed on specific API endpoints.

 

In my next post, we’ll look at how we can set up user authentication.

 

Leave a Reply

Your email address will not be published. Required fields are marked *