2

I'm developing a web application that will store multiple Organizations, where each organization has its own users. An organization is identified by a domain (CharField), and users only see data pertaining to their own organization. For simplicity and performance reasons, I store the data of all organizations onto the same database instead of creating N databases/views.

What I want to implement is a simple way to restrict queries by the relevant domain whenever I'm in the context of a user request.

What I tried

Filtering all querysets

Since each User belongs to an organization the first option that comes to mind is to use a queryset defined this way:

queryset = MyModel.objects.all(domain=request.user.domain)

I can't use this solution because it forces the developer to filter all queries manually all around the web application. If someone forgets to properly filter querysets, users of an organization can see data of other organizations. It's error-prone.

Thread locals

Since I can find the user inside the request. another solution would be to expose the request through a middleware and automatically filter by domain using the domain of the user that will execute the request. For this purpose I found this question but opinion are quite divergent.

I still have to figure out exactly why using thread locals is a bad choice. I discussed this on Freenode / #django but no one expanded on the reasons to avoid it. I would like to better understand what are the pros & cons of this solution.

Model mixins

I would like to have a mixin (let's say a DomainModelMixin) that sets two managers:

objects = DomainManager()
super_objects = models.Manager()

super_objects is the default manager (unfiltered), and objects is a custom manager that filters by domain and only provides data pertaining to the organization.

The problem is that at the model layer we don't have the request, and therefore we don't have the User and we don't know which domain to restrict the queryset to. How can we pass the domain to the manager?

The big question

How can we automatically filter objects.all() in a way that is transparent and easy to use for developers? And what if we want use the same logic from a context that doesn't have a Request object (e.g. the interactive shell or tests)?

Davide R.
  • 860
  • 5
  • 24
realnot
  • 721
  • 1
  • 11
  • 31

1 Answers1

1

Edit: I'm leaving my original post in the end, with explanation why it's not such a good idea.

It is possible with a manager, you can add a custom function to it, and use it to add the user argument.

This technique is documented on the Django Docs, adding extra manager methods. The documentation example is kinda misleading by writing a full-blown custom SQL. You can just write something using the normal ORM. What you need to do: - Custom manager, with a get_for_user(self, user) or so method that returns the filtered queryset
- In the model, redirect objects to the new manager objects = CustomManager()
- use it with a model.objects.for_user(user)

Following is the Original post, please disregard. It ends up too hacky to consider okay. It bloats the model(s) with not-quite related methods. Using a custom manager in the objects also has the benefit of being not too far form the standard way of doing things, with hopefully IDE suggestion to help in using the correct method.

I would also have hoped to solve that with a Django manager, which would be the most idiomatic way to solve that, but the lack of request information at the model layer makes it inappropriate.

Instead, I think the best way to make it less error-prone is simply a custom function in the model, and call it where it can be dynamic. I'm assuming your users are extending the Django user.

//models.py
class MyUserModel(model.Models):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="my_user_model")
    domain = models.ForeignKey(Domain, on_delete=models.CASCADE)

    def domains(self):
        queryset = MyModel.objects.filter(domain=self.domain)
        return queryset

// View or else
my_user = request.user.my_user_model
domains_queryset = my_user.domains()

You can also use some duck typing there, if you have several types of custom users, just implement the relevant domains() function for each, and it would give you the appropriately filtered queryset. Don't forget querysets are lazy, so you can add some more filters from the view.

LotB
  • 461
  • 4
  • 10