Django: How to quickly add permissions to CBVs
In this article, I will explore how to integrate permissions management with class-based-views (CBVs) in Django.
Like most concepts in Django, the solution is straightforward, though it is important to first establish some of the concepts beforehand.
Default Django Permission schemes
Django automatically creates permission schemes for each model in your project, following a very standard and predictable scheme. Each permission follows the naming scheme APP.ACTION_MODEL
where APP
is the name of the Django app, ACTION
describes one of view, add, change, or delete, and MODEL
is the model name. For example, warehouseapp.change_pallet
would be the name of a permission allowing users to edit the Pallet
model in the warehouseapp
app. These permissions are automatically created for us, and also automatically enforced in the Django admin pages.
Administrators can manage which users are assigned these permissions via the Django admin page either by editing a single user’s permissions or group’s permissions (to change all members’ permissions). Users with the “superuser status” are automatically granted all permissions in perpetuity.
Enforcing permissions on CBVs
While Django automatically enforces these permissions on the Django admin by default, it is up to us, the app developers, to enforce them on our class based views wherever we deem appropriate.
Luckily, Django’s CBV provide a very simple way to this. All we need to do is make sure our CBV inherits from PermissionRequiredMixin
, then set the permission_required
field, which is a tuple of all the permissions a user must have to use this view:
from django.views.generic import UpdateView
from django.contrib.auth.mixins import PermissionRequiredMixin
class PalletUpdateView(PermissionRequiredMixin, UpdateView):
model = Pallet
permission_required = ('warehouseapp.change_pallet')
template_name = 'update_pallet.html'
With this field set, the user must have all the listed permissions in order to be able to use this CBV.
Any user who browses to this view without the appropriate permissions will be presented with your Django app’s customized“restricted” page.
Checking permissions
There are times where it may be useful to check or enforce permissions within our views. This is where the has_perm
and get_all_permissions
methods on the User
model come in handy. As seen in the shell output below, the has_perms
method takes in the permission name and returns True or False whether that user has the given permission. The get_all_permissions
method is very handy for debugging user permissions as it lists all permissions for the user.
>>> from django.contrib.auth.models import User
>>> admin = User.objects.first()
>>> admin
<User: admin>
>>> admin.has_perms('warehouseapp.view_pallet')
True
>>> admin.get_all_permissions()
{'auth.delete_user', 'sessions.change_session', 'admin.add_logentry', 'contenttypes.delete_contenttype', 'warehouseapp.delete_warehouse', 'warehouseapp.view_pallet', 'auth.view_group', 'warehouseapp.change_warehouse', 'contenttypes.add_contenttype', 'auth.change_permission', 'admin.delete_logentry', 'auth.change_user', 'auth.add_permission', 'warehouseapp.delete_pallet', 'auth.view_permission', 'warehouseapp.change_pallet', 'admin.view_logentry', 'auth.delete_group', 'auth.change_group', 'contenttypes.change_contenttype', 'contenttypes.view_contenttype', 'sessions.delete_session', 'admin.change_logentry', 'auth.delete_permission', 'auth.add_group', 'auth.add_user', 'warehouseapp.add_warehouse', 'auth.view_user', 'warehouseapp.view_warehouse', 'warehouseapp.add_pallet', 'sessions.view_session', 'sessions.add_session'}
This can be coupled with the request
object in a CBV as outlined in the example below, however in most cases using the permission_required
field on the CBV as outlined in the prior example is the most efficient way to enforce permissions on views:
from django.views.generic import UpdateView
class PalletUpdateView(UpdateView):
model = Pallet
template_name = 'update_pallet.html'
def form_valid(self, form):
if self.request.user.has_perms('warehouseapp.change_pallet'):
# Do something
Checking permissions in templates
Now that we’re enforcing permissions at the view level, we’ll want to update our templates to stop displaying certain links/buttons on the UI where users don’t have access to those views. For example, in the template below the “edit” link is only shown on the page if the user has the permission to edit that model:
<ul>
{% for pallet in pallets %}
<li>
{{ pallet.name }}
{% if perms.warehouseapp.change_pallet %}
<a href="{% url 'edit_pallet' pallet.pk %}">Edit</a>
{% endif %}
</li>
{% endfor %}
</ul>
The perms
object follows the same format as before: perms.APP_NAME.ACTION_MODELNAME
for the example above the app name is warehouseapp
the action is change
(for editing) and the model is pallet
.
Creating custom permissions
While Django automatically creates these built-in permissions for us and automatically enforces them for us, we also have the capability to define our own permissions on our models and enforce them as we wish.
To create a new permission for a model, simply add it to the model’s Meta
class using the permissions
list of tuples, as seen below:
class Pallet(models.Model):
name = models.CharField(max_length=64)
contents = models.TextField(null=True, blank=True)
source_address = models.TextField(null=True, blank=True)
dest_address = models.TextField(null=True, blank=True)
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
class Meta:
permissions = [
(
"can_change_warehouse",
"Can change which warehouse a pallet is stored in"
)
]
This code blocks creates a new permission (in addition to the existing ones) for this model only. After running the makemigration
and migrate
commands, this model now has an additional permission:
>>> from django.contrib.auth.models import Permission
>>> from django.contrib.contenttypes.models import ContentType
>>> from .models import Pallet
>>> palletmodel = ContentType.objects.get_for_model(Pallet)
>>> Permission.objects.filter(content_type=palletmodel)
<QuerySet [<Permission: warehouseapp | pallet | Can add pallet>, <Permission: warehouseapp | pallet | Can change which warehouse a pallet is stored in>, <Permission: warehouseapp | pallet | Can change pallet>, <Permission: warehouseapp | pallet | Can delete pallet>, <Permission: warehouseapp | pallet | Can view pallet>]>
Custom permissions can be checked and enforced the same way as all other Django permissions:
>>> from django.contrib.auth.models import User
>>> admin = User.objects.first()
>>> admin
<User: admin>
>>> admin.has_perms('warehouseapp.can_change_warehouse')
True
That’s it! Django once again provides us with simple and elegant solutions to common problems.