Django permissions demystified

Django permissions is often an overlooked topic by a Django developer. Gain a solid understanding of permissions in this post.

Django permissions demystified

Agenda

This post attempts to understand Django permissions in detail. We will cover the following:

  • What is a permission.
  • Where can permissions be used.
  • How are permissions related to users.
  • How Django admin internally uses permissions.
  • Understanding Permission model.
  • How to create custom permissions and associate them with users.
  • How to check if a user has specific permission.

Permission

Permission is a technique to determine if an action should be allowed or disallowed for a particular user.

Let's consider the Django polls app. You might want to allow voting on a choice only to few users. You might want to disallow voting for certain set of users. Such scenarios need to be handled using permissions.

Similarly you might want to allow viewing voting results for only a certain set of users. This scenario needs to be handled using permissions.

Django permissions are represented by model django.contrib.auth.models.Permission.

Permissions are closely related to models User and ContentType. We will spend few minutes understanding permission's relation with User and ContentType.

User

A Django user has three attributes which work in tandem with permissions.

- is_superuser
- is_staff
- user_permissions

user_permissions is a many-to-many association with User.

Let's create a user named steve.

In [1]: steve = User.objects.create_user(username='steve', password='abc')

In [2]: steve.user_permissions.all()
Out[2]: <QuerySet []>

Till now, we haven't associated any permission with steve. That's why user_permissions.all() returned an empty queryset.

Let's understand how permissions play with Django admin.

Try to login to admin as steve. You wouldn't be allowed. Django would complain with:

Please enter the correct username and password for a staff account.

Make steve a staff.

In [6]: steve.is_staff = True

In [7]: steve.save()

Try to login as steve again. You would be able to login. However you wouldn't see any model listed in the admin. Instead you would see the following message:

You don't have permission to edit anything.

Django allows staff users to login to admin but doesn't show change list or add forms unless permissions are assigned to the staff user.

Let's add a permission for steve.

In [17]: from django.contrib.auth.models import Permission

In [18]: from django.contrib.contenttypes.models import ContentType

In [19]: ct = ContentType.objects.get_for_model(User)

In [20]: permission = Permission.objects.get(content_type=ct, codename='add_user')

In [21]: steve.user_permissions.add(permission)

We will understand model Permission and ContentType in detail in following sections.

Refresh admin, you should see Users in the admin and the '+Add' button for Users.

Clicking on '+Add' will give a 404 with the following message:

Your user does not have the "Change user" permission. In order to add users, Django requires that your user account have both the "Add user" and "Change user" permissions set.

That's why let's assign 'Change user' permission too to steve.

In [22]: permission = Permission.objects.get(content_type=ct, codename='change_user')

In [23]: steve.user_permissions.add(permission)

Now steve would be able to '+Add' as well as 'Change' users.

We used some permissions which weren't created by us. They were created by Django by default. Whenever a model is created, Django creates three default permissions for the model. eg: When model User was created, Django created permissions with codenames add_user, change_user and delete_user.

We assigned the permissions add_user and change_user to steve. This enables steve to create and change other user instances.

ContentType

Django provides a model called ContentType. An instance of ContentType is created for every model in the Django project.

So a ContentType instance is created for User model. For polls app, a ContentType instance would be created for model Question. Similarly another ContentType instance for model Choice and so on.

Let's run a ContentType query.

In [18]: from django.contrib.contenttypes.models import ContentType

In [19]: ct = ContentType.objects.get_for_model(User)

ContentType has following attributes:

  • app_label
  • model
In [24]: ct.app_label
Out[24]: 'auth'

In [25]: ct.model
Out[25]: 'user'

app_label tells the name of app the model is part of. model is a string and tells the model name. get_for_model() is a helper method to easily get the ContentType instance corresponding to the model.

If you have polls app setup, you can get ContentType for Question and Choice.

In [27]: ct = ContentType.objects.get_for_model(Question)

In [28]: ct.app_label
Out[28]: 'polls'

In [29]: ct.model
Out[29]: 'question'

If you have an app called billing having a model Account. You would do something like:

In [27]: ct = ContentType.objects.get_for_model(Account)

In [28]: ct.app_label
Out[28]: 'billing'

In [29]: ct.model
Out[29]: 'account'

Django permissions are always associated to a model. eg: Add a user would be associated with model User. Considering Django polls app, permission Add a question would be associated with model Question.

And since a model is represented by a ContentType, so this essentially means that Permission needs to have a ForeignKey to ContentType.

Permission

Model Permission has following attributes:

  • name
  • codename
  • content_type

As we already discussed, three permissions are created for each content_type by default.

Let's query the permissions created for user content type.

In [34]: ct = ContentType.objects.get_for_model(User)

In [35]: Permission.objects.filter(content_type=ct)
Out[35]: <QuerySet [<Permission: auth | user | Can add user>, <Permission: auth | user | Can change user>, <Permission: auth | user | Can delete user>]>

A permission looks like:

<Permission: auth | user | Can add user>

__str__() of model Permission returns this format. It returns permission.content_type.app_label | permission.content_type.model | permission.name.
.

Let's check different attributes of this permission.

In [36]: permission = Permission.objects.filter(content_type=ct)[0]

In [45]: permission.name
Out[45]: 'Can add user'

In [46]: permission.codename
Out[46]: 'add_user'

In [47]: permission.content_type
Out[47]: <ContentType: user>

Let's query the permissions created for content type of Question.

In [48]: from polls.models import Question, Choice

In [49]: ct = ContentType.objects.get_for_model(Question)

In [50]: Permission.objects.filter(content_type=ct)
Out[50]: <QuerySet [<Permission: polls | question | Can add question>, <Permission: polls | question | Can change question>, <Permission: polls | question | Can delete question>]>

Let's query the permissions created for content type of Choice.

In [51]: ct = ContentType.objects.get_for_model(Choice)

In [52]: Permission.objects.filter(content_type=ct)
Out[52]: <QuerySet [<Permission: polls | choice | Can add choice>, <Permission: polls | choice | Can change choice>, <Permission: polls | choice | Can delete choice>]>

This confirms that 3 default permissions are created for each model.

It's worth mentioning that Permission has a unique_together on content_type and codename. Check it on Github. It's possible that multiple permissions have same codename. But pair (content_type, codename) would always be unique. That's why it's a good practise to filter using both content_type and codename.

That's why, earlier while adding permissions for Steve, we did:

In [19]: ct = ContentType.objects.get_for_model(User)

In [20]: permission = Permission.objects.get(content_type=ct, codename='add_user')

Also name is just a descriptive name for the permission and shouldn't be used to get() or filter() the permissions.

Checking permissions for a user

Most of the times you would assign permissions to user. Permissions can be assigned to Group too which we will see in a bit.

Earlier, we assigned some permissions to steve.

Let's check if steve has permissions to act on User model.

In [48]: steve.has_perm('auth.add_user')
Out[48]: True

In [49]: steve.has_perm('auth.change_user')
Out[49]: True

In [50]: steve.has_perm('auth.delete_user')
Out[50]: False

Model User has a method called has_perm(). While checking for permissions using has_perm() we need to pass the permission's content_type's app_label and permission's codename. These two has to be separated using a dot.

It is worth mentioning that has_perm always returns true when checked against a superuser. We don't have to explicitly assign the permissions to superuser.

Create a superuser named ken.

In [51]: user = User.objects.create_superuser(username='ken', email='[email protected]', password='a#431')

We haven't assigned any permission to ken. Let's check has_perm() on ken.

In [52]: user.has_perm('auth.add_user')
Out[52]: True

Custom permissions

Let's add some custom permissions now.

We will use Django polls app as reference here. Assume you only want to allow certain users to vote on choices.

The concerned models are:

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User)

class Choice(models.Model):
    choice_text = models.CharField(max_length=200)
    question = models.ForeignKey(Question)
    votes = models.IntegerField(default=0)

We will create a permission called can_vote and only users with this permission would be able to vote on a choice. Since this is related to model Choice, so we should be associating this permission with Choice's content type.

In [2]: from django.contrib.contenttypes.models import ContentType

In [3]: from polls.models import Choice

In [4]: ct = ContentType.objects.get_for_model(Choice)

Let's first check the existing permissions for Choice.

In [5]: Permission.objects.filter(content_type=ct)
Out[5]: <QuerySet [<Permission: polls | choice | Can add choice>, <Permission: polls | choice | Can change choice>, <Permission: polls | choice | Can delete choice>]>

As expected, there are 3 existing permissions.

Let's create the new permission.

In [7]: permission = Permission.objects.create(content_type=ct, name='Can vote on choice', codename='can_vote_choice')

Let's ensure that the new permission was successfully created.

In [10]: Permission.objects.filter(content_type=ct)
Out[10]: <QuerySet [<Permission: polls | choice | Can add choice>, <Permission: polls | choice | Can vote on choice>, <Permission: polls | choice | Can change choice>, <Permission: polls | choice | Can delete choice>]>

4 permissions are associated with Choice. This confirms that permission was successfully created.

Check if steve has permission to vote on choices.

In [11]: steve.has_perm('polls.can_vote_choice')
Out[11]: False

Let's grant this permission to steve.

In [12]: steve.user_permissions.add(permission)

Check steve's permission again.

In [14]: steve = User.objects.get(pk=steve.pk)

In [15]: steve.has_perm('polls.can_vote_choice')
Out[15]: True

NOTE: We fetched steve again from the database because permissions are cached on the user instance. You must fetch the user from the db to have the newly assigned permissions reflected on the user.


TIP: instead of having to explicitly re-fetch the particular instance of a Model from the DB, you can add a helper method to the model which would do the work:

def reload(self):
    new_self = self.__class__.objects.get(pk=self.pk)
    # You may want to clear out the old dict first or perform a selective merge
    self.__dict__.update(new_self.__dict__)

Now when you need to update the reference to a model object with the latest data in the DB, you can do so:

steve.reload()


Your voting view code can have has_perm() line to ensure that only users with permission are allowed to vote. It would look something like:

@login_required
def vote(request, choice_pk):
    if request.user.has_perm('polls.can_vote_choice'):
        choice = get_object_or_404(Choice, pk=choice_pk)
        choice.votes += 1
        choice.save()
    else:
        return HttpResponse('Unauthorized', status=401)

There is also a method called get_all_permissions() on User which can come handy in some scenarios. Let's find all permissions granted to steve.

In [16]: steve.get_all_permissions()
Out[16]: {'auth.add_user', 'auth.change_user', 'polls.can_vote_choice'}

Let's assume we have a page which allows logged in users to create questions and choices. In case we want to restrict question creation to certain users, our view code could use polls.add_question.

def create_question(request):
    if request.user.has_perm('polls.add_question'):
        # allow creating question
    else:
        # return Unauthorized

In this case, we could use Django created polls.add_question itself and didn't have to create a custom permission.

Groups and permissions

It would be cumbersome to grant permissions to users individually in case you want to grant a permission or a set of permissions to multiple users. Groups come to rescue there.

Let's create two users named bill and mark.

In [17]: bill = User.objects.create_user(username='bill', password='abc')

In [18]: mark = User.objects.create_user(username='mark', password='abc')

We want to grant permission to bill as well as mark to vote on choices. We can create a Group called Allowed to vote, associate the users with this group and grant permission on the group.

Let's first ensure that the new users don't have permission to vote.

In [20]: bill.has_perm('polls.can_vote_choice')
Out[20]: False

In [21]: mark.has_perm('polls.can_vote_choice')
Out[21]: False

Create group and associate with users.

In [22]: group = Group.objects.create(name='Allowed to vote')

In [23]: group.user_set.add(bill)

In [24]: group.user_set.add(mark)

Grant this group permission to vote.

In [25]: group.permissions.add(permission)

Check again if the permission has been granted to bill and mark by virtue of belonging to this group.

In [27]: mark = User.objects.get(username='mark')

In [28]: mark.has_perm('polls.can_vote_choice')
Out[28]: True

In [29]: bill = User.objects.get(username='bill')

In [30]: bill.has_perm('polls.can_vote_choice')
Out[30]: True

As you can notice, has_perm() returns True for both bill and mark even though we didn't explicitly grant them the permissions.

Permission using model meta

In last few sections we created custom permission using Permission.objects.create(), there is another way permission can be created.

code_name and name for the permission can be provided in model's Meta.permissions and these permissions will then be created by Django.

Say we want to add a custom permission called can_add_multiple_questions using which we will determine if a user can create multiple questions. We can do the following in such case.

class Question(models.Model):
    ...
    model fields
    ...

    class Meta:
        permissions = [('can_add_multiple_questions', 'Can add multiple questions')]

We can then run makemigration and migrate to ensure that an instance of Permission is created in the database. And then we can associate this permission with any user.

We can then use user.has_perm('polls.can_add_multiple_questions') in the view to decide if a user can create multiple questions.

Hope this knowledge makes working with permissions more straightforward for you.