Skip to content
_lvleo21
← All articles

How do you control access by role with Django Role Permissions?

Role-based access control with django-role-permissions: roles, assigning them to users, checks in views and templates, object-level permissions and Django REST Framework integration.

  • django
  • python

I first published this article on Medium (in Portuguese) in May 2020. In this version I reviewed the code, fixed two mistakes in the template example and added view protection, object-level permissions and Django REST Framework integration. The examples were tested with Django 6.1, Python 3.13 and django-role-permissions 3.2.

As a Django project grows, you need to control what each user can see and access. Picture someone opening your personal data on your bank’s website. django-role-permissions handles this by organizing permissions by role: you define the system’s roles, say which permissions each one has and then ask, in code or in the template, whether the user can do a given thing.

Under the hood, the library uses Django’s own authentication system. Each role becomes a Group and each permission becomes a Permission, so there are no new tables to migrate.

Installation and setup

pip install django-role-permissions

In settings.py, add the app:

INSTALLED_APPS = [
    # ...
    "rolepermissions",
]

Then create a roles.py file in the project folder, the same one as settings.py, and point to it in the settings. In the examples the project is called school:

ROLEPERMISSIONS_MODULE = "school.roles"

Defining the roles

The example is a school grading system with two kinds of user, teacher and student. A student can only see their own standing, while a teacher sees everyone’s. Let’s call these permissions look_situation and look_all_situations.

In roles.py, each role is a class that inherits from AbstractUserRole, and the available_permissions attribute lists that role’s permissions:

# school/roles.py
from rolepermissions.roles import AbstractUserRole


class Teacher(AbstractUserRole):
    available_permissions = {
        "look_all_situations": True,
    }


class Student(AbstractUserRole):
    available_permissions = {
        "look_situation": True,
    }

True means the user gets the permission as soon as they get the role. A permission set to False is available to the role, but only takes effect when you grant it with grant_permission(user, "permission_name"). That is useful for permissions that only some users of that role should have.

If you change roles.py when users are already registered, the python manage.py sync_roles command creates the missing groups in the database.

Assigning roles to users

To say which role a user belongs to, use assign_role, which takes the user and the role name:

from django.contrib.auth import get_user_model
from rolepermissions.roles import assign_role

User = get_user_model()

teacher = User.objects.get(username="teacher")
assign_role(teacher, "teacher")

student = User.objects.get(username="student")
assign_role(student, "student")

The role name is the class name in snake_case. Teacher becomes "teacher", and a PedagogicalCoordinator class would become "pedagogical_coordinator". If you prefer another name, set the role_name attribute on the class. To take a role away, there is remove_role(user, "teacher").

In the original I fetched the users with user.objects.get. The right way is to get the model with get_user_model(), which also works when the project uses a custom user model.

Checking permissions in the backend

The most common check is has_permission, which takes the user and the permission name:

from rolepermissions.checkers import has_permission, has_role

has_permission(student, "look_all_situations")  # False
has_permission(teacher, "look_all_situations")  # True

has_role(student, "student")  # True
has_role(student, ["student", "teacher"])  # True if they have any of the roles

The first result is False because the question is “does the student have the look_all_situations permission?”, and that permission belongs only to the teacher. What to do with the answer depends on the business rule: an if, a different filter on the queryset, an error.

Superusers pass every check by default. If you don’t want that, set ROLEPERMISSIONS_SUPERUSER_SUPERPOWERS = False in the settings.

Protecting views

Most of the time, the check is there to block a whole view. For that, the library ships mixins for class-based views and decorators for function-based views:

from django.views.generic import ListView
from rolepermissions.decorators import has_permission_decorator
from rolepermissions.mixins import HasPermissionsMixin

from .models import Situation


class AllSituationsView(HasPermissionsMixin, ListView):
    required_permission = "look_all_situations"
    model = Situation


@has_permission_decorator("look_all_situations")
def all_situations(request):
    ...

The mixin must come before ListView in the inheritance. When the user lacks the permission, or is not logged in, the view responds with 403. To send the user to the login page instead, use redirect_to_login = True on the class or ROLEPERMISSIONS_REDIRECT_TO_LOGIN = True in the settings. To check roles instead of permissions, there are HasRoleMixin, with the allowed_roles attribute, and has_role_decorator.

Checking in the template

In the HTML, load the library’s template tags at the top of the file:

{% load permission_tags %}

The has_role filter asks whether the user has any of the given roles:

{% if request.user|has_role:"student,teacher" %}
  <h1>Students and teachers can see this heading</h1>
{% endif %}

The original example had two mistakes here. The space after the colon (has_role: "...") makes Django raise TemplateSyntaxError. And the space after the comma ("student, teacher") leaves the teacher out, because the filter splits the roles on the comma without stripping spaces and looks for a role called " teacher", which does not exist. Write the list without spaces.

The can filter asks about the permission:

{% if request.user|can:"look_all_situations" %}
  <h1>The teacher can see every student and their grades</h1>
{% endif %}

Object-level permissions

The rule “a student cannot see their classmates’ grades” is not solved by look_situation alone. Every student has that permission; what changes is which standing we are talking about. For rules that depend on the object, the library has object checkers.

Create a permissions.py inside the app. The library loads this file automatically in every app in INSTALLED_APPS:

# grades/permissions.py
from rolepermissions.permissions import register_object_checker

from school.roles import Teacher


@register_object_checker()
def view_situation(role, user, situation):
    if role == Teacher:
        return True
    return situation.student == user

The checker receives the user’s role (the class), the user and the object. In the backend it is called by the function name:

from rolepermissions.checkers import has_object_permission

has_object_permission("view_situation", student, own_situation)  # True
has_object_permission("view_situation", student, classmate_situation)  # False
has_object_permission("view_situation", teacher, classmate_situation)  # True

In the template, can also works as a tag for this case:

{% for situation in situations %}
  {% can "view_situation" situation user=request.user as can_view %}
  {% if can_view %}
    <p>{{ situation.student }}: {{ situation.grade }}</p>
  {% endif %}
{% endfor %}

Hiding items in the template is no substitute for filtering the queryset. In a listing, the student should only get their own standings (Situation.objects.filter(student=request.user)), and the checker is for detail pages and for places where the object is already loaded.

Using it with Django REST Framework

The library has no DRF integration of its own, but its check functions fit straight into a permission class:

# grades/api.py
from rest_framework.permissions import BasePermission
from rolepermissions import checkers


class SituationPermission(BasePermission):
    def has_permission(self, request, view):
        return checkers.has_permission(
            request.user, "look_situation"
        ) or checkers.has_permission(request.user, "look_all_situations")

    def has_object_permission(self, request, view, obj):
        return checkers.has_object_permission("view_situation", request.user, obj)

Importing the checkers module, instead of the bare functions, avoids mixing up the library’s has_permission with the class method of the same name. With permission_classes = [SituationPermission] on a ModelViewSet, the student gets a 200 when opening their own standing and a 403 when opening a classmate’s.

Maintenance and built-in alternatives

The latest release of django-role-permissions, 3.2.0, came out in June 2023. It still works with Django 6.1, but it is worth keeping in mind before adopting the library in a new project.

Since roles are groups and permissions are Django Permission objects, the built-in tools also see what the library creates. The permissions are tied to the user model, so teacher.has_perm("auth.look_all_situations") returns True, and {% if perms.auth.look_all_situations %} works in the template. If the project only needs a few fixed groups, you can use Group, PermissionRequiredMixin and @permission_required directly, with no extra dependency. The library pays off when you want to declare roles in code and have object checkers ready to go.

References