on
SoftwareEngineering
Exploring Django Rules
Recently, I’ve been working on a hobby project in my free time. There’s no real goal yet for this project aside from hacking together something with my sister, Elvyna, who is a data scientist. Hence, we decided to go with a Python-based project so she can utilize her data skills in this project with support from the Python ecosystem for data analytics and machine learning.
I myself, while quite comfortable working with Python, have mostly used it just for system scripts and tools development so far. Given that I used to be a Ruby on Rails developer, I decided to go with Django to develop the web application for this project since both Rails and Django were hugely popular back in late 2000s and early 2010s.
One Issue that I found with Django’s built-in authorization system is that it was built for a CMS-like application where each user owns certain resources in the system and they may allow other users to access and modify their resources. While this is great for building systems such as news sites where an author can author multiple articles, which they could share with other authors and editors for collaboration (which requires maintaining complex user-to-resource access mapping), this isn’t ideal for cases where the access rule is relatively static after its initial rule definition.
Back when using Ruby on Rails, I primarily worked with CanCan gem, which was later discontinued and forked as CanCanCan. CanCan and CanCanCan can be used to define access rules in Rails apps in the code itself without writing the rules to the DB, so I was looking for something in Django that works similarly.
I found django-cancan, which was inspired by the CanCan gem. But after going through it for a bit, I found that django-cancan is still writing the access rules we defined into our DB, which is not something I wanted since I aimed to have the access rule definitions defined just in code and minimize the DB access operations for authorization.
After doing a bit more exploration, I found django-rules. It does exactly what I want, defining the access rules in code without writing the rules to DB and requiring us to read from DB for authorization.
In this post, I’d like to summarize a few things I’ve learned about how to use django-rules and how I’ve set it up in our hobby project.
Setting Up Rules in Project Config
First, install the django-rules package if you haven’t.
pip install rules
In settings.py
, add the following configurations.
INSTALLED_APPS = [
# ... some other apps
'rules.apps.AutodiscoverRulesConfig',
# ... some other apps
]
AUTHENTICATION_BACKENDS = [
# ... some other authentication backends
'rules.permissions.ObjectPermissionBackend',
# ... some other authentication backends
]
For the INSTALLED_APPS
config, we can simply add rules
instead of rules.apps.AutodiscoverRulesConfig
to have the Django server to load rules.py
from the Django web config directory. But I prefer to have separate rules.py
files for each Django apps to keep every rule definition localized to the context of its app scope.
In urls.py
, add the following snippet to enable a custom 403 response error handler.
from django.conf.urls import handler403
from django.shortcuts import render
def custom_403_handler(request, exception):
return render(request, '403.html', status=403)
handler403 = custom_403_handler
This will allow us to set up a custom 403 response page template. In my case, I put it in templates/403.html
since the Django project is set up to store the view templates in the templates
directory.
Defining and Using App Authorization Rules
When you set up a Django app, you can now add a rules.py
file in the app directory to define the authorization rules.
import rules
# Define custom authorization rules
#
# We can actually use the built-in rules.is_authenticated instead of defining our own rule for
# this, but bear with it since it's just an example snippet
@rules.predicate
def user_authenticated(user):
return user.is_authenticated
# Register authorization rules
rules.add('site.index', rules.always_allow)
rules.add('site.settings', user_authenticated)
The @rules.predicate
decorator seems to be suggested to be used for our custom rule definitions for django-rules. The source code of the predicate
implementation of django-rules is available here. It basically contains a set of validation sequences to ensure the predicate
functions are properly implemented, along with a few default predicate
functions for basic authorization rules.
What I found pretty convenient is the @permission_required
decorator we can use in our views. The following is a sample view.py
file.
from django.shortcuts import render
from django.http import HttpResponse
from django.template import loader
from django.core import exceptions
from rules.contrib.views import permission_required
@permission_required('site.index', raise_exception=True)
def index(request):
template = loader.get_template('site/index.html')
return HttpResponse(template.render({}, request))
@permission_required('site.settings', raise_exception=True)
def settings(request):
template = loader.get_template('site/settings.html')
return HttpResponse(template.render({}, request))
The @permission_required
decorator can be used to access the rules we registered in the rules.py
file and perform a check using the authorization function we put under the registered rule. In this case, we have registered the site.index
and site.settings
rules in rules.py
before, and we’re now using it to perform a permission check for executing the index
and settings
functions defined in views.py
.
The raise_exception=True
parameter is passed to the @permission_required
decorator to let it throw 403 error response in cases when authorization fails. There are more complex usages of the @permission_required
decorator as described here, where we can have the decorator to automatically retrieve the target object the user intends to access to be checked in the authorization rule.
Conclusion
There are definitely more use cases of the django-rules package that I haven’t explored yet, such as calling the authorization logic from model and from view. It also has a bunch of advanced features I haven’t touched since, in the context of the project I’m currently working on, so far I still haven’t felt the necessity for using them (the application is still relatively simple).
But overall, I’m happy to find that there’s a Django authorization library where I can simply define the authorization rules in the code without having to touch the DB for storing and reading the permissions. Otherwise, I would have had to write one by myself. While that’s definitely doable, it might not be done in the best possible way given that I’m not a Django expert at this point in time.
References
TP-Link Wireless Router ARP List Monitoring
ryanb/cancan: Authorization Gem for Ruby on Rails.
CanCanCommunity/cancancan: The authorization Gem for Ruby on Rails.
pgorecki/django-cancan: Authorization library for Django
dfunckt/django-rules: Awesome Django authorization, without the database