Managing User Permissions in Django

honeybadger_staff

Honeybadger Staff

Posted on September 28, 2022

Managing User Permissions in Django

This article was originally written by Muhammed Ali on the Honeybadger Developer Blog.

All applications need some sort of check or permission that controls what each user can do and determine whether the user is authenticated.
In this article, you will learn how to set up multiple user roles, as well as how user permissions can be set depending on the user role. Furthermore, I will illustrate how to use Django’s built-in Users and Groups to apply permissions to multiple users.
While a lot of articles only cover user permissions and the default Django user permissions, I will take it a step further by building an application with different user roles and then set permissions based on the role of the user.

Prerequisites

To follow this article, the following are required:

  • Prior working knowledge of Django and Python
  • Python 3.7 or higher versions installed

Working with Django’s Built-in Permissions

By default, Django provides a built-in authorization (permissions). This feature enables you to define what actions each user is permitted to perform. The framework includes built-in models for Users and Groups, which are used to provide permissions to a group of users, permissions/flags that designate whether a user may perform a task, view tools for restricting content, etc. When django.contrib.auth is listed in your INSTALLED_APPS, it will provide the four default permissions – add, change, delete, and the view for each model.
Now, let's assume you have a project with an app product and a model Order. To test which users have basic permissions, you can use the following code.

python manage.py shell
user = User.objects.first()
Enter fullscreen mode Exit fullscreen mode
  • add: user.has_perm('product.add_order')
  • change: user.has_perm('product.change_order')
  • delete: user.has_perm('product.delete_order')
  • view: user.has_perm('product.view_order') Adding permissions to restrict a function to only users that have that particular permission can be done by using a Django built-in decorator, permission_required.
from django.contrib.auth.decorators import permission_required

@permission_required('product.change_name')
def admin_view(request):
    """Raise permission denied exception or redirect user"""
Enter fullscreen mode Exit fullscreen mode

If you are using a class-based view, you just need to use a mixin, PermissionRequiredMixin:

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView

class ProductListView(PermissionRequiredMixin, ListView):
    permission_required = 'product.change_name'
Enter fullscreen mode Exit fullscreen mode

How to add Custom Permissions to Models

If permissions are not stated in the views, you can specify them in your models. Attaching permissions is done on the model's class Meta using the permissions field. You will be able to specify as many permissions as you need, but it must be in a tuple. For example, you might define a permission to allow a user to change the name of a product like this:

from django.db import models

class Product(models.Model):
    user = models.ForeignKey(User)    
    class Meta:
        permissions = (("change_name", "can change name of product"),)
Enter fullscreen mode Exit fullscreen mode

To implement the changes, run python manage.py makemigrations and migrate.

How to Add Custom Permissions to Templates

The user's permissions can be retrieved by calling {{ perms }} in the template. To check if a particular user has permission to view the template, all you have to do is get perms from the particular model holding the permissions. It should look like this:

{{ perms.<name_of_app>.<permission>}} e.g., {{ perms.app.change_name }} .
With an if statement, you can add a permission to specific portions of your template.

{% if perms.app.change_name %}
<p>can change name of product</p>

{% endif %}
Enter fullscreen mode Exit fullscreen mode

By doing this, only users with change_name permission can use that particular section of the template.

How to add Custom Permissions to Views

Just like when you were working with the built-in Django permissions, you can also add custom permissions to views by using the permission_required decorator or, in a class-based view, using the PermissionRequiredMixin. For function-based views:

from django.contrib.auth.decorators import permission_required

@permission_required('app.change_name')
def the_view(request):
    ...
Enter fullscreen mode Exit fullscreen mode

For class-based views:

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'app.change_name'
    # Or multiple permissions
    permission_required = ('app.change_name', 'app.edit_name')
    # Note that 'catalog.can_edit' is just an example, you can replace it with
    # whatever permissions you have created

Enter fullscreen mode Exit fullscreen mode

Note

If a user is not permitted to access a view and they try to access it, PermissionRequiredMixin and @permission_required act differently. @permission_required redirects to the login screen, PermissionRequiredMixin returns HTTP 403 (Status Forbidden) if a user is logged in but does not have the correct permission.

The PermissionRequiredMixin default behavior is optimal when building apps. To duplicate this behavior in function-based views, you will have to add the @login_required decorator and apply @permission_required with raise_exception=True. This is how it will look:

from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required('app.change_name', raise_exception=True)
def the_view(request):
    ...
Enter fullscreen mode Exit fullscreen mode

Add Permissions to Multiple User Types

Usually, when you are implementing multiple user types, you'll need to make sure each user type has some restrictions. For example, if you are building an assignment application where the teacher can set questions and students can only answer questions. In this case, you must set the permission system in such a way that students will not have the option to set questions. To illustrate this, let's assume you are trying to build an application where an authenticated teacher can create assignments and authenticated students can answer questions.
Assuming our models.py file looks like this:

from django.contrib.auth.models import AbstractUser
from django.db import models

**class** **User**(AbstractUser):
    is_student **=** models**.**BooleanField(default**=**False)
    is_teacher **=** models**.**BooleanField(default**=**False)

**class** **Student**(models**.**Model):
    user **=** models**.**OneToOneField(User, on_delete**=**models**.**CASCADE, primary_key**=**True)
    subjects **=** models**.**ManyToManyField(Subject, related_name**=**'subject')
Enter fullscreen mode Exit fullscreen mode

To handle permissions for these different users, you need to create a custom decorator of your own.

# decorator.py

from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import user_passes_test

def for_students(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url='login'):
    '''
    A decorator to check logged in user could be a student and redirects to the login page if the user isn't authenticated.
    '''
    actual_decorator = user_passes_test(
        lambda u: u.is_active and u.is_student,
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

def for_teachers(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url='login'):
    '''
    A decorator to check whether the logged-in user is a teacher and 
    redirect to the login page if the user is not authenticated.
    '''
    actual_decorator = user_passes_test(
        lambda u: u.is_active and u.is_teacher,
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator-
Enter fullscreen mode Exit fullscreen mode

Now, to use them in the views, all you need to do is call it as you did previously. It should look like this:

# views.py 
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from .decorators import for_students, for_teachers
from .models import Assignment #this is an assumption

@login_required
@student_required  # <-- here is the decorator
def answer_assignment(request, pk):
    quiz = get_object_or_404(Assignment, pk=pk)
    student = request.user.student

    # body of the view...

Enter fullscreen mode Exit fullscreen mode

If you are using class-based views, your view will look like this:

from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from .decorators import for_students

@method_decorator([login_required, student_required], name='dispatch')
class AnswerAssignment(CreateView):

    # body of the view...
Enter fullscreen mode Exit fullscreen mode

Add Permissions to a Group

Thankfully, Django makes it very easy to create groups with the admin panel provided by default.
This section will contain an illustration of how you can potentially add permissions to groups in your application. Let's assume you are building a website that provides real estate suggestions, and if a user is subscribed to "Tier 1", they can view 3 suggestions, and "Tier 2" can view 6 suggestions.
With the above in mind, your models.py file will look like this:

from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    first_name = models.CharField(blank = True, max_length = 20)                      
    last_name = models.CharField(blank = True, max_length = 20)                      
    # You can add more User fields as required

    # define the custom permissions
    # related to User.
    class Meta:
        permissions = (("tier_1", "Can view 3 suggestions"), ("tier_2", "Can view 6 suggestions"),
        # You can add other custom permissions as required
        )

Enter fullscreen mode Exit fullscreen mode

If you are using AbstractUser in Django, you must add AUTH_USER_MODEL = 'YourAppName.YourClassName' . This way, you are telling Django to use our custom user model instead of the default one.
The code below should go in your admin.py file so that you can see your user model.

from django.contrib import admin
from .models import User

admin.site.register(User)
Enter fullscreen mode Exit fullscreen mode

To implement the changes, run $ python manage.py makemigrations and $ python manage.py migrate.
Next, you will create a superuser that you will use to create the groups.
To do this, run $ python3 manage.py createsuperuser and fill in the necessary details.
Now, run your server with $ python manage.py runserver and head to the admin and login with the user details you just created.
This is what you should see after logging in:
Image of admin page
What you will do next is click on the "+ Add" button on the "Groups" row.
You will be prompted with a form that looks like this:

Image of group form

You will see that you can select various permissions and attach them to a particular group.

Conclusion

This article's aim was to take you from not knowing how to manage permissions in Django applications to showing you the different ways in which you can manipulate and manage permissions when building Django applications. Hopefully, I was able to do that. With this article, you should have learned how to add permissions to views, models, and templates. I also showed how you can potentially create your own custom permission. Finally, I was able to show how to add permissions to groups using the Django admin panel. Now that you have learned all these, hopefully you will be able to start implementing the knowledge in your future Django projects.

💖 💪 🙅 🚩
honeybadger_staff
Honeybadger Staff

Posted on September 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related