Restricting Django Views by User Group with a Decorator Factory


In Django, you sometimes need to wrap a view function in a decorator that checks whether the current user belongs to certain groups.

There are a few things we want from this decorator:

  • It should be able to check several groups at once.
  • The list of groups should be configurable through the decorator’s arguments.
  • It should be able to carry some custom behavior. For example, only admins should even know that a given page exists — every other user gets a 404.

Let’s create a module for our decorators, call it decorators.py, and start with a simple one. It checks whether the user is in the admin group; if that check fails, we return a 404.

from django.http import Http404

def check_user_able_to_see_page(function):
    def wrapper(request, *args, **kwargs):
        if request.user.groups.filter(name="admin").exists():
            return function(request, *args, **kwargs)
        raise Http404

    return wrapper

We can use it in views.py like this:

@check_user_able_to_see_page
def hidden_page(request):
    ....

The problem

There’s a catch. If we want to grant access to a different group, we have to modify the decorator or write a new one. And if we have several pages, each needing its own group check, we’d end up creating a separate decorator for every one of them.

A decorator receives a single argument: the function it decorates. There’s no room to pass anything else. The way around this is a decorator factory — a function that returns a decorator.

from django.http import Http404

def check_user_able_to_see_page(*groups):

    def decorator(function):
        def wrapper(request, *args, **kwargs):
            if request.user.groups.filter(name__in=groups).exists():
                return function(request, *args, **kwargs)
            raise Http404

        return wrapper

    return decorator

Now you can use it in any of these ways:

@check_user_able_to_see_page("admin")
def hidden_page(request):
    ....

or

@check_user_able_to_see_page("manager")
def hidden_page(request):
    ....

or even with several groups at once:

@check_user_able_to_see_page("admin", "manager")
def hidden_page(request):
    ....

Making it nicer with an enum

This already works well, but let’s polish it a little. First, create a module that enumerates the groups:

# constants.py
from enum import Enum

class Group(Enum):
    admin = "Administrator"
    manager = "Manager"

Then rewrite the decorator to accept those enum members:

from django.http import Http404
from .constants import Group

def check_user_able_to_see_page(*groups: Group):

    def decorator(function):
        def wrapper(request, *args, **kwargs):
            if request.user.groups.filter(
                name__in=[group.name for group in groups]
            ).exists():
                return function(request, *args, **kwargs)
            raise Http404

        return wrapper

    return decorator

Now the usage reads cleanly, with no magic strings to mistype:

from .constants import Group

@check_user_able_to_see_page(Group.admin)
def hidden_page(request):
    ...