The Django framework has a built-in permissions engine that will suit most simple projects. I can say that this is an ideal solution for the built-in admin interface. But what if the project access rights should be implemented in more complicated way?
In this article I will share our team’s experience and will give you some examples of the custom permission engine. Our example will not interfere with Django's built-in permissions. This article is accompanied by a complete Django project with implementation. To simplify the presentation of the code, I deliberately removed the lines that relate to active query caching and performance optimization.
The demo project will also be useful for novice developers to explore.
We started using the first version of the described approach starting from the Django 1.2 version on the project, called Billing platform for ISP. The requirement of the project in terms of security was the differentiation of user access rights between various companies. The summary below gives more details:
As it can be understood from the description of the requirements above, defying the access rights must be carried out at the Object level.
For clarity, I will give a brief description of the models:
class Company(models.Model): name = models.CharField(max_length=255) class Meta: db_table = 'company' class Customer(models.Model): company = models.ForeignKey(Company, on_delete=models.PROTECT) name = models.CharField(max_length=255) class Meta: db_table = 'customer'
In the project, these models are separated into different modules for convenience. Let's accept that the description of the permission will consist of two parts: the name of the module and the description of the user action that the permission will control:
Module
Action
Description
company
read
Viewing the object Company
create
Creation of the object Company
update
Alteration of the object Company
delete
Deleting the object Company
customer
Viewing the object Customer
Creation of the object Customer
Alteration of the object Customer
Deleting the object Customer
verify
Verification of the object Customer
activate
Activating the object Customer
According to the table, some of the actions are “classic” CRUD, but for display only; both real business applications, and the permission system are more complicated; two additional functions have been added for the Customer module: to verify and to activate. Thus, in building the permissions engine, we are not limited to the CRUD model, but we will be able to control user actions with the necessary granularity.
To simplify the code, the rights will be assigned only to groups. In real projects, it is sometimes required to issue rights for individual actions at the user level.
The two-level structure of permissions will be described by the following models:
class AccessModule(models.Model): name = models.CharField(max_length=128) slug = models.CharField(max_length=128, unique=True) class Meta: db_table = 'access_module' class AccessPermission(models.Model): module = models.ForeignKey(AccessModule, on_delete=models.PROTECT) name = models.CharField(max_length=128) slug = models.CharField(max_length=128) groups = models.ManyToManyField(Group) class Meta: db_table = 'access_permission' unique_together = (('module', 'slug'),)
The AccessPermission model has a hyperlink to a Group. In such a way a list of groups that are allowed to this permission is identifed.
In the ERP platform project, such models can be represented by the following configuration interface:
In the result, presentation of sections and access rights by user groups appears flexible and user-friendly in terms of functionality. One only needs to add a model that will describe which of the groups a user belongs to in different companies:
The object HttpRequest is present in many contexts: it can be found in View, it can be transferred to the template context. Information about the authorized user is also available in request.user.
Therefore, it would be convenient to add the access to the rights check via Request. For this purpose, middleware has been developed, this is a lazy object to Acl.
For example, it is possible to use the following construction:
def get_queryset(self): qs = super().get_queryset() qs = qs.filter(company__in=self.request.acl.get_companies('customer', 'view')) return qs
The example above shows the Customer list filtration only for companies where the user has rights.
It is highly recommended to view the example of CustomerListView from the project.
In this article I have not touched upon active caching of requests by rights. As you can see from the code, adding caching is not difficult when composing a caching key in the following way:
<module_slug>:<permissions_slug>:<company_id>
In my next works, an example and description of use of this permission engine combined with Django Rest Framework will be performed, as well as auto-documenting permissions in conjunction with drf-spectacular will be presented.
Software Development Hub is a Ukraine-based software engineering company with Python/Django stack. We help tech-enabled organizations meet their growth ambitions through IT talent outsourcing. With a pool of more than 100 highly skilled specialists, SDH can build engineering units with nearly any skillset, providing maximum flexibility to deliver high-quality software development services to projects of any complexity or scale.
Drop us a line, and we provide you with a qualified consultation.