Django-CRUM

Django-CRUM (Current Request User Middleware) captures the current request and user in thread local storage.

It enables apps to check permissions, capture audit trails or otherwise access the current request and user without requiring the request object to be passed directly. It also offers a context manager to allow for temporarily impersonating another user.

It provides a signal to extend the built-in function for getting the current user, which could be helpful when using custom authentication methods or user models.

It is tested against:
  • Django 1.11 (Python 3.5 and 3.6)
  • Django 2.0 (Python 3.5, 3.6 and 3.7)
  • Django 2.1 (Python 3.5, 3.6 and 3.7)
  • Django 2.2 (Python 3.5, 3.6, 3.7, 3.8 and 3.9)
  • Django 3.0 (Python 3.6, 3.7, 3.8 and 3.9)
  • Django 3.1 (Python 3.6, 3.7, 3.8 and 3.9)
  • Django 3.2 pre-release (Python 3.6, 3.7, 3.8 and 3.9)
  • Django main/4.0 (Python 3.8 and 3.9)

Installation

Install the application from PYPI:

pip install django-crum

Add CurrentRequestUserMiddleware to your MIDDLEWARE setting:

MIDDLEWARE += ('crum.CurrentRequestUserMiddleware',)

That’s it!

Usage

The crum package exports three functions as its public API.

get_current_request()

get_current_request returns the current request instance, or None if called outside the scope of a request.

For example, the Comment model below overrides its save method to track the IP address of each commenter:

from django.db import models
from crum import get_current_request

class Comment(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    comment = models.TextField()
    remote_addr = models.CharField(blank=True, default='')

    def save(self, *args, **kwargs):
        request = get_current_request()
        if request and not self.remote_addr:
            self.remote_addr = request.META['REMOTE_ADDR']
        super(Comment, self).save(*args, **kwargs)

get_current_user()

get_current_user returns the user associated with the current request, or None if no user is available.

If using the built-in User model from django.contrib.auth, the returned value may be the special AnonymousUser, which won’t have a primary key.

For example, the Thing model below records the user who created it as well as the last user who modified it:

from django.db import models
from crum import get_current_user

class Thing(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    created_by = models.ForeignKey('auth.User', blank=True, null=True,
                                   default=None)
    modified = models.DateTimeField(auto_now=True)
    modified_by = models.ForeignKey('auth.User', blank=True, null=True,
                                    default=None)

    def save(self, *args, **kwargs):
        user = get_current_user()
        if user and not user.pk:
            user = None
        if not self.pk:
            self.created_by = user
        self.modified_by = user
        super(Thing, self).save(*args, **kwargs)

impersonate(user=None)

impersonate is a context manager used to temporarily change the current user as returned by get_current_user. It is typically used to perform an action on behalf of a user or disable the default behavior of get_current_user.

For example, a background task may need to create or update Thing objects when there is no active request or user (such as from a management command):

from crum import impersonate

def create_thing_for_user(user):
    with impersonate(user):
        # This Thing will indicated it was created by the given user.
        user_thing = Thing.objects.create()
    # But this Thing won't have a created_by user.
    other_thing = Thing.objects.create()

When running from within a view, impersonate may be used to prevent certain actions from being attributed to the requesting user:

from django.template.response import TemplateResponse
from crum import impersonate

def get_my_things(request):
    # Whenever this view is accessed, trigger some cleanup of Things.
    with impersonate(None):
        Thing.objects.cleanup()
    my_things = Thing.objects.filter(created_by=request.user)
    return TemplateResponse(request, 'my_things.html',
                            {'things': my_things})

Signals

(New in 0.6.0) The crum package provides a signal to extend the capabilities of the get_current_user() function.

current_user_getter

The current_user_getter signal is dispatched for each call to get_current_user(). Receivers for this signal should return a tuple of (user, priority). Receivers should return None for the user when there is no current user set, or False when they can not determine the current user.

The priority value which will be used to determine which response contains the current user. The response with the highest priority will be used as long as the user returned is not False, otherwise lower-priority responses will be used in order of next-highest priority. Built-in receivers for this signal use priorities of -10 (current request) and +10 (thread locals); any custom receivers should usually use -10 < priority < 10.

The following example demonstrates how a custom receiver could be implemented to determine the current user from an auth token passed via an HTTP header:

from django.dispatch import receiver
from crum import get_current_request
from crum.signals import current_user_getter

@receiver(current_user_getter)
def (sender, **kwargs):
    request = get_current_request()
    if request:
        token = request.META.get('HTTP_AUTH_TOKEN', None)
        try:
            auth_token = AuthToken.objects.get(token=token)
            return (auth_token.user, 0)
        except AuthToken.DoesNotExist:
            return (None, 0)
    return (False, 0)