How to safely access request object in Django models

Ahtisham picture Ahtisham · Sep 7, 2018 · Viewed 9k times · Source

What I am trying to do:

I am trying to access request object in my django models so that I can get the currently logged in user with request.user.

What I have tried:

I found a hack on this site. But someone in the comments pointed out not to do it when in production.

I also tried to override model's __init__ method just like mentioned in this post. But I got an AttributeError: 'RelatedManager' object has no attribute 'request'

Models.py:

class TestManager(models.Manager):
    def user_test(self):
        return self.filter(user=self.request.user, viewed=False)

class Test(models.Model):

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(Test, self).__init__(*args, **kwargs)

    user = models.ForeignKey(User, related_name='test') 
    viewed = models.BooleanField(default=False)
    objects = TestManager()

Answer

Willem Van Onsem picture Willem Van Onsem · Sep 7, 2018

I trying to access request object in my Django models so that I can get the currently logged in user with request.user.

Well a problem is that models are not per se used in the context of a request. One for example frequently defines custom commands to do bookkeeping, or one can define an API where for example the user is not present. The idea of the Django approach is that models should not be request-aware. Models define the "business logic" layer: the models define entities and how they interact. By not respecting these layers, one makes the application vulnerable for a lot of problems.

The blog you refer to aims to create what they call a global state (which is a severe anti-patten): you save the request in the middleware when the view makes a call, such that you can then fetch that object in the model layer. There are some problems with this approach: first of all, like already said, not all use cases are views, and thus not all use cases pass through the middleware. It is thus possible that the attribute does not exist when fetching it.

Furthermore it is not guaranteed that the request object is indeed the request object of the view. It is for example possible that we use the model layer with a command that thus does not pass through the middleware, in which case we should use the previous view request (so potentially with a different user). If the server processes multiple requests concurrently, it is also possible that a view will see a request that arrived a few nanoseconds later, and thus again take the wrong user. It is also possible that the authentication middleware is conditional, and thus that not all requests have a user attribute. In short there are more than enough scenario's where this can fail, and the results can be severe: people seeing, editing, or deleting data that they do not "own" (have no permission to view, edit, or delete).

You thus will need to pass the request, or user object to the user_test method. For example with:

from django.http import HttpRequest

class TestManager(models.Manager):
    def user_test(self, request_or_user):
        if isinstance(request_or_user, HttpRequest):
            return self.filter(user=request_or_user.user, viewed=False)
        else:
            return self.filter(user=request_or_user, viewed=False)

one thus has to pass the request object from the view to the function. Even this is not really pure. A real pure approach would only accept a user object:

class TestManager(models.Manager):
    def user_test(self, user):
            return self.filter(user=user, viewed=False)

So in a view one can use this as:

def some_view(request):
    some_tests = Test.objects.user_test(request.user)
    # ...
    # return Http response

For example if we want to render a template with this queryset, we can pass it like:

def some_view(request):
    some_tests = Test.objects.user_test(request.user)
    # ...
    return render(request, 'my_template.html', {'some_tests': some_tests})