Managers

Per-Object Permission Levels

Motivation

A frequent requirement for information system database records are often not publicly visible. There are strict rules who can view what. The django permission system allows restricting access to a whole model but in a lot of cases the visibility needs to be determined on object level. Let’s say we have this data structure:

# models.py
class Company(models.Model):
    name = models.CharField(max_length=100)

class User(models.Model):
    name = models.CharField(max_length=100)
    company = models.ForeignKey(Company)

class Project(models.Model):
    name = models.CharField(max_length=100)
    company = models.ForeignKey(Company)

We have a multi-tenant system which helps to organise projects for different companies. As an employee of company “A” I am only allowed to see all projects belonging to company “A”. If we restrict access with the django permissions to the project model, this would not suit our purpose.

Custom manager

To tackle this problem, this library provides an abstract class (actually, there are two of them) to help structure these access levels in a reproducible, understandable and DRY way.

If you derive your managers from AbstractUserSpecificManager, you will expose three methods: visible_for(), editable_for() and deletable_for(). Each method needs to be implemented per manager class like this:

# managers.py
class ProjectManager(AbstractUserSpecificManager):

    def visible_for(self, user):
        return self.get_queryset().filter(company=user.company)

    def editable_for(self, user):
        return self.get_queryset().filter(company=user.company)

    def deletable_for(self, user):
        return self.get_queryset().filter(company=user.company)

Now just register the custom manager within the model:

# models.py
class Project(models.Model):
    name = models.CharField(max_length=100)
    company = models.ForeignKey(Company)

    objects = ProjectManager()

Now you can easily use the given methods in every view, service or serializers as follows:

# views.py
def project_list(request):
    project_list = Project.objects.all().visible_for(request.user)
    return render(request, 'project/project_list.html', {'project_list': project_list})

Obviously, if you are not listing your projects but want to validate if the user is allowed to create/edit or delete the given object, you should use the other two methods.

Best practice

Custom manager and custom queryset

In most cases you want to implement a custom manager to put your model-related methods into and register a custom queryset class to handle the permissions.

# managers.py
class ProjectQuerySet(AbstractUserSpecificQuerySet):

    def visible_for(self, user):
        return self.filter(company=user.company)

    def editable_for(self, user):
        return self.filter(company=user.company)

    def deletable_for(self, user):
        return self.filter(company=user.company)

class ProjectManager(models.Manager):

    def get_queryset(self):
        return ProjectQuerySet(self.model, using=self._db)

In the model, you’ll register only the manager as usual.

# models.py
class Project(models.Model):
    name = models.CharField(max_length=100)
    company = models.ForeignKey(Company)

    objects = ProjectManager()

Directly use custom QuerySet class

Under some circumstances, you could skip the manger and directly use a QuerySet class as a manager. Therefore, this package provides the class AbstractUserSpecificQuerySet. Usually these circumstances are “small” models where you don’t have custom manager methods. Note that you’ll have to refactor your code when you start implementing custom manger methods!

At first, use a custom queryset class like this:

# managers.py
class ProjectQuerySet(AbstractUserSpecificQuerySet):

    def visible_for(self, user):
        return self.filter(company=user.company)

    def editable_for(self, user):
        return self.filter(company=user.company)

    def deletable_for(self, user):
        return self.filter(company=user.company)

Take care that you need to register it a little different in the model:

# models.py
class Project(models.Model):
    name = models.CharField(max_length=100)
    company = models.ForeignKey(Company)

    objects = ProjectQuerySet.as_manager()

Then you’ll save the .all() and can directly use the exposed methods:

project_list = Project.objects.visible_for(request.user)

Default manager for global visibility

If you have the case that a certain model does not require any user-level permissions, you can use the GloballyVisibleQuerySet. This manager (or more precisely custom queryset) just returns all records when calling any of its base methods:

def visible_for(self, user):
    return self.all()

It is advisable to use this manager for every class. This way, you can ensure that if some permissions are added to a model, they are put in the right place (the visible_for() method) and not somewhere in the code.

Secondly you do not have to decide every time you need the manger if you need to use visible_for() or not - just use it all the time.

Usually you would use this manager for metadata like categories. As pointed out above, you could use the base manager class BUT if you have to add some user-level permissions later on, you reduce the risk of bad patterns in your code.