Django Best Practices: User Permissions

Updated Best Practices

Table of Contents

Setting user permissions is a common part of most Django projects and can become quite complex quickly. We'll use the Blog example from my Django for Beginners book as an example.

The example is a basic blog website with user accounts but no permissions. So, how could we add some?

Views

Generally, permissions are set in the views.py file. The current view for updating an existing blog post, BlogUpdateView, looks as follows:

# blog/views.py
from django.views.generic import UpdateView

class BlogUpdateView(UpdateView):
    model = Post
    template_name = "post_edit.html"
    fields = ["title", "body"]

LoginRequired

Now, let's assume we want a user to be logged in before they can access BlogUpdateView. There are multiple ways to do this, but the simplest, in my opinion, is to use the built-in LoginRequiredMixin.

If you've never used a mixin before, they are called in order from left to right, so we'll want to add the login mixin before UpdateView. That means if a user is not logged in, they'll be redirected to the login page.

# blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import UpdateView

class BlogUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    template_name = "post_edit.html"
    fields = ["title", "body"]

UserPassesTestMixin

Next-level permissions are specific to the user. In our case, let's enforce the rule that only the author of a blog post can update it. We can use the built-in UserPassesTestMixin for this.

# blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdateView

class BlogUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    template_name = "post_edit.html"
    fields = ["title", "body"]

    def test_func(self):
        obj = self.get_object()
        return obj.author == self.request.user

Note that we import UserPassesTestMixin at the top and add it second in our list of mixins for BlogUpdateView. That means a user must first be logged in and then pass the user test before accessing UpdateView. Could we put UserPassesTestMixin first? Yes, but generally, it's better to start with the most general permissions and then become more granular as you move along to the right.

The test_func method is used by UserPassesTestMixin for our logic. We need to override it. In this case, we set the variable obj to the current object returned by the view using get_object. Then we say, if the author on the current object matches the current user on the webpage (whoever is logged in and trying to make the change), then allow it. If false, a PermissionDenied exception is raised, resulting in a 403 response.

There are other ways to set per-user permissions, including overriding the dispatch method, but UserPassesTestMixin is elegant and specifically designed for this use case.

Function-Based Views

The mixin-based approach above applies to class-based views. For function-based views, Django provides equivalent decorator versions. The @login_required decorator redirects unauthenticated users to the login page:

# blog/views.py
from django.contrib.auth.decorators import login_required

@login_required
def blog_update_view(request, pk):
    ...

For per-object ownership checks, the raise PermissionDenied pattern is the FBV equivalent of UserPassesTestMixin:

# blog/views.py
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404

@login_required
def blog_update_view(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if post.author != request.user:
        raise PermissionDenied
    ...

@user_passes_test is also available for user-level conditions (e.g. checking staff status), but for per-object ownership the inline check is more straightforward.

Regardless of whether you use CBVs or FBVs, the same principle applies: layer permissions from most general to most specific. Check that a user is logged in first, then check that they have the right to act on the specific object. Django's built-in tools — LoginRequiredMixin, UserPassesTestMixin, @login_required, and PermissionDenied — cover the vast majority of real-world cases without needing a third-party package.