This post explains (using a very simple example) how we implemented a view-based permissions system using Django and Django REST Framework, and attempts to justify why we chose to do such a thing.
It can be read end-to-end for a re-cap of Django’s existing permissions system and the motivation for moving to a view-based permissions system. Alternatively the “View Based Permissions” section can be read in isolation for the implementation details.
Model Based Permissions
Django implements a form of the role based access control architecture where:
- A “role” is known as a “group”.
- Both “permissions” and “users” are assigned to groups.
- The permissions held by a user are determined by the group they are a part of ¹.
- Permissions are associated with primitive create, read, update and delete operations on models. That is, when a new model is created, four permissions are automatically defined for creating, reading, updating and deleting instances of that model.
As an example consider an application for making and selling sausages:
sausage_factory. This application defines four models:
Migrating the database will produce a set of sixteen permissions: four for each model. These can be accessed from the Django admin page, or by querying the
Permission model in an interactive shell.
( As a side note, there will be other permissions after running the statements above, for brevity only those relating to the models defined earlier have been shown )
Where This Approach Breaks Down
As applications (and businesses) grow larger, user interactions may involve complex operations on multiple models. For example suppose the views in the sausage selling application were defined as follows:
In this example each view calls a helper function that implements a set of business rules (in this case for making or selling a sausage).
For the sake of the example, assume that each service implements some kind of complex business logic. Specifically let’s say that the
make_sausage service will attempt to make a sausage with the specified name, out of the specified materials. It may also have some kind of fall-back logic to deal with corner cases such as where a sausage with the specified name has already been made, or the materials the sausage is being made from don't exist.
Managing model-based permissions in code like this is difficult because each process requires a mix of permissions on an unknown set of models. For example if we want to figure out which permissions are required to make sausages it will be necessary to know:
- Which models the
make_sausagehelper function interacts with, both directly within the function, and indirectly via foreign keys and/or many to many relationships.
- Any models the helper function interacts with outside of the
- How the helper function interacts with all the models indicated above (for example does it simply need read access, or does it need to create and delete model instances?).
Establishing this is an exercise in and of itself. Consider also that what it means to make a sausage is likely to change over the lifetime of the business. The data that needs to be accessed to make a sausage (and how it is accessed) will not stay the same, and neither will the permissions. Locking down access to the application will therefore be an ongoing (and quite involved) concern.
This is also not intuitive for users requesting access to the application. These users only want to be able to make or sell sausages, and don’t necessarily want to be involved in the particulars of operations on specific database rows.
View Based Permissions
One possible solution to this problem is to move permissions up to the same level as the business logic. That is, implement permissions on views rather than models.
The code below implements this behavior in the sausage selling application:
Starting in models.py, each permission is defined in the
Meta class of
PermissionContainer. This is an unmanaged model (meaning there is no corresponding table in the database) intended only for storing permissions. The reason it exists is that permissions must be defined in Django in the
Meta class of a specific model. It doesn't make sense to tie a view based permission, which may interact with multiple models, to a single model. Therefore a new class is defined to hold these permissions ².
Note that the choice of name here is arbitrary. the name
PermissionContainer was adopted only because it was descriptive and easily understood within our team.
In permissions.py, the logic underlying each permission is defined in a new set of classes overriding the
BasePermission object from Django REST Framework. The important bit here is the implementation of the
has_permission method. This takes a request and a view as arguments, and returns a boolean value indicating if the request is permitted to access the view.
In this case our method executes three checks to ensure that:
- A valid (ie: not-null) user object is attached to the request.
- The user making the request is a staff member.
- The user making the request has the required permission (ie: is in a group that is assigned to the required permission).
Finally, in views.py, the permission is attached to the view via the
permission_classes decorator, which is imported from
It’s worth noting that this approach isn’t intended to be the best way of doing permissions in Django — it’s just another way of doing it. There are no doubt use cases where it makes more sense to use the out of the box model-based approach.
That said, this was the approach that worked for our use case, and we believe there will be other projects that will benefit from this sort of “top-down” approach to permissions management.
: Permissions may also be assigned directly to users, by-passing the role-based architecture entirely. This is not the approach taken here, but could suit some use cases.
: Note that this doesn’t stop us from storing permissions on an existing model (for example using the
Meta attribute on the
Sausage class instead of defining a new model to act as a container). It could even be argued in this case that it’s more idiomatic storing the permissions directly on the
Sausage class, as each of the actions is directly tied to instances of this class by it’s name. However recall the assumption that all of these actions are complex, and interact with a range of models across the project both directly and indirectly via foreign keys and database relationships. It therefore makes sense to separate permissions from model definitions for the reasons stated. Of course there will always be situations where it makes more sense to store permissions directly on a specific model, and this should be kept in mind when implementing a permissions system.