Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 40

Django Permissions: Implementing Authorization

Recommended resources

Hello! Welcome back to our backend engineering course.

In the last lesson, we successfully implemented a stateless JWT authentication system. We now have a robust way to answer the question, "Who is this user?". This is authentication. Today, we address the next critical question: "What is this user allowed to do?". This is authorization.

This lesson will introduce you to Django's built-in permission system, a powerful and straightforward way to control access to different parts of your application. By the end of this lesson, you will be able to implement fine-grained access control for your models, views, and templates.

1. From Authentication to Authorization

First, let's solidify the distinction we touched on previously.

  • Authentication (Who are you?): The process of verifying a user's identity, which we now handle with JWTs.
  • Authorization (What can you do?): The process of determining if an authenticated user has the necessary privileges to perform an action or access a resource.

Django has a built-in authorization framework that is automatically enabled when you include django.contrib.auth in your INSTALLED_APPS, which is done by default in new projects.

Permissions in Django

To start, let's read a quick primer that clarifies the difference between authentication and authorization from the article 'Permissions in Django' on TestDriven.io.

Read the brief section titled 'Authentication vs Authorization'. It provides a concise definition that sets the stage for this lesson.


2. Django's Default Permissions

A key feature of Django's authorization system is that it automatically creates a set of default permissions for every model in your application. For each model you define, Django creates four permissions upon running manage.py migrate:

  • add: The user can add new objects of this model.
  • change: The user can modify existing objects of this model.
  • delete: The user can delete objects of this model.
  • view: The user can see objects of this model.

These permissions follow a standard naming convention: <app_label>.<action>_<model_name>. For example, if you have a Post model in a blog app, the permission to view posts is named blog.view_post.

Under the Hood: The Database Schema

Given your interest in understanding the underlying mechanisms of the systems you build, let's look at how Django stores this information in the database. The authorization system relies on a few key tables:

  • auth_user: Stores your user accounts.
  • auth_permission: Stores all available permissions in the system, including the default ones for your models. Each permission is just a row in this table.
  • auth_group: Stores groups, which are used to manage permissions for multiple users at once (we'll cover this in depth in the next lesson).
  • auth_user_user_permissions: A many-to-many join table linking users directly to their permissions.
  • auth_group_permissions: A many-to-many join table linking groups to permissions.
  • auth_user_groups: A many-to-many join table linking users to groups.

To see how these tables relate, let's watch a short video segment and look at an Entity-Relationship Diagram (ERD).

Django Permissions | Model Level Permissions | Admin Site | Introduction | Part 1

The video 'Django Permissions | Introduction' by Very Academy provides an excellent visual walkthrough of the database tables involved in the permission system.

Watch from 09:41 to 15:00. The presenter does a great job of explaining how the auth_permission table works and how the many-to-many relationships connect users, groups, and permissions. This is a great look at the database schema.

Now, let's look at the full picture with an ERD.

Django Authentication and Authorization Database Schema ERD
This ERD shows the database schema for Django's authentication and authorization system. You can clearly see the `auth_user` table and its many-to-many relationships with `auth_group` and `auth_permission` through their respective join tables.

Understanding this structure is key to grasping how Django's role-based access control is built upon standard relational database principles.


3. Assigning and Checking Permissions

Now that we know permissions exist, how do we use them? The most straightforward way to manage permissions for a user is through the Django admin site.

Django Permissions | Model Level Permissions | Admin Site | Introduction | Part 1

Let's continue with the Very Academy video. It demonstrates how to assign specific permissions to a user and see the immediate effect in the admin interface.

Watch from 17:06 to 21:46. Follow the steps shown to log in as a superuser, select a non-superuser, and assign them the 'view user' permission. Note how this changes what the user can see when they log into the admin.

While the admin is great for manual assignment, the real power comes from programmatically checking permissions in your code. The core method for this is user.has_perm().

# Check if a user has a single permission
if request.user.has_perm('blog.view_post'):
    # The user can view posts
    ...

# You can also check for multiple permissions
# Note: has_perms requires all permissions in the list to be present.
if request.user.has_perms(['blog.add_post', 'blog.change_post']):
    # The user can add and change posts
    ...

Superusers, by definition, will always have all permissions, so user.has_perm() will always return True for them.

Test your understanding!

Imagine you have a Django project with an app named inventory containing a model named Product. A user, jane_doe, needs to be able to add new products but should not be able to delete them.

  1. What is the string name for the permission required to add a product?
  2. What Python code would you use to check if jane_doe (represented by the user object) has this permission?
Show answer
  1. The permission name is 'inventory.add_product'.
  2. The code to check for this permission is user.has_perm('inventory.add_product').

4. Enforcing Permissions in Views and Templates

Knowing a user has a permission is useful, but we need to enforce it by restricting access. Django provides tools to do this at both the view and template level.

In Function-Based Views (FBVs)

For FBVs, you can use the @permission_required decorator. It checks for a permission and, if the user doesn't have it, redirects them (usually to the login page).

Django Permissions | Model Level | Views and Templates

The video 'Django Permissions | Views and Templates' by Very Academy demonstrates how to use decorators to protect your views.

Watch from 08:54 to 12:00. This section clearly shows how to apply the @permission_required decorator to a view function to restrict access based on a specific permission.

Here's a code example:

from django.contrib.auth.decorators import permission_required

@permission_required('inventory.view_product', raise_exception=True)
def product_list(request):
    # ... view logic here ...

By setting raise_exception=True, Django will return a 403 Forbidden response instead of redirecting, which is often more appropriate for APIs.

In Class-Based Views (CBVs)

For CBVs, which are common in DRF, you use the PermissionRequiredMixin.

Using the Django authentication system

The official Django documentation provides a clear guide on using mixins for permission checks in class-based views. This is the standard approach for CBVs and DRF.

Read the subsection 'The PermissionRequiredMixin mixin'. It shows how to add the mixin to a class and specify the permission_required attribute.

Here's how you'd apply it to a DRF view:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.contrib.auth.mixins import PermissionRequiredMixin

class ProductStatsView(PermissionRequiredMixin, APIView):
    permission_required = 'inventory.view_product_stats'  # A custom permission
    permission_classes = [IsAuthenticated] # Ensures user is logged in first

    def get(self, request, format=None):
        # This code will only run if the user is authenticated AND
        # has the 'inventory.view_product_stats' permission.
        stats = {"total_products": 150, "in_stock": 120}
        return Response(stats)

In Templates

A good user experience involves hiding UI elements that a user doesn't have permission to use. Django makes this easy with the perms template variable, which is available in your templates by default.

Django Permissions | Model Level | Views and Templates

Let's return to the Very Academy video to see a practical demonstration of hiding a button based on user permissions.

Watch from 19:42 to 22:15. This part shows how to wrap a button in an {% if %} block that checks perms to conditionally render it.

The template code looks like this:

{% if perms.inventory.add_product %}
    <a href="{% url 'product-add' %}" class="btn btn-primary">Add New Product</a>
{% endif %}

This ensures that only users with the add_product permission will even see the button to add a new product.


5. Custom Permissions and Groups

While the four default permissions are useful, you'll often need more specific ones (like view_product_stats in the example above). You can define custom permissions directly in your model's Meta class.

Permissions in Django

Let's read the 'Model-level Permissions' section from the TestDriven.io article. It shows how simple it is to add your own permissions to a model.

Read the section 'Model-level Permissions'. Focus on the syntax used within the class Meta to define a new permission tuple.

After adding a permission like this and running makemigrations and migrate, it becomes available just like any default permission.

class Product(models.Model):
    # ... fields ...
    class Meta:
        permissions = [
            ("view_product_stats", "Can view product statistics"),
        ]

The Next Step: Groups

Assigning permissions one-by-one to hundreds of users is not scalable. The solution is to use Groups. You can create a group (e.g., "Editors"), assign a set of permissions to that group, and then simply add users to the "Editors" group. Any user in the group automatically inherits all of its permissions. This creates a clean, maintainable, role-based access control (RBAC) system.


Conclusion

Today you've learned the fundamentals of Django's built-in authorization system. You now have the tools to control what authenticated users can do within your application.

Key Takeaways:

  • Django automatically creates add, change, delete, and view permissions for every model.
  • Permissions can be assigned to users via the Django admin or programmatically.
  • You can check for permissions using user.has_perm('<app_label>.<permission_name>').
  • Access to function-based views is controlled with the @permission_required decorator.
  • Access to class-based views is controlled with the PermissionRequiredMixin.
  • UI elements can be conditionally displayed in templates using the perms object (e.g., {% if perms.app.can_do_thing %}).

Preview of the Next Lesson:
We've seen how to assign permissions to individual users, but this approach doesn't scale well. In our next lesson, we will focus entirely on using Groups to create well-defined user roles. This will allow us to build a powerful and easy-to-manage role-based access control system for our application.