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:
- Permissions: Granular access controls attached to models
- Groups: Collections of permissions that can be assigned to users
- 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 instanceschange_modelname
: Ability to modify existing instancesdelete_modelname
: Ability to delete instancesview_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
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
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:
{% 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:
# 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
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:
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:
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:
@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:
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:
pip install django-guardian
Add it to your settings:
INSTALLED_APPS = [
# ...
'guardian',
]
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', # Django's default
'guardian.backends.ObjectPermissionBackend', # guardian backend
]
Using django-guardian:
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:
- Readers: Can view published posts
- Writers: Can create and edit their own posts
- Editors: Can publish, edit and delete any post
Models
# 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
# 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
# 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
<!-- 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
- Defense in Depth: Apply permissions at multiple levels (views, templates, models)
- Least Privilege: Give users only the permissions they need
- Use Groups: Organize permissions using groups for easier management
- Test Authorization: Write tests to verify your permission system works as expected
- Document Permissions: Keep track of permissions to avoid confusion
- Audit Access: Log permission checks and authorization failures
Common Pitfalls
- Forgetting to Check Permissions: Always check permissions before performing restricted actions
- Over-Permissive By Default: Start with restrictive permissions and add as needed
- Hardcoding Permissions: Use Django's permission system instead of hardcoding user checks
- Ignoring Object-Level Permissions: Consider when you need per-object permissions
- 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
- Django Official Documentation on Permissions
- Django-Guardian Documentation for object-level permissions
- Django Access Control Patterns on the Django weblog
Exercises
- Create a simple library app with different permission levels (patrons, librarians, administrators)
- Implement object-level permissions where authors can only edit their own content
- Build a custom decorator that checks for multiple permissions with OR logic (any permission grants access)
- Create a middleware that logs all permission checks for audit purposes
- 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! :)