Skip to main content

Django User Authorization

Introduction

User authorization is a critical aspect of web application security that determines what actions authenticated users can perform within your application. While authentication verifies who users are, authorization controls what they're allowed to do after they've logged in.

Django provides a robust permission system that allows you to control access to views, models, and other resources based on user roles and permissions. In this guide, we'll explore Django's authorization framework and learn how to implement access controls in your applications.

Authorization vs Authentication

Before diving in, let's clarify the distinction:

  • Authentication: Verifies the identity of users (who they are)
  • Authorization: Determines what authenticated users can do (what permissions they have)

Django's Permission System

Django's built-in permission system consists of:

  1. Permissions: Granular access controls attached to models
  2. Groups: Collections of permissions that can be assigned to users
  3. User object permissions: User-specific permissions for particular model instances

Default Permissions

When you create a model in Django, it automatically creates four basic permissions:

  • add_modelname: Ability to create new instances
  • change_modelname: Ability to modify existing instances
  • delete_modelname: Ability to delete instances
  • view_modelname: Ability to view instances

For example, if you have a Book model, Django automatically creates add_book, change_book, delete_book, and view_book permissions.

Setting Up Basic Authorization

1. Checking Permissions in Views

Django provides several ways to check permissions in views:

Using Function-Based Views with @login_required and @permission_required

python
from django.contrib.auth.decorators import login_required, permission_required

# Require login for this view
@login_required
def profile_view(request):
return render(request, 'profile.html')

# Require specific permission
@permission_required('books.add_book')
def add_book_view(request):
# View logic here
return render(request, 'add_book.html')

# Multiple permissions (all required)
@permission_required(('books.add_book', 'books.change_book'))
def manage_books_view(request):
return render(request, 'manage_books.html')

# Permission with login redirect
@permission_required('books.add_book', login_url='/login/')
def add_book_with_redirect(request):
return render(request, 'add_book.html')

Using Class-Based Views with Mixins

python
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import ListView, CreateView
from .models import Book

# Require login
class BookListView(LoginRequiredMixin, ListView):
model = Book
template_name = 'books/book_list.html'
login_url = '/login/' # Redirect URL if not logged in

# Require specific permission
class BookCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Book
fields = ['title', 'author', 'description']
template_name = 'books/book_create.html'
permission_required = 'books.add_book'
# For multiple permissions (all required)
# permission_required = ('books.add_book', 'books.change_book')

2. Checking Permissions in Templates

You can also check permissions in templates:

html
{% if perms.books.add_book %}
<a href="{% url 'book-create' %}" class="btn btn-primary">Add New Book</a>
{% endif %}

{% if perms.books.change_book %}
<a href="{% url 'book-update' book.id %}" class="btn btn-secondary">Edit</a>
{% endif %}

{% if perms.books.delete_book %}
<form method="post" action="{% url 'book-delete' book.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
{% endif %}

3. Checking Permissions in Python Code

To check permissions programmatically:

python
# Check if a user has a specific permission
if request.user.has_perm('books.add_book'):
# User can add books
pass

# Check for multiple permissions (AND logic)
if request.user.has_perms(['books.add_book', 'books.change_book']):
# User can both add and change books
pass

Managing Permissions and Groups

Assigning Permissions to Users

python
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
from books.models import Book

# Get a permission
content_type = ContentType.objects.get_for_model(Book)
permission = Permission.objects.get(
codename='add_book',
content_type=content_type,
)

# Assign to user
user = User.objects.get(username='john')
user.user_permissions.add(permission)

# Check if user has permission
user.has_perm('books.add_book') # Returns True

# Remove permission
user.user_permissions.remove(permission)

Working with Groups

Groups help you organize users with similar permissions:

python
from django.contrib.auth.models import Group, Permission, User

# Create a group
editors_group, created = Group.objects.get_or_create(name='Editors')

# Add permissions to the group
book_permissions = Permission.objects.filter(
content_type__app_label='books',
codename__in=['add_book', 'change_book']
)
editors_group.permissions.add(*book_permissions)

# Add user to group
user = User.objects.get(username='jane')
user.groups.add(editors_group)

# Now user inherits all permissions from the group
user.has_perm('books.add_book') # Returns True

Custom Permissions

You can define custom permissions for specific actions beyond the defaults:

python
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
content = models.TextField()
published = models.BooleanField(default=False)

class Meta:
permissions = [
("publish_book", "Can publish book"),
("feature_book", "Can mark book as featured"),
]

After migrating, you can use these permissions like the built-in ones:

python
@permission_required('books.publish_book')
def publish_book_view(request, book_id):
book = get_object_or_404(Book, id=book_id)
book.published = True
book.save()
return redirect('book-detail', book_id=book.id)

Object-Level Permissions

Sometimes you need more granular permissions for specific instances:

python
from django.contrib.auth.mixins import UserPassesTestMixin

class BookUpdateView(UserPassesTestMixin, UpdateView):
model = Book
fields = ['title', 'content']

def test_func(self):
book = self.get_object()
# Allow if user is author or has change permission
return book.author == self.request.user.username or \
self.request.user.has_perm('books.change_book')

Using django-guardian for Object Permissions

For more advanced object-level permissions, consider using django-guardian:

First, install it:

bash
pip install django-guardian

Add it to your settings:

python
INSTALLED_APPS = [
# ...
'guardian',
]

AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', # Django's default
'guardian.backends.ObjectPermissionBackend', # guardian backend
]

Using django-guardian:

python
from guardian.shortcuts import assign_perm, get_perms, remove_perm

# Assign object permission
book = Book.objects.get(id=1)
user = User.objects.get(username='john')
assign_perm('change_book', user, book)

# Check object permission
if user.has_perm('books.change_book', book):
# User can edit this specific book
pass

# Remove object permission
remove_perm('change_book', user, book)

Real-World Example: Blog with Different User Roles

Let's implement a simple blog system with three user roles:

  1. Readers: Can view published posts
  2. Writers: Can create and edit their own posts
  3. Editors: Can publish, edit and delete any post

Models

python
# blog/models.py
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
published = models.BooleanField(default=False)

class Meta:
permissions = [
("publish_post", "Can publish post"),
]

Set Up Groups and Permissions

python
# management/commands/setup_groups.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 = 'Set up permission groups'

def handle(self, *args, **options):
# Content type for Post model
post_content_type = ContentType.objects.get_for_model(Post)

# Create Readers group (view only)
readers_group, created = Group.objects.get_or_create(name='Readers')
view_post = Permission.objects.get(
codename='view_post',
content_type=post_content_type
)
readers_group.permissions.add(view_post)

# Create Writers group
writers_group, created = Group.objects.get_or_create(name='Writers')
writers_perms = Permission.objects.filter(
content_type=post_content_type,
codename__in=['add_post', 'change_post', 'view_post']
)
writers_group.permissions.add(*writers_perms)

# Create Editors group
editors_group, created = Group.objects.get_or_create(name='Editors')
editors_perms = Permission.objects.filter(
content_type=post_content_type
)
editors_group.permissions.add(*editors_perms)

self.stdout.write(self.style.SUCCESS('Successfully set up permission groups'))

Views

python
# blog/views.py
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin
from .models import Post

class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'

def get_queryset(self):
# Readers only see published posts
queryset = Post.objects.filter(published=True)
# Editors and writers see all posts
if self.request.user.has_perm('blog.change_post'):
queryset = Post.objects.all()
return queryset

class PostCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Post
fields = ['title', 'content']
template_name = 'blog/post_form.html'
permission_required = 'blog.add_post'

def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
fields = ['title', 'content']
template_name = 'blog/post_form.html'

def test_func(self):
post = self.get_object()
# Writers can edit their own posts, editors can edit any post
if self.request.user.has_perm('blog.change_post'):
return post.author == self.request.user or \
self.request.user.has_perm('blog.publish_post')
return False

class PublishPostView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = Post
template_name = 'blog/publish_post.html'
fields = [] # No fields to edit
permission_required = 'blog.publish_post'

def form_valid(self, form):
form.instance.published = True
return super().form_valid(form)

Template Example

html
<!-- blog/templates/blog/post_detail.html -->
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }} - {{ post.created_at|date:"F j, Y" }}</p>
<div class="content">
{{ post.content }}
</div>

<div class="actions">
{% if perms.blog.change_post and user == post.author or perms.blog.publish_post %}
<a href="{% url 'post-update' post.id %}">Edit Post</a>
{% endif %}

{% if not post.published and perms.blog.publish_post %}
<form method="post" action="{% url 'post-publish' post.id %}">
{% csrf_token %}
<button type="submit">Publish Post</button>
</form>
{% endif %}

{% if perms.blog.delete_post %}
<form method="post" action="{% url 'post-delete' post.id %}">
{% csrf_token %}
<button type="submit" onclick="return confirm('Are you sure?')">
Delete Post
</button>
</form>
{% endif %}
</div>

Best Practices for Authorization

  1. Defense in Depth: Apply permissions at multiple levels (views, templates, models)
  2. Least Privilege: Give users only the permissions they need
  3. Use Groups: Organize permissions using groups for easier management
  4. Test Authorization: Write tests to verify your permission system works as expected
  5. Document Permissions: Keep track of permissions to avoid confusion
  6. Audit Access: Log permission checks and authorization failures

Common Pitfalls

  1. Forgetting to Check Permissions: Always check permissions before performing restricted actions
  2. Over-Permissive By Default: Start with restrictive permissions and add as needed
  3. Hardcoding Permissions: Use Django's permission system instead of hardcoding user checks
  4. Ignoring Object-Level Permissions: Consider when you need per-object permissions
  5. Performance Issues: Be careful with extensive permission checks that might affect performance

Summary

Django provides a comprehensive authorization system that allows you to control what users can do in your application. We've covered:

  • The difference between authentication and authorization
  • Django's built-in permission system
  • How to check permissions in views, templates, and code
  • Working with user groups and permissions
  • Creating custom permissions
  • Object-level permissions with django-guardian
  • A real-world example of a blog with different user roles

With these tools, you can build secure applications that properly restrict access based on user roles and permissions.

Additional Resources

Exercises

  1. Create a simple library app with different permission levels (patrons, librarians, administrators)
  2. Implement object-level permissions where authors can only edit their own content
  3. Build a custom decorator that checks for multiple permissions with OR logic (any permission grants access)
  4. Create a middleware that logs all permission checks for audit purposes
  5. Extend the blog example by adding a "Moderator" role that can only review and approve posts but not create them


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)