Skip to main content

Welcome to a Grasp lesson

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

Lesson 62

Django RBAC: Built-in Permissions

Recommended resources

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.

Django Authentication and Authorization Database Schema ERD
This Entity-Relationship Diagram (ERD) shows the database tables for Django's authentication system. Notice the intermediate tables like `auth_user_groups` and `auth_group_permissions`, which implement the many-to-many relationships between users, groups, and permissions.

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:

  1. Reader: Can only view blog posts.
  2. Writer: Can view, create, and edit posts.
  3. 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.

  1. Create the directory structure for a management command:

    blog/
    └── management/
        ├── __init__.py
        └── commands/
            ├── __init__.py
            └── setup_roles.py
    
  2. 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.

Django Admin: Adding a Group and Assigning Permissions for RBAC
This screenshot of the Django admin shows the manual equivalent of what our script just did: creating a group (in this case, 'Teacher') and assigning specific permissions to it.

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.

  1. 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
    
  2. Create a ViewSet in blog/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 IsAuthenticated and DjangoModelPermissions doing?

    • IsAuthenticated: We used this in the last lesson. It simply checks if request.user is 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) -> requires app.view_model permission.
      • POST (create) -> requires app.add_model permission.
      • PUT/PATCH (update) -> requires app.change_model permission.
      • DELETE (destroy) -> requires app.delete_model permission.

    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 a 403 Forbidden response.

  3. Set up URLs. Create blog/urls.py and include it in core/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.

  1. 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)
    
  2. 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 Group objects and assigning a set of Permission objects to them.
  • Django Automates Permissions. For every model, Django creates add, change, delete, and view permissions automatically.
  • DRF Enforces Permissions. The DjangoModelPermissions class is a powerful tool for DRF ViewSets 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.