Search code examples
djangodjango-modelsdjango-viewsdjango-permissionsdjango-guardian

Implement Django permissions per group per user's companies


I have a CustomUser model, each user could be linked to multiple companies. I have initiated my application with 3 generic Django groups; viewer, editor, and supervisor and each user could be a member in only one group.

class CustomUser(AbstractUser):
    companies = models.ManyToManyField(Company, blank=True) 

Permissions

I have used some global Django permissions like the {app_label}.add_{class_name} permisison with the editor group for an instance. In parallel, I used also django-guardian to have some object-level permissions to get deeper with my objects' permissions as they vary depending on some of the object attributes' values.

So, at the end of the day, each user (depending on his group) has some global permissions and some object-level permissions. It works fine that way with no problem so far.

Problem

As stated earlier, each user is linked to multiple companies. Additionally, each object in my database is linked to a specific company. So, I need to have a higher level of permissions so that each user can deal and interact ONLY with the objects that are linked to one of his companies weather he/she is a viewer, editor, or supervisor.

Proposed solutions

Those are the solutions that I thought about, but each one of them has some drawbacks and I don't prefer actually:

  1. To control this also through django-guardian by giving the permissions to the users of the groups instead of the groups directly, so that I can control giving the permissions to the company's objects only. But this solution has multiple drawbacks. It is a heavy operation to give permissions per object per user as I have many users in each company. Also, if I changed one of the user's group or if I added a new user, I have to do some extra work to add to or change his/her permissions.
  2. To handle this explicitly in each view in views.py by always getting the object's company at the very beginning and check if it is one of the user's companies, then explicitly return 403 for an instance if not. The drawbacks of this solution are: it seems like I am doing my own permission layer instead of relying on one of the two permission layers I have, also it needs more extra work with each view explicitly and may be some redundancy.

I like the second solution more than the first one as I can implement a unified Django mixin layer that can do that for me and just inherit it in all my target views.

Do you agree with any of those solutions or have another proposed one?


Solution

  • I followed another solution and I didn't do it through permissions. I have created a generic QuerySet and added it with all the models that are linked to companies:

    class ByCompanyQuerySet(models.QuerySet):
        def all_by_companies(self, user):
            if user.is_superuser:
                return self.all()
            return self.filter(company__in=user.companies.all())
    
    
    class FooModel(models.Model):
        foo = models.CharField(max_length=100)
    
        objects = ByCompanyQuerySet.as_manager()
    

    Then, I have used this all_by_company in all my related Class-based views and also created a common function called get_object_by_company_or_404 to be used in all details views instead of get_object_or_404:

    def get_object_by_company_or_404(my_model, user, *args, **kwargs):
        try:
            return my_model.objects.select_related('company').all_by_companies(user).get(*args, **kwargs)
        except my_model.DoesNotExist:
            raise Http404('This item does not exist.')