Django Authorization: An Implementation Guide
- Share:
Django is a powerful framework for building web applications, widely used for everything from content management systems to complex data-driven platforms. A key aspect of many Django applications is managing who can access what, ensuring that users only interact with resources they are authorized to use.
By default, Django provides a basic role-based permission system, where users are assigned roles such as admin, editor, or viewer. While this works for simple applications, it’s just not enough for most production-ready applications today.
To handle these challenges, a more fine-grained authorization approach is required. Instead of relying solely on predefined roles, we need to consider both user relationships and dynamic attributes when determining access.
This guide explores how to implement Relationship-Based Access Control (ReBAC) and Attribute-Based Access Control (ABAC) - more advanced and fine-grained authorization models in Django by using Permit.io. We’ll cover how to:
- Set up permissions that depend on user relationships and the resources they access.
- Create attribute-based rules considering contexts like time, location, and resource state.
- Combine ReBAC and ABAC to build a more thorough access control system.
We will do this by building an E-Learning demo application, where access to courses is determined by relationships (like instructor, student, or owner) and different factors such as course level, enrollment status, and progress.
Before we get into it, let’s cover some basic concepts -
What are ReBAC and ABAC?
Relationship-Based Access Control (ReBAC)
ReBAC extends RBAC by considering relationships between identities and resources. The consideration of these relationships allows us to create authorization policies for hierarchical structures.
It is easiest to visualize ReBAC as a graph, with each node on the graph representing a resource or identity and each edge representing a relationship.
Graph-based authorization systems are perfect for mapping hierarchies and nested relationships. Because they can manage high volumes of data while maintaining consistency, these systems also prove effective in large-scale environments.
Think of any file-sharing system. Suppose you create a folder and share it with someone:
- If you grant them Editor access to the folder, they automatically get edit access to all files inside it.
- If they share a specific file from that folder with another person as Viewer, that new person can only view that file—not the entire folder.
- If you later remove your colleague’s access to the folder, they lose access to all files within it.
This approach provides fine-grained access control, ensuring users have the right level of access based on their role within a specific context rather than a broad, system-wide role assignment.
Attribute-Based Access Control (ABAC)
ABAC extends traditional role-based access control by evaluating attributes—properties of users, resources, and the environment—when making authorization decisions. Instead of relying solely on predefined roles, ABAC allows for more fine-grained control by considering multiple attributes.
Each access request is evaluated based on a set of attributes, which can include:
- User attributes (e.g., job title, subscription level, course progress)
- Resource attributes (e.g., course difficulty, content type, geographic restrictions)
- Environmental attributes (e.g., time of day, location, device type)
Because ABAC enforces policies based on multiple attributes rather than static roles, it ensures granular control over access permissions.
With a basic understanding of these two concepts, let’s dive into the implementation.
Setting Up the E-Learning Platform
To get right into the practical part of this tutorial, we have set up a starter project for the demonstrations. First, clone the starter project by running the following command:
git clone <https://github.com/icode247/elearning_platform>
cd elearning_platform
Then create a new virtual environment and install dependencies with the following commands:
python -m venv env
source env/bin/activate # On Windows: env\\Scripts\\activate
pip install -r requirements.txt
Preparing our Authorization Implementation
Before writing any application code, let's configure our authorization model in Permit.io. We'll set up both relationship-based (ReBAC) and attribute-based (ABAC) policies using the Permit.io dashboard.
Setup a New Project and Credentials
To get started, let's set up a new project and get credentials to connect Permit SDK to our project. To do that, follow the steps:
- Create a Permit.io account if you haven't already
- Create a new project named
elearning
in the dashboard - Select your preferred environment (Development/Production) and copy your API Key
Configuring ReBAC
Before we dive into configuring Permit.io, let’s take a moment to understand our ReBAC model structure and how it applies to our e-learning platform.
Resources
In our platform, we’ll focus on two main types of resources:
- Course: The core learning resource that students can enroll in.
- CourseSection: The individual components of a course, like modules or chapters.
Actions
Each resource comes with specific actions that users can perform:
For Course:
view
: See course details and content.edit
: Modify course information.delete
: Remove the course.enroll
: Join the course as a student.
For CourseSection:
view
: Access section content.edit
: Modify section information.
Resource Roles
We’ll define different roles to determine what level of access a user has:
- Owner: Full control over the resource (typically the course creator).
- Teaching Assistant: Limited administrative permissions.
- Student: Can view content and participate in the course.
Relationships
Since our platform follows a hierarchical structure, we’ll define relationships that shape access:
- A Course can have multiple CourseSections (parent-child relationship).
- A user’s role on a Course also impacts their access to related CourseSections.
Role Derivations
To keep things streamlined, we’ll set up automatic role inheritance:
- A Course Owner is automatically the owner of all associated CourseSections.
- A Teaching Assistant for a course also gets the same role for its sections.
This ReBAC model creates a graph-like permission structure where access propagates from courses to their sections, mimicking the natural hierarchy of an educational platform.
Let’s proceed to setting our ReBAC rules in Permit.io. We’ll do that using the following steps below:
Step 1: Setting Up Resources and Actions
First, let's define all our resources and their relationships. For ReBAC, we need to create all the necessary resources before we can establish relationships between them. Follow the steps below to create your ReBAC policy:
Navigate to Policy → Resources
Click on Add Resource and create these resources in order:
Create a Course resource with
view
,edit
,delete
, andenroll
actions.Add a CourseSection resource with
view
, andedit
actions.
Step 2: Mapping Resource Roles
For each resource, we need to set up resource roles specific to that resource, under "ReBAC Options":
- Add
owner
,teaching_assistant
andstudent
resource roles to the Course and CourseSection resources: - Go to the Policy Editor tab, and check the boxes to set up the relevant permissions for each resource role.
Step 3: Setting Up Resource Relationships
Now define the relationships between Course and CourseSection resources. Open the Course resource to edit and under Relations, set up the following relationships:
Step 4: Setting Up Role Derivations
Now we'll configure how roles are derived based on parent-child relationships:
- Navigate to the 'Roles' tab
- Under 'ReBAC Options', set up the following derivation:
For Course Owner derivation: A user who is a course#owner
will also be a coursesection#owner
when
a course instance is the parent of a coursesection
instance.
For Teaching Assistant derivation: A user who is a course#teaching_assistant
will also be a coursesection#assistant
when
a course instance is the parent of a coursesection
instance.
This setup follows the parent-child relationship pattern where permissions flow from the parent resource (Course) to the child resource (CourseSection) based on the user's role on the parent.
Syncing ReBAC with Django
Now that we have configured our ReBAC policies in Permit.io, let's implement them in our Django application.
Step 1: Setting up local PDP container:
To run the Permit's ReBAC and ABAC policies, you need to set up your own local PDP container. Do that by running the command below:
docker pull permitio/pdp-v2:latest
docker run -it -p 7766:7000 \\
--env PDP_DEBUG=True \\
--env PDP_API_KEY=<YOUR_API_KEY> \\
permitio/pdp-v2:latest
If you do not have Docker installed yet, click here to install Docker. Because you need it for the above command.
Replace <YOUR_API_KEY>
in the above command with your Permit API Key.
Step 2: Initialize Permit Client
First, create a new file named permit_client.py
in your Django project root:
# elearning/permit_client.py
from permit import Permit
from dotenv import load_dotenv
import os
load_dotenv()
permit = Permit(
pdp=os.getenv("PERMIT_PDP_URL"),
token=os.getenv("PERMIT_SDK_KEY")
)
This code initializes the Permit SDK, which is our bridge between Django and Permit.io’s authorization services. We use environment variables for secure credential management, so our PDP URL and API key stay safe.
Then update the .env
file in the project root and add your Permit.io credentials:
PERMIT_PDP_URL=your_pdp_url
PERMIT_SDK_KEY=your_sdk_key
Step 3: Create Permission Middleware
Create a middleware.js
file in the courses directory to enforce our ReBAC policies:
# courses/middleware.py
from functools import wraps
from django.http import HttpResponseForbidden
from elearning_platform.permit_client import permit
from asgiref.sync import async_to_sync
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(view_instance, request, *args, **kwargs):
course_id = kwargs.get('pk')
if course_id:
try:
course = Course.objects.get(id=course_id)
except Course.DoesNotExist:
return HttpResponseForbidden("Course not found")
permitted = async_to_sync(permit.check)(
str(request.user.id),
action,
{
"type": "Course",
"instance": f"Course:{course.id}"
}
)
else:
permitted = async_to_sync(permit.check)(
str(request.user.id),
action,
{
"type": "Course",
"instance": "Course"
}
)
if not permitted:
return HttpResponseForbidden("Access denied")
return view_func(view_instance, request, *args, **kwargs)
return _wrapped_view
return decorator
Step 4: Update Views with ReBAC Permissions
Now update the views in the courses/views.py
file to use our permission middleware:
# courses/views.py
from rest_framework import viewsets, permissions
from rest_framework.response import Response
from rest_framework.decorators import action
from django.shortcuts import get_object_or_404
from django.db import models
from .models import Course, StudentEnrollment
from .serializers import CourseSerializer, CourseSectionSerializer, StudentEnrollmentSerializer
from .middleware import check_permit_permission
class CourseViewSet(viewsets.ViewSet):
serializer_class = CourseSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
if user.is_staff:
return Course.objects.all()
return Course.objects.filter(
models.Q(instructor=user) |
models.Q(studentenrollment__student=user)
).distinct()
@check_permit_permission(action="view")
def list(self, request):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
@check_permit_permission(action="view")
def retrieve(self, request, pk=None):
queryset = self.get_queryset()
course = get_object_or_404(queryset, pk=pk)
serializer = self.serializer_class(course)
return Response(serializer.data)
@check_permit_permission(action="edit")
def create(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
serializer.save(instructor=request.user)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
@check_permit_permission(action="edit")
def update(self, request, pk=None):
queryset = Course.objects.filter(instructor=request.user)
course = get_object_or_404(queryset, pk=pk)
serializer = self.serializer_class(course, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)
@check_permit_permission(action="delete")
def destroy(self, request, pk=None):
queryset = Course.objects.filter(instructor=request.user)
course = get_object_or_404(queryset, pk=pk)
course.delete()
return Response(status=204)
@check_permit_permission(action="enroll")
@action(detail=True, methods=['post'])
def enroll(self, request, pk=None):
course = get_object_or_404(Course, pk=pk)
enrollment, created = StudentEnrollment.objects.get_or_create(
student=request.user,
course=course
)
serializer = StudentEnrollmentSerializer(enrollment)
return Response(serializer.data, status=201 if created else 200)
Step 5: Sync Users and Roles
Create a new signal to sync users and their roles with Permit.io when they're assigned to courses or when a student enrolls in a course:
# courses/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from asgiref.sync import async_to_sync
from .models import Course, StudentEnrollment
from elearning_platform.permit_client import permit
@receiver(post_save, sender=Course)
def sync_course_to_permit(sender, instance, created, **kwargs):
if created:
try:
# First sync the instructor user
async_to_sync(permit.api.users.sync)({
"key": str(instance.instructor.id),
"email": instance.instructor.email,
"first_name": instance.instructor.first_name,
"last_name": instance.instructor.last_name
})
# Then assign the role
async_to_sync(permit.api.users.assign_role)({
"user": str(instance.instructor.id),
"role": "owner",
"resource_instance": f"Course:{instance.id}",
"tenant": "default"
})
except Exception as e:
print(f"Error syncing with Permit.io: {str(e)}")
@receiver(post_save, sender=StudentEnrollment)
def sync_enrollment_to_permit(sender, instance, created, **kwargs):
if created:
try:
# Sync the student user
async_to_sync(permit.api.users.sync)({
"key": str(instance.student.id),
"email": instance.student.email,
"first_name": instance.student.first_name,
"last_name": instance.student.last_name
})
# Assign student role
async_to_sync(permit.api.users.assign_role)({
"user": str(instance.student.id),
"role": "student",
"resource_instance": f"Course:{instance.course.id}",
"tenant": "default"
})
except Exception as e:
print(f"Error syncing student with Permit.io: {str(e)}")
Our signals.py
implementation makes sure that our authorization system remains in sync with Django’s database. The signals automatically update Permit.io’s permission graph when new courses are created or relationships change. It defines resource roles for instructors and keeps parent-child relationships between courses and their sections.
Update the apps.py
to register the signals:
# courses/apps.py
from django.apps import AppConfig
class CoursesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'courses'
def ready(self):
from . import signals
Configuring ABAC
To make our authorization system more dynamic, we’re layering ABAC (Attribute-Based Access Control) on top of ReBAC. This means permissions won’t just depend on relationships but also on real-time user and resource attributes.
User Attributes
We’ll track key details about each user to tailor their access:
- Level: Their academic level (e.g., "BEGINNER," "INTERMEDIATE," "ADVANCED").
- Region Restrictions: Where they’re allowed to access content from.
- Prerequisites Required: A simple true/false flag to check if they need to complete prerequisites.
- Progress: A numeric value (0-100) showing how much of a course they’ve completed.
- Location: Their current geographic location.
Resource Attributes
For each course, we’ll track:
- Level: The difficulty rating of the course.
- Region Restrictions: The geographic areas where the course is accessible.
Condition Sets
We’ll define two main condition sets to enforce rules:
- Prerequisites Check – Ensures users complete necessary prerequisites before accessing advanced content.
- Regional Access Control – Restricts course access based on geographic availability.
Policy Rules
Our access rules will take all these factors into account. For example, to view a course, a user must meet these conditions:
- Their level matches or exceeds the course level.
- They’ve completed any required prerequisites.
- Their progress is at least 75%.
- They’re in an allowed region for that course.
Similar rules will apply to enrolling in a course.
Now that we have an understanding of what our ABAC rule will do and how it will be implemented, let’s processed by actually configuring our ABAC rules in Permit.io in the following steps:
Step 1: Define User Attributes
Navigate to Directory → Users → Settings → User Attributes and add the following user attributes:
{
"level": {
"type": "string",
},
"region_restrictions": {
"type": "array",
},
"prerequisites_required": {
"type": "boolean"
},
"progress": {
"type": "number",
},
"location": {
"type": "string"
}
}
Step 2: Creating Condition Sets
Now let's configure the condition sets with the following steps:
- Navigate to Policy → ABAC Rules
- Click "Create Condition Sets"
- Create the following sets for course access control:
For prerequisite requirements:
For regional access:
Step 3: Create a New Policy
Now that we have our resources, user attributes, and ABAC rules defined:
- Navigate to the Policy tab
- You'll see a policy created for the course resource
- Click the dropdown for the course resource
- For each action we defined earlier (view, edit, delete, enroll), we can now specify access based on our ABAC rules:
- For the
view
action:- Allow when user's level matches the course level
- Allow when user has completed prerequisites (prerequisites_required = true)
- Allow when user's progress is >= 75
- Allow when user's location is in course's region_restrictions
- For the
enroll
action enforce the same conditions as view
Implementing ABAC in Django
First, update the courses/middleware.py
file to create Permission Middleware:
# courses/middleware.py
def check_course_attributes(action):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(view_instance, request, *args, **kwargs):
course_id = kwargs.get('pk')
course = Course.objects.get(id=course_id)
enrollment = StudentEnrollment.objects.filter(
student=request.user,
course=course
).first()
context = {
"user": {
"key": str(request.user.id),
"attributes": {
"level": course.level,
"progress": enrollment.progress if enrollment else 0,
"prerequisites_required": course.prerequisites.exists(),
"location": request.headers.get('X-User-Location')
}
},
"action": action,
"resource": {
"type": "Course",
"instance": f"Course:{course.id}",
"attributes": {
"level": course.level,
"region_restrictions": course.region_restrictions
}
}
}
permitted = async_to_sync(permit.check)(**context)
if not permitted:
return HttpResponseForbidden("Access denied")
return view_func(view_instance, request, *args, **kwargs)
return _wrapped_view
return decorator
Our middleware.py
implements two crucial decorators: check_permit_permission
and check_course_attributes
. The first is Relation Based Access Control (ReBAC) which checks if the users have the right relationship (e.g. instructor or student) with courses. The second verifies dynamic conditions such as course prerequisites and regional restrictions using Attribute Based Access Control (ABAC). Permit.io's asynchronous permission checks are handled using Django's async_to_sync
.
Then update the course views in the views.py
file to use our ABAC middleware:
#...
@check_permit_permission(action="view")
@check_course_attributes(action="view")
def retrieve(self, request, pk=None):
course = get_object_or_404(Course, pk=pk)
serializer = self.serializer_class(course)
return Response(serializer.data)
@check_permit_permission(action="enroll")
@check_course_attributes(action="enroll")
@action(detail=True, methods=['post'])
def enroll(self, request, pk=None):
course = get_object_or_404(Course, pk=pk)
enrollment, created = StudentEnrollment.objects.get_or_create(
student=request.user,
course=course
)
serializer = StudentEnrollmentSerializer(enrollment)
return Response(serializer.data, status=201 if created else 200)
Here we have enhanced our Django REST Framework viewset
by applying both middleware decorators. This creates a layered permission system where each course action (view, edit, delete, enroll) is protected by both relationship and attribute checks. It keeps the original query set
filtering, but adds the sophisticated authorization layer of Permit.io.
Testing the ReBAC and ABAC Policies
With the ReBAC and ABAC policies implemented in our application, and implemented in our Django application, let's test the policies to ensure everything works as expected. Create a test command by running the following commands:
mkdir -p courses/management/commands
touch courses/management/commands/create_test_data.py
Then add this code to create_test_data.py
file to create new users, courses and course enroll:
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from courses.models import Course, StudentEnrollment
class Command(BaseCommand):
help = 'Creates test data for the e-learning platform'
def handle(self, *args, **kwargs):
try:
# Create test users
instructor = User.objects.create_user(
username='instructor-1',
email='instructor@example.com',
password='testpass123'
)
self.stdout.write(self.style.SUCCESS('Created instructor user'))
student = User.objects.create_user(
username='student-1',
email='student@example.com',
password='testpass123'
)
self.stdout.write(self.style.SUCCESS('Created student user'))
# Create test course
advanced_course = Course.objects.create(
title='Advanced Python',
description='Advanced Python concepts',
instructor=instructor,
level='ADVANCED',
region_restrictions=['US', 'UK']
)
self.stdout.write(self.style.SUCCESS('Created advanced course'))
# Create enrollment
enrollment = StudentEnrollment.objects.create(
student=student,
course=advanced_course,
progress=50
)
self.stdout.write(self.style.SUCCESS('Created student enrollment'))
except Exception as e:
self.stdout.write(self.style.ERROR(f'Error: {str(e)}'))
Now run the command below to the script:
python manage.py create_test_data
If everything goes well you should see the out below on your terminal.
If you navigate to Directory -> Users in your Permit UI you will find the created synced users.
Finally, run your Django server with the python manage.py runserver
command and access the API at http://localhost:8000. Try accessing the course endpoint with different users (instructor vs student) and varying the location header (X-User-Location
) to test both ReBAC roles and ABAC attribute checks. As an instructor, you should have full access, while students will be restricted based on their progress and location.
Conclusion
Django’s built-in authorization system is not sufficient for modern e-learning platforms because of its static nature and inability to handle complex access patterns. In this tutorial, we've solved these limitations by integrating Permit.io into our Django application which enables us to:
- Implement sophisticated permission rules based on relationships between courses, instructors, and students (ReBAC), along with dynamic attributes like progress and prerequisites (ABAC), all without complex code changes.
- Control course access based on multiple factors:
- Student progress in prerequisite courses
- Regional availability of content
- Course level requirements
- Manage all authorization logic through Permit.io's user-friendly UI, separating it from our application code.
- Update access rules in real time without requiring application redeployment.
This solution takes our basic Django e-learning platform and turns it into a full featured system with enterprise class authorization, a great fit for modern educational applications that require fine grained access control based on relationships and attributes.
Written by
Ekekenta Clinton
Senior Technical Writer | Developer Advocate focused on Web Development Technologies | Community Manager | API Documentation | Documentation Engineer | Docs-as-Code | Jira | Markdown