How to Implement Role-Based Access Control (RBAC) into a Django Application
- Share:
Role-Based Access Control (RBAC) is a method used to manage access permissions based on users’ roles within an application. It’s a crucial feature for modern applications, especially multi-tenant ones, where each organization or tenant might have different permission requirements.
Django provides a built-in permissions system known as Django authorization, which comes with Django permissions for access control. However, this standard approach is static and can not handle modern multi-tenant authorization needs or complex business logic. This means we need a more customized way of doing authorization.
In this tutorial, you will learn how to:
- Define roles and permissions that users can have (e.g., Admin, Editor, Viewer) and what actions they can perform(create, view, update, delete) without code changes.
- Handle multi-tenant authorization, where each organization can have its permission structure.
- Implement server-side checks to verify user permissions before allowing them to perform any action.
Throughout this tutorial, you’ll learn these concepts in practice by building a document management app. We’ll cover best practices for implementing RBAC in Django APIs and equip you with the required knowledge to create a production-ready authorization system in your Django applications.
Django’s Native Permissions
Django provides a default permission system through django.contrib.auth
that assigns permissions to users and groups. While useful for basic authorization, it has several limitations -
Looking at the code from our starter project that shows how we currently filter and create documents based on the current organization to ensure proper data isolation:
# documents/views.pydef get_queryset(self):
organization_id = self.request.headers.get('X-Organization-Id')
if not organization_id:
return Document.objects.none()
return Document.objects.filter(organization_id=organization_id)
def perform_create(self, serializer):
organization_id = self.request.headers.get('X-Organization-Id')
organization = get_object_or_404(Organization, id=organization_id)
serializer.save(
organization=organization,
created_by=self.request.user
)
While this works for basic filtering, it has several significant shortcomings:
- If we want to add role-based permissions (e.g., some users can only read, others can edit), we’d need to modify the application code
- We can’t dynamically change who can create/edit/delete documents without code changes and redeployment
- There’s no way to add contextual rules (like “users can only edit documents they created”) without significant custom code
- Changes to permission logic require developer intervention and database migrations
Also, our current OrganizationMember
model shows the static nature:
# core/models.pyclass OrganizationMember(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
role = models.CharField(max_length=50)
While we have a role
field, it’s just a string, so there’s no built-in way to:
- Define what each role can do
- Change role permissions without code changes
- Handle complex scenarios like temporary access or conditional permissions
To address these limitations in your Django application, we’ll implement a dynamic RBAC system using Permit.io.
Permit.io allows us to define fine-grained access control policies based on user roles, resource attributes, and environmental conditions. It separates your core business logic from your authorization logic and makes it easier to manage your codebase. Permit's no-code UI also allows us to create very specific policies that precisely match your application's real-world requirements, all without having to write policy-as-code manually.
Setting up Django Project.
To focus this tutorial on implementing the multi-tenant RBAC policy, we have set up a starter project for the demonstration. Clone the starter project by running the following command:
git clone <https://github.com/icode247/multidoc>
cd multidoc
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\\activatepip install -r requirements.txt
Setting Up Multi-tenant RBAC in Permit.io
Before writing any application code, let’s configure our authorization model in Permit.io and get the necessary credentials. To do that, follow the steps below:
- Create a Permit.io account and set up a new project named multidoc in the dashboard.
New project creation in Permit.io
- By default, Permit.io provides you with Production and Development environments for each project. Select your preferred environment or create a new one and Copy the API Key.
Screenshot 2025-01-06 at 10.18.30
Defining Document Resource
Next, let’s define a document resource for our application and create RBAC roles and policies in Permit.io.
Navigate to Policy → Resources
Click on the Add Resource button
Create a document resource with these actions:
create
: Create new documentsread
: View documentsupdate
: Edit documentsdelete
: Delete documents
Creating RBAC Policies
Now create RBAC roles and policies that will define what access or action each user has or can perform on each tenant.
From the Policy page, click on the Roles tab. The following roles were defined when we created the document resource:
Admin
: Full access to documentsEditor
: Can create and edit documentsViewer
: Can only read documents
Navigate to the Policy Editor and grant each role access to the document resource.
Setting Up Organizations (Tenants)
Since we are implementing a multi-tenant policy in this tutorial, you need to create two example organizations TechCorp
and DesignStudi
. Each organization represents a tenant in your application. To do that, follow the steps:
Go to Directory -> Default Tenant and click the + Create New button from the dropdown.
Create two example organizations:Create a new tenant for
TechCorp
andDesignStudi
organizations.
You can create separate users for each tenant and give them different permissions.
Integrating Permit.io with Django
Now that we’ve implemented the multi-tenant RBAC policies in Permit.io, it’s time to integrate them into your Django project.
Initializing Permit.io SDK
Create a new file named permit_client.py
in the multidoc folder and add the following code snippets:
from permit import Permit
import os
permit = Permit(
pdp=os.getenv('PERMIT_PDP_URL', '<https://cloudpdp.api.permit.io>'),
token=os.getenv('PERMIT_API_KEY')
)
Then create a .env file in the project root folder and add your PERMIT_API_KEY
.
In production, you might need to change the PERMIT_PDP_URL to fit your deployment
DJANGO_SECRET_KEY=PERMIT_API_KEY=PERMIT_PDP_URL=
Syncing Users with Permit.io
Next, sync your users with Permit.io to enforce the policies you’ve set. A great time to do this is during the creation of new organizations and their members.
First, a new tenant for each organization should be created to apply the multi-tenant RBAC policy.
Then, as you add organization members, link each new user to their respective tenant. We’ll achieve this using the Django signal. Create a new signal.py
file in the core folder and add the code snippets:
from django.db.models.signals import post_save
from django.dispatch import receiver
from asgiref.sync import async_to_sync
from .models import Organization, OrganizationMember
from multidoc.permit_client import permit
@receiver(post_save, sender=Organization)
def sync_organization_to_permit(sender, instance, created, **kwargs):
if created:
async_to_sync(permit.api.tenants.create)({
"key": str(instance.id),
"name": instance.name,
"description": f"Tenant for {instance.name}" })
@receiver(post_save, sender=OrganizationMember)
def sync_user_to_permit(sender, instance, created, **kwargs):
if created:
async_to_sync(permit.api.users.sync)({
"key": str(instance.user.id),
"email": instance.user.email,
"first_name": instance.user.first_name,
"last_name": instance.user.last_name
})
async_to_sync(permit.api.users.assign_role)({
"user": str(instance.user.id),
"role": instance.role,
"tenant": str(instance.organization.id)
})
The above code snippet will receive a signal from the Organization
and OrganizationMember
models to create a new tenant with permit.api.tenants.create
, sync user to the tenant with permit.api.users.sync
and assign a role to the created member using permit.api.users.assign_role
.
Then update your core/app.py
file to listen to the signal.
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' name = 'core' def ready(self):
from . import signals
Creating Permission Middleware
Now that the user is synced, we can use its identifier to check permissions with permit.check()
. The user identifier used was the user ID; it can be anything (email, db ID, etc) but must be unique for each user. Create a new file named middleware.py
in the documents folder and add the code:
from django.http import HttpResponseForbidden
from functools import wraps
from multidoc.permit_client import permit
from asgiref.sync import async_to_sync
def check_permit_permission(action):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(view_instance, request, *args, **kwargs):
organization_id = request.META.get('HTTP_X_ORGANIZATION_ID')
if not organization_id:
return HttpResponseForbidden("Organization context required")
permitted = async_to_sync(permit.check)(
str(request.user.id),
action,
{
"type":"document",
"tenant": organization_id,
}
)
if not permitted:
return HttpResponseForbidden("Access denied")
return view_func(view_instance, request, *args, **kwargs)
return _wrapped_view
return decorator
Then update the documents/views.py
to use the view to protect the API routes:
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from .models import Document
from .serializers import DocumentSerializer
from .middleware import check_permit_permission
from django.shortcuts import get_object_or_404
from core.models import Organization
from rest_framework import status
class DocumentViewSet(ViewSet):
serializer_class = DocumentSerializer
def get_queryset(self):
organization_id = self.request.META.get('HTTP_X_ORGANIZATION_ID')
return Document.objects.filter(
organization_id=organization_id
)
@check_permit_permission(action="read")
def list(self, request):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
@check_permit_permission(action="create")
def create(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
organization_id = request.META.get('HTTP_X_ORGANIZATION_ID')
organization = get_object_or_404(Organization, id=organization_id)
serializer.save(
organization=organization,
created_by=request.user
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@check_permit_permission(action="update")
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.serializer_class(instance, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@check_permit_permission(action="delete")
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def get_object(self):
queryset = self.get_queryset()
obj = get_object_or_404(queryset, id=self.kwargs['pk'])
return obj
Testing the Multi-tenant Permissions
Run the Django application with the command:
python manage.py runserver
Then navigate to
<http://127.0.0.1:8000/api/organizations/>
In your browser, and create a new organization.
Once you have done that, you’ll find a new tenant created for the newly created organization in your Permit.io UI.
Now if a user tries to access the documents from a different organization, they’ll get an Access denied
.
Conclusion
Django’s built-in RBAC system falls short of modern multi-tenant applications due to its static nature, limited flexibility, and lack of dynamic updates. In this tutorial, we’ve solved these limitations by integrating Permit.io into our Django application. This enabled us to:
- Dynamically manage permissions via a user-friendly UI without code changes or migrations.
- Ensure true multi-tenant isolation where organizations can fully control their permission structure.
- Give different roles (Admin, Editor, Viewer) that can be customized per organization.
- Change access rights in real-time without redeploying the application
This solution turns our basic Django application into a secure multi-tenant system with enterprise-grade authorization, making it suitable for advanced access control cases.
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