Django and Django Rest Framework Permissioning system

What are Permissions

In Django Permissions can apply to

  • Individual Users
  • Groups of Users

Permissions are all about

  • Access – Who can see what?
  • Control – Who can do what

In django docs, Permissions are defined like so:

Permissions [are] binary (yes/no) flags designating whether a user may perform a certain task

So, the above definitions covers both, who can see what and who can do what. What stuck out to me were two points in above definition:

  1. binary (yes/no) flags: Its important to understand permissions as binary state. Its either tru or false. There is no middle ground.
  2. perform a certain task: This is really about control and not access. And this is on a per task basis and not like on a per page basis.

Also, this definition kind of implicitly separates permissions from tasks. And if you are building a permissioning system, you should keep it that way.

Rule No. 1: Permissions, while related to tasks, should be kept separate from tasks.

By keeping them separate, it becomes easier to avoid unexpected outcomes. To extend permissions management systems. And most importantly, to write tests around your permissions management system.

This separation is also similar in user in groups.

Types of users in Django

There are three types of users in Django:

  1. Anonymous users (logged out users)
  2. Regular users
  3. Superusers

Both 2 and 3 are logged in users.

What is Permissioning?

A system for managing permissions.

Its like if permissions is your state, then permissioning is your state-flow.

Django Permissions – Out of the Box

  • Out of the box Django permissions are tied to (and part of) the Django authentication system. This is understandable because permissions are tied to user.
  • It includes concepts of Groups. Like you can have a group of privileged users and assign/change permissions to the whole group rather than assigning/changing them individually for each member. This makes good sense but should be used carefully as side-effects may arise when both user level and group level permissions are being changed.
  • It allows permissions to be associated with models i.e. permissioning is tied to the model system in Django. It doesn’t have to be but it usually is.
  • Has three types of model permissions: add, change and delete. You get these three types of permissions for free out of the box. What is important to know is that there is read permission implicit in this i.e. its implicit that if you are logged in, you can read everything in the site or you don’t have access to the model at all. It can be changed but by default read permissions are implicit. So, if you content that you want some logged in users to read but not others, you have to make that permission explicit. In most complex permission management system this is explicitly handled.
  • Django (out of the box) does not allow permissions to be associated with individual model instances/objects. A direct consequence is that its not possible that a user is able to see some articles but now all. Out of the box, Django permissions work like this: a user has either read permission on all instances of model Article or none at all. There is no middle ground. If you want to change this behavior, you need to customize.

If you had code like this (scenario is that we have users who can create articles):

class Articles(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=255, blank=False)
    body = models.TextField(blank=False)
    author = models.ForeignKey(settings.AUTH_USER_MODEL)

    class Meta:
        db_table = 'content__articles'
def article_edit(request, article_id):
    if not request.user.is_superuser:
        raise Http404
    # normal view logic to edit the article here

Its a totally valid code. It will work 100%. BUT YOU BROKE RULE NO.1. We shouldn’t check if user is a superuser or not. We should check if the user has the permissions to interact with the view. Here this is a distinction without a difference but in scenario where you have a third type of user like editor, this logic of just checking for superuser will not work.

So, it is pretty easy to keep the permissions separate from the task. We don’t check if a user is a superuser or not. Instead we check if the user has permission to change the articles.

def article_edit(request, article_id):
    if not request.user.has_perm('app_label.change_articles'):
        raise Http404
    # normal view logic to edit the article here

This approach also makes testing for this particular permission easy.

Lets take a look at the Django source code for has_perm we just used:

def has_perm(self, perm, obj=None):
        """
        Return True if the user has the specified permission. Query all
        available auth backends, but return immediately if any backend returns
        True. Thus, a user who has permission from a single auth backend is
        assumed to have permission in general. If an object is provided, check
        permissions for that object.
        """
        # Active superusers have all permissions.
        if self.is_active and self.is_superuser:
            return True

        # Otherwise we need to check the backends.
        return _user_has_perm(self, perm, obj)

You see that for superuser, its always true. For a non superuser, further check is done. Also notice the obj=None in arguments. This obj is not actually used. But this lays the possibility for object level permissions.

The scenario that we have till now have is that of the three model permissions for an article:

A superuser can do everything.

A regular user can only add articles but not change or delete them.

With the above code, we can easily implement this scenario.

Now lets complicate things a bit:

A superuser can do everything.

A regular user can add articles but only change articles added by him. Regular user still cannot change outher user’s article or delete any article.

def article_edit(request, article_id):
    instance = get_object_or_404(Articles, id=article_id)
    user_is_owner = (request.user == instance.author)
    user_has_perm = request.user.has_perm('app_label.change_articles')
    if not user_is_owner or not user_has_perm:
        raise Http404

    # normal view logic to edit the article here

The above works 100%. But YOU BROKE RULE NO.1

If we want to separate the permissions logic from task logic, lets do it like this:

def has_perm_or_is_author(user_object, permission, instance=None):
    if instance is not None:
        if user_object == instance.user:
            return True
    return user_object.has_perm(permission)

(Its preferable to make Permissions as class based, but function based like here is easier for explaining). Note that Django rest Framework makes it a point to make all permissions class based.

So lets now look at how to do a permission management system. In a permission management system, we should have the ability to assign and revoke permissions. So now we add a third kind of user, the editor. An editor can assign or revoke permissions to any Article by any user but on any other model. An editor can also add, change or delete any Article but not have access to an other model.

By separating the permissions check into a permissions.py file, we are already most of the way into implementing this.
We can do this in two ways.

One way is to create a file, say utils.py and put in it two opposite functions, one to grant permission and another to revoke them. Also here we will directly work with the user object.

def grant_editor_status(user_object):
    add_perm = Permissions.objects.get(name='Can add article')
    change_perm = Permissions.objects.get(name='Can change article')
    delete_perm = Permissions.objects.get(name='Can delete article')

    user_object.user_permissions.add(add_perm, change_perm, delete_perm)
    return User.objects.get(id=user_object.id)

and the revoke function just works the other way, though not included in above file.

The other way to do this is to use django’s groups permissionsing system. With this approach, you assign permissions to a group and then you add the user to that group. So, here, rather than the user have permissions explicitly, the user has permissions implicitly by virtue of the fact that they are member of a group.

Rule No. 2: You can assign permissions to users or to groups, but you probably shouldn’t do both.

def add_editor_perms_to_group():
    #one time use
    group = Group.objects.get(name='editors')
    add_perm = Permission.objects.get(name='Can add article')
    change_perm = Permission.objects.get(name='Can change article')
    delete_perm = Permission.objects.get(name='Can delete article')
    group.permissions.add(add_perm, change_perm, delete_perm)
    return group

def grant_editor_status(user_object):
    group = Group.objects.get(name='editors')
    user_object.groups.add(group)
    return User.object.get(id=user_object.id)

def revoke_editor_status(user_object):
    group = Group.objects.get(name='editors')
    user_object.groups.remove(group)
    return User.object.get(id=user_object.id)

There is nothing in Django that will stop you from breaking rule 2 but you might end up in situations where you revoked a permission from a user, but the user ends up still having it because he is a member of a group. or if you removed a user from a group, but the user still has some privilaged permissions because they were also assigned to the user explicitly. So, just to manage the permission state in you application, try to adhere to Rule 2.

Pros and cons of user permissions vs group permissions approach

Assumption is that in you project, we only either use user permissions or group permissions, not both.

User permissions pros:
You assign/revoke the permissions explicitly. So you are always sure that when you revoke a permission from a user, you know precisely that that user does not have that permission anymore.

User permission cons:
It can be bit hard to manage rules because every time a rule is added/modified, we have to ensure we have the logic to cycle it through users and apply it individually.

Group permission pros:
Managing permissions is really easy. If you change some permissions, they are automatically applied to all users in the group.

Group permissions cons:
There will be scenarios where there will be multiple groups, with some common permissions. Now it becomes tricky because, even if we removed a user from a group, that user can still have some permissions from that group simply because they are also common with another group and that user is still a part of that another group.

Object Permissions

As said before, Django provides foundation but no implementation of object level permissions out of the box. Any attempt to check objeck oobject level permissions will always return false.

But this foundation given by Django can be extended to actually provide object level permissions.

Out of all the Django extensions, django-guardian is the most well maintained that does this.
django-guardian:

  • Extends Django’s out of the box permissions
  • Uses an additional authentication backend
  • Great project, less than great documentation

So, we take an example from before where you have this site with users and articles and you want to add a paywall to it so that some articles are behind a paywall and others aren’t. Obviously this is a trivial example where you can have either paid users or unpaid users but we are going to do it with object level permissioning.

So, let’s add object level permissions to the model we had before.

class Articles(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=255, blank=False)
    body = models.TextField(blank=False)
    author = models.ForeignKey(settings.AUTH_USER_MODEL)

    class Meta:
        db_table = 'content__articles'
        permissions = (("view_articles", "Can view articles"),)

Note that here we added a permissions line at the bottom.

Django, out of the box, gives you the add, delete and change permission. It doesn’t give you the view permission. But since that’s exactly we want, we add it to the permissions tuple to the meta of the model.

Then we create view for the article. We don’t have to do anything else other than checking for view permission:

def article_view(request, article_id):
    instance = get_object_or_404(Articles, id=article_id)
    can_view = request.user.has_perm('app_label.view_articles', instance,)
    if not can_view:
        raise Http404
    # normal view logic to view article

In the above view, we are just checking if the user has the permission to view an article. Notice that we are passing the instance of the article when checking for permission. If we did this in out of the box django, it will always return False. django-guardian has the required extensions to handle object level permissions, so here we can do this.

Lets see how to assign and revoke permissions to the user:

from guardian.shortcuts import assign_perm, remove_perm

def assign_object_view_perm(user_object, instance):
    assign_perm('view_articles', user_object, instance)
    return User.objects.get(id=user_objects.id)

def revoke_object_view_perm(user_object, instance):
    remove_perm('view_articles', user_object, instance)
    return User.objects.get(id=user_objects.id)

Here we are using shortcuts provided by guardian. We pass the permission, user object and the instance. The same shortcuts work for group objects as well. There you will pass the permission, group object and the instance. See below for an example of providing an example of group permissions:

def add_view_perms_to_group(instance):
    # one time use
    group = Group.objects.get(name='viewers')
    assign_perm('view_articles', group, instance)
    return group

And we are done. We can still use the Django provided has_perm. We just have to pass the object as well to check for object level permissions. You can see this in the views.py above.

REST Framework Examples

from rest_framework.permissions import DjangoObjectPermissions

class ArticlesList(generics.ListCreateAPIView):
    queryset = Articles.objects.all()
    serializer_class = ArticlesSerializer
    permission_classes = (DjangoObjectPermissions,)

class ArticlesDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Articles.objects.all()
    serializer_class = ArticlesSerializer
    permission_classes = (DjangoObjectPermissions,)

Here serializer class is where serialization and deserialization occurs. But the permissions are checked in the view.
Here we are passing DjangoObjectPermissions which are default permissions provided by DRF out of the box. If we want to customize the permissions, it is this that we will change. As we will see later.

From DRF documentation for Permissions https://www.django-rest-framework.org/api-guide/permissions/

How permissions are determined
Permissions in REST framework are always defined as a list of permission classes.

Before running the main body of the view each permission in the list is checked. If any permission check fails an exceptions.PermissionDenied or exceptions.NotAuthenticated exception will be raised, and the main body of the view will not run.

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.
The request was not successfully authenticated, and the highest priority authentication class does not use WWW-Authenticate headers. — An HTTP 403 Forbidden response will be returned.
The request was not successfully authenticated, and the highest priority authentication class does use WWW-Authenticate headers. — An HTTP 401 Unauthorized response, with an appropriate WWW-Authenticate header will be returned.

So, as we see in the view.py code, all the permission classes in the permissions_classes are checked before loading the body of the view. If any of the permissions return a permission denied or an exception, the access is not given to the view.

The permissions that DRF gives you for free, out of the box, are:
Permissions Classes:

  • AllowAny
  • IsAuthenticated
  • IsAdminUser
  • IsAuthenticatedOrReadOnly
  • DjangoModelPermissions
  • DjangoModelPermissionsOfAnonReadOnly
  • DjangoObjectPermissions

Note, here IsAdminUser is not the same as IsSuperUser. IsAdminUser corresponds to the IsStaff field in the Django user model. Also, IsAuthenticated is same as IsAuthenticatedOrReadOnly except that in the latter case, the logged out users have view permission.

DjangoModelPermissions is where it starts to get interesting. DjangoModelPermissions is where it starts to use the view permission that we assigned to the model (using django-guardian) and also the add permission, change permission and the delete permission that Django gives for free.

DjangoObjectPermissions check if a user has object level permissions. This is IN ADDITION TO CHECKING if user has DjangoModelPermissions. So, you have to pass both. If you only assign users DjangoObjectPermissions, the users will not be able to access the things that they are being assigned to because BOTH DjangoObjectPermissions AND DjangoModelPermissions are checked before giving access.

All of these permissions classes inherit from the BasePermission class that DRF also provides. It looks like this:

class BasePermission(metaclass=BasePermissionMetaclass):
    """
    A base class from which all permission classes should inherit.
    """

    def has_permission(self, request, view):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

    def has_object_permission(self, request, view, obj):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

BasePermission class has only two methods. has_perm and has_object_perm. By default, they both return True. But you can extend this, or any of the other permissions that DRF gives you to do custom permissioning. And if you are going after an object, as long as these two things return true, you will have access to that object. If either of them returns false (has_permission and has_object_permission), you won’t.

In our Article example, we could have just used these DRF provided permissions but there is one thing that will keep us from using any of these permission classes. We need that can view permission that Django does not give us for free. We had to add it later. So we will have to add a custom permission for our view.
FYI, this is the DjangoObjectPermission class. YOU WILL NEED DJANGO-GUARDIAN TO USE THIS CLASS:

class DjangoObjectPermissions(DjangoModelPermissions):
    """
    The request is authenticated using Django's object-level permissions.
    It requires an object-permissions-enabled backend, such as Django Guardian.
    It ensures that the user is authenticated, and has the appropriate
    `add`/`change`/`delete` permissions on the object using .has_perms.
    This permission can only be applied against view classes that
    provide a `.queryset` attribute.
    """
    perms_map = {
        'GET': [],
        'OPTIONS': [],
        'HEAD': [],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }

    def get_required_object_permissions(self, method, model_cls):
        kwargs = {
            'app_label': model_cls._meta.app_label,
            'model_name': model_cls._meta.model_name
        }

        if method not in self.perms_map:
            raise exceptions.MethodNotAllowed(method)

        return [perm % kwargs for perm in self.perms_map[method]]

    def has_object_permission(self, request, view, obj):
        # authentication checks have already executed via has_permission
        queryset = self._queryset(view)
        model_cls = queryset.model
        user = request.user

        perms = self.get_required_object_permissions(request.method, model_cls)

        if not user.has_perms(perms, obj):
            # If the user does not have permissions we need to determine if
            # they have read permissions to see 403, or not, and simply see
            # a 404 response.

            if request.method in SAFE_METHODS:
                # Read permissions already checked and failed, no need
                # to make another lookup.
                raise Http404

            read_perms = self.get_required_object_permissions('GET', model_cls)
            if not user.has_perms(read_perms, obj):
                raise Http404

            # Has read permissions.
            return False

        return True

The interesting thing here is that if you look at the perms_map, note the add, change, delete. These are the three permissions given for free by Django. We need to update this perms_map so that is also has view perms for GET, OPTIONS and HEAD.

So, lets do that here to make our CustomObjectPermissions:

from rest_framework.permissions import DjangoObjectPermissions

class CustomObjectPermissions(DjangoObjectPermissions):

    SAFE_METHODS = ()

    perms_map = {
        'GET': ['%(app_label)s.view_%(model_name)s'],
        'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
        'HEAD': ['%(app_label)s.view_%(model_name)s'],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }

First of all, we are going to make sure that none of the methods come out as SAFE_METHODS (by default SAFE_METHODS = (‘GET’, ‘HEAD’, ‘OPTIONS’)). And we subclassed from DjangoObjectPermissions class.

So our DRF views.py with custom permissions (for object level view permissions) will look like this:

from my_app.permissions import CustomObjectPermissions

class ArticlesList(generics.ListCreateAPIView):
    queryset = Articles.objects.all()
    serializer_class = ArticlesSerializer
    permission_classes = (CustomObjectPermissions,)

class ArticlesDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Articles.objects.all()
    serializer_class = ArticlesSerializer
    permission_classes = (CustomObjectPermissions,)

And now the custom permissions will work in the two class based views. We can assign the permissions on an object level and assign permissions granularly based on the type of request, i.e. based on whether its a add, get(view), change or delete.

In Conclusion

Permissioning can be complicated but can be simple if we take the time to understand it. Its much easier if we stick to the two rules:

Rule No. 1: Permissions, while related to tasks, should be kept separate from tasks.

Rule No. 2: You can assign permissions to users or to groups, but you probably shouldn’t do both.

Django, DRF and django-guardian give you everything you need if you are in a Django project.

Next Steps

So, if you want to implement permissioning in your projects, here are the next steps you can do:

  1. Start separating your permissioioning logic from your task logic
  2. Decide if you want to implement permissions directly on user objects or indirectly using groups. This is an important decision and is not something you can easily go back and forth
  3. Use classes for your permissions logic so that they’re more reusable and extendable
  4. Implement object level permissioning if/when you need to



No Comments


You can leave the first : )



Leave a Reply

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