Photo by FLY:D on Unsplash

Django: How to quickly add permissions to CBVs

Adrien Van Thong

--

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.

Resources

--

--