Hello!
In our last lesson, we built a complete JWT authentication system. We now have a reliable way to answer the question, "Who is making this request?". The server receives a token, validates it, and attaches an authenticated user object to the request.
This lesson addresses the next logical step: authorization. Now that we know who the user is, we need to determine what they are allowed to do. We'll move from theory to a concrete, hands-on implementation of Role-Based Access Control (RBAC).
This lesson directly addresses the learning outcome: Implement RBAC in a Django application using its built-in permissions framework. We will leverage Django's powerful, built-in auth models to create roles and enforce access rules on our API endpoints. By the end, you'll have a clear, working example of how to manage user capabilities in a scalable way.
The Core Components of Django's Auth System
At its heart, Django's permission system is built on three key models that work together: User, Group, and Permission. Your Computer Engineering background will appreciate that this is essentially a database schema design problem that Django has solved for us.
- User: The individual account, which we've already set up.
- Permission: A single, granular action a user might perform (e.g., "can add a post", "can delete a user").
- Group: A collection of permissions. This is how we create "roles". Instead of assigning 20 individual permissions to a user, you create a "Writer" group with those 20 permissions and assign the user to that single group.
These models are linked by many-to-many relationships, providing a flexible structure.

For every model you create in your Django application, the framework automatically generates four default permissions when you run migrate:
add_<modelname>change_<modelname>delete_<modelname>view_<modelname>
This powerful feature gives us the building blocks for RBAC right out of the box.
To get a comprehensive overview of these concepts, the video below provides an excellent deep dive into Django's native permission system.
Django Permissions system - deep dive with Users, Groups and Permissions
This video from BugBytes provides a fantastic deep dive into Django's built-in User, Group, and Permission models. It covers both the theory and practical application through the Django admin.
Watch from 00:46 to 07:31. Focus on understanding: The relationship between the User, Group, and Permission models. How a user inherits permissions from the groups they belong to. How Django automatically creates four default permissions for each model. The underlying database tables (auth_permission, auth_user_user_permissions, etc.) that store this data.
Step 1: Defining Roles and Permissions
Let's continue with our project from the previous lesson and build a simple blog. We will define three roles:
- Reader: Can only view blog posts.
- Writer: Can view, create, and edit posts.
- Editor: Can do everything a Writer can, plus delete posts.
First, let's create a new blog app and a Post model.
python manage.py startapp blog
Now, define the model in blog/models.py. We'll link it to our custom user model.
# blog/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='posts'
)
def __str__(self):
return self.title
Add the new app to INSTALLED_APPS in core/settings.py:
# core/settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'accounts',
'blog', # Add the blog app
]
Finally, run the migrations. This is the step that creates the Post table and our four default permissions (view_post, add_post, change_post, delete_post).
python manage.py makemigrations
python manage.py migrate
Step 2: Creating Groups and Assigning Permissions
We can create our roles (Groups) and assign permissions in a few ways: through the Django admin, the shell, or a management command/data migration. For a reproducible setup, a management command is the most robust approach.
Let's create one to set up our roles.
-
Create the directory structure for a management command:
blog/ └── management/ ├── __init__.py └── commands/ ├── __init__.py └── setup_roles.py -
Add the following code to
blog/management/commands/setup_roles.py:
# blog/management/commands/setup_roles.py
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from blog.models import Post
class Command(BaseCommand):
help = 'Creates default roles (Groups) and assigns permissions'
def handle(self, *args, **options):
# Get the content type for the Post model
content_type = ContentType.objects.get_for_model(Post)
# Get permissions for the Post model
post_permissions = Permission.objects.filter(content_type=content_type)
view_post_perm = post_permissions.get(codename='view_post')
add_post_perm = post_permissions.get(codename='add_post')
change_post_perm = post_permissions.get(codename='change_post')
delete_post_perm = post_permissions.get(codename='delete_post')
# Create Reader group
reader_group, created = Group.objects.get_or_create(name='Reader')
if created:
reader_group.permissions.add(view_post_perm)
self.stdout.write(self.style.SUCCESS('Successfully created Reader group'))
# Create Writer group
writer_group, created = Group.objects.get_or_create(name='Writer')
if created:
writer_group.permissions.add(view_post_perm, add_post_perm, change_post_perm)
self.stdout.write(self.style.SUCCESS('Successfully created Writer group'))
# Create Editor group
editor_group, created = Group.objects.get_or_create(name='Editor')
if created:
editor_group.permissions.add(view_post_perm, add_post_perm, change_post_perm, delete_post_perm)
self.stdout.write(self.style.SUCCESS('Successfully created Editor group'))
Now, run the command:
python manage.py setup_roles
This script programmatically creates our three groups and assigns the relevant Post permissions to each.

Step 3: Enforcing Permissions in the API
With our roles defined, we now need to enforce them. Django REST Framework provides a clean, declarative way to do this using permission classes.
First, let's create the API endpoint for our posts.
-
Create a serializer in
blog/serializers.py:# blog/serializers.py from rest_framework import serializers from .models import Post class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content', 'author'] read_only_fields = ['author'] # Author should be set automatically -
Create a
ViewSetinblog/views.py. This is where the magic happens.# blog/views.py from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions from .models import Post from .serializers import PostSerializer class PostViewSet(viewsets.ModelViewSet): """ A ViewSet for viewing and editing posts. """ queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [IsAuthenticated, DjangoModelPermissions] def perform_create(self, serializer): # Automatically set the author to the current user. serializer.save(author=self.request.user)Deep Dive: What are
IsAuthenticatedandDjangoModelPermissionsdoing?IsAuthenticated: We used this in the last lesson. It simply checks ifrequest.useris present and authenticated. It's the gatekeeper; you must be logged in to even attempt an action.DjangoModelPermissions: This is the core of our RBAC enforcement. It automatically maps the HTTP method of the request to the required permission on the model.GET(list and retrieve) -> requiresapp.view_modelpermission.POST(create) -> requiresapp.add_modelpermission.PUT/PATCH(update) -> requiresapp.change_modelpermission.DELETE(destroy) -> requiresapp.delete_modelpermission.
The permission class runs a check like
request.user.has_perm('blog.view_post'). If the user has the permission (either directly or through a group), the request proceeds. If not, DRF returns a403 Forbiddenresponse. -
Set up URLs. Create
blog/urls.pyand include it incore/urls.py.# blog/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import PostViewSet router = DefaultRouter() router.register(r'posts', PostViewSet, basename='post') urlpatterns = [ path('', include(router.urls)), ] # core/urls.py (add this) path('api/', include('blog.urls')), # Make sure this is inside the main urlpatterns
Step 4: Testing the RBAC Flow
Our system is complete. Let's test it end-to-end. For convenience, we'll add a utility endpoint to let a user assign themselves to a group for testing purposes.
-
Add a utility view in
accounts/views.py:# accounts/views.py # Add these imports from rest_framework import status from django.contrib.auth.models import Group # ... (other views) class JoinGroupView(APIView): permission_classes = [IsAuthenticated] def post(self, request): group_name = request.data.get('group_name') if not group_name: return Response({'error': 'group_name is required'}, status=status.HTTP_400_BAD_REQUEST) try: group = Group.objects.get(name=group_name) request.user.groups.add(group) return Response({'message': f'User added to {group_name} group'}, status=status.HTTP_200_OK) except Group.DoesNotExist: return Response({'error': 'Group not found'}, status=status.HTTP_404_NOT_FOUND) -
Add the URL in
accounts/urls.py:# accounts/urls.py from .views import RegisterView, UserView, JoinGroupView # Update import urlpatterns = [ # ... path('join-group/', JoinGroupView.as_view()), # Add this ]
Now, let's use curl to test the permissions.
Setup:
- Register a new user and get their JWT access token, as we did in the last lesson. Let's call this
TOKEN.
Test 1: User with no role
# Try to view posts. This should be forbidden.
curl -X GET http://127.0.0.1:8000/api/posts/ \
-H "Authorization: Bearer $TOKEN"
# Expected: 403 Forbidden
Test 2: The Reader Role
# Join the 'Reader' group
curl -X POST http://127.0.0.1:8000/api/join-group/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"group_name": "Reader"}'
# Now, try to view posts. This should work.
curl -X GET http://127.0.0.1:8000/api/posts/ \
-H "Authorization: Bearer $TOKEN"
# Expected: 200 OK
# Try to create a post. This should be forbidden.
curl -X POST http://127.0.0.1:8000/api/posts/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"title": "My First Post", "content": "Hello world"}'
# Expected: 403 Forbidden
Test 3: The Writer Role
# Join the 'Writer' group
curl -X POST http://127.0.0.1:8000/api/join-group/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"group_name": "Writer"}'
# Try to create a post. This should now work.
curl -X POST http://127.0.0.1:8000/api/posts/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"title": "A Writer Post", "content": "..."}'
# Expected: 201 Created
# Let's say the new post has ID 1. Try to delete it. This should be forbidden.
curl -X DELETE http://127.0.0.1:8000/api/posts/1/ \
-H "Authorization: Bearer $TOKEN"
# Expected: 403 Forbidden
You can continue this pattern to test the 'Editor' role, which should be able to delete posts.
Extending with Custom Permissions
What if you have an action that doesn't map to CRUD, like publish_post? You can add custom permissions.
Using the Django authentication system
The official Django documentation explains how to add custom permissions to a model's Meta class.
Read the short section titled 'Programmatically creating permissions'. It shows the syntax for defining custom permissions inside a model. While the example shows programmatic creation, the same principle applies when defining them in a model's Meta options, which is a common pattern.
The "Django Permissions system - deep dive" video also covers this topic well from 19:30 onwards (resource_id: [LINK](https://www.youtube.com/watch?v=IF_ZpCiZKkw)).
To implement it, you would add a Meta class to your Post model:
# blog/models.py
class Post(models.Model):
# ... fields
class Meta:
permissions = [
("can_publish_post", "Can publish post"),
]
After migrating, you could assign this blog.can_publish_post permission to a group and check for it in a custom ViewSet action using self.request.user.has_perm('blog.can_publish_post').
Conclusion
You have now successfully implemented a full Role-Based Access Control system on top of your JWT authentication backend. You've seen how Django's built-in auth framework provides a robust and scalable foundation for managing what users can and cannot do.
Key Takeaways:
- RBAC in Django = Groups + Permissions. You define roles by creating
Groupobjects and assigning a set ofPermissionobjects to them. - Django Automates Permissions. For every model, Django creates
add,change,delete, andviewpermissions automatically. - DRF Enforces Permissions. The
DjangoModelPermissionsclass is a powerful tool for DRFViewSets that maps HTTP methods to the corresponding permissions, drastically simplifying your views. - Management Commands are Key. Setting up roles and permissions programmatically via management commands or data migrations ensures your environment is reproducible and consistent.
Preview of the Next Lesson
We have covered authentication (who you are) and authorization (what you can do). However, the way we handle our authentication tokens (JWTs) has its own set of security considerations. In our next lesson, "Discuss common JWT security pitfalls, including secret key management and strategies for token revocation," we will dive into the security implications of using JWTs and explore best practices to keep your application and users safe.