Home Django Django roles, permissions, and groups

Django roles, permissions, and groups

Permissions are a set of rules (or constraints) that allow a user or a group of users to view, add, alter, or delete items in Django. Django has a permissions system built-in. It enables you to provide permissions to individual users or groups of users.

Using django.contrib.auth, Django provides several default permissions.

When you include django.contrib.auth in your INSTALLED APPS option, it will create four default permissions for each Django model defined in one of your installed applications: add, change, delete, and view.

This article seeks to provide a concise and easy-to-understand overview of Django roles, permissions, and groups. To do so, I’ll go through everything you need to know and understand about permissions, groups, and roles, and then I’ll look at some code samples.

Permissions in Django

Key things to understand to better use Permissions:

  • Users
  • Permissions
  • Groups
  • Users Roles

To comprehend when these permissions are appropriate, we must first grasp the difference between authentication and authorization.

Authentication vs. authorization: what’s the difference?

Authentication involves verifying that the user’s credentials, such as email and password, are correct. Authorization, on the other hand, specifies what the user is allowed to do within the application.

What is the meaning of user roles?

Suppose you’re establishing a Django project with many user types, for example, a School project with multiple roles such as a teacher, accountant, librarian, and so on. In that case, you’ll need a separate job for each individual to categorize.

What are the benefits of using user roles?

To use roles in your models, you must extend AbstractUser most straightforwardly.

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
  TEACHER = 1
  ACCOUNTANT = 2
  LIBRARIAN =3
  

   ROLE_CHOICES = (
      (TEACHER, 'Teacher'),
      (ACCOUNTANT, 'Accountant'),
      (LIBRARIAN, 'Librarian'),
  )
  role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, blank=True, null=True)
  # You can create Role model separately and add ManyToMany if user has more than one role

To check the user role, run the following set of commands.

user = User.objects.first()
user.role = User.TEACHER
user.save()
user.role == User.ACCOUNTANT
False
user.role == User.LIBRARIAN
True

Assuming you have an app with an app_label or app name School and a Vote model, you can use the code below to see which users have the necessary permissions.

python manage.py shell
user = User.objects.first()

add: user.has_perm('school.add_vote')
change: user.has_perm('school .change_vote')
delete: user.has_perm('school .delete_vote')
view: user.has_perm('school .view_vote')

Alternatively, you can use the decorator to confirm if the current user has permission to execute the function or not.

from django.contrib.auth.decorators import permission_required
@permission_required('school.add_vote')
def your_func(request):
  """or you can raise permission denied exception"""

The other approach is to use the class, PermissionRequiredMixin just in case you use a class-based view.

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView
class VoteListView(PermissionRequiredMixin, ListView):
  permission_required = 'school.add_vote'
  # Or multiple of permissions<br>permission_required = ('school .add_vote', 'school .change_vote')

The syntax for checking if the given user has the necessary permissions in the template is as follows.

{% if perms.app_label.can_do_something %}
{% if perms.school.add_vote %}

Alternatively, you can use the if condition within your function to check if the user has the necessary permissions.

user.has_perm('vote.change_vote')

And, from the abstract Permission model PermissionsMixin, this User(AbstractUser) model has a ManyToMany field named user permissions. Below are the methods for assigning and removing permission from or to the user.

user.user_permissions.set([permission_list])
user.user_permissions.add(permission, permission, …)
user.user_permissions.remove(permission, permission, …)
user.user_permissions.clear()

The syntax for fetching all the permissions for users is as follows.

user.user_permissions.all()

Permissions on a model-by-model basis

If you’re not happy with the default permissions in Django, you can create your custom permissions.

On the Vote model, for example, I added two extra special permissions.

from django.db import models
class Vote(models.Model):
  user = models.ForeignKey(User)
class Meta:
    permissions = (
                   ("view_student_reports", "can view student reports"),
                   ("find_student_vote", "can find a student's vote"),
                  )

Remember to run makemigrations and migrate the changes to the model.

How to create custom permissions programmatically

from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType


content_type = ContentType.objects.get_for_model(Vote)
permission = Permission.objects.create(
codename='can_see_vote_count',
name='Can See Vote Count',
content_type=content_type,
)

Let’s look at the following example to understand permission caching.

#be sure every permission must be unique(set, duplicate not allowed)

user.has_perm('school.find_owner') # False,because no perm available

# create permission

content_type = ContentType.objects.get_for_model(Vote)
permission = Permission.objects.get(
codename='find_owner',
content_type=content_type,
)

# assign new permission to user user.user_permissions.add(permission)
# Checking the cached permission set
user.has_perm('school .find_owner') # False, because of cache problem.

# get new instance of User
# Be aware that user.refresh_from_db() won't clear the cache.
user = get_object_or_404(User, pk=user_id)

# Permission cache is repopulated from the database
user.has_perm('myapp.find_owner') # True

Note that superuser has all permissions (has_perm will return True), but you can change them to meet your project’s needs.

# source code verification
# Active superusers have all permissions.

if self.is_active and self.is_superuser:
  return True
	

An object, an instance, a record, or a row in a database table Permission

Django guardian library is designed to grant object or instance level permission. So, when you build a model, Django generates four permissions, and they are not available to the user until they are assigned.

We need to restrict users from modifying other users’ records because they have the permissions. For example, if the admin gives change_vote permission to one user, that user should not change all the votes that belong to other users. As a result, we need to restrict the user from modifying other user records for this purpose.

Groups in Django

As the name implies, groups can contain a list of permissions or is simply a collection of items.
A group is also a collection of users. A single user can belong to many groups, while a single group can have multiple users. Groups can also label users.

django.contrib.auth.models

Group models are a generic means of categorizing users so that permissions or other labels can be applied. A user can be a member of as many groups as they choose.

A user who is a member of a group inherits the group’s permissions. If the group site editors have permission to edit the home page, for example, any user in that group will have the ability.

Rather than maintaining permissions for each user, which is not scalable, it is preferable to have Groups such as Teacher, Accountant, and Librarian. Then assign users to these groups or multiple groups. Subsequently, check these user permissions using the syntax mentioned above (has_perm).

If a user is a group member, that groups’ permissions will be passed down to that user.

To limit permissions, you can form groups such as Teacher1, Teacher2, Accountant1, Accountant2, etc.

How to Create Groups

from django.contrib.auth.models import Group
teacher_group, created = Group.objects.get_or_create(name='Teacher')

The next step involves assigning the given set of permissions to a specified group.

teacher_group.permissions.set([permission_list])
teacher_group.permissions.add(permission, permission, …)
teacher_group.permissions.remove(permission, permission, …)
teacher_group.permissions.clear()

Assigning a single user to groups

teacher_group.user_set.add(user)

OR

user.groups.add(teacher_group)

How to find a given user in the group

def is_teacher(user):
  return user.groups.filter(name='Teacher').exists()

from django.contrib.auth.decorators import user_passes_test

@user_passes_test(is_teacher)
def my_view(request):
pass

# Check user in the group in the template
# you can use a custom template tag to check users in a group or groups

Groups vs. Roles

Most of the time, roles and groups are referred to interchangeably, but their functions are distinct, and most users have numerous roles and groups. Therefore, when creating a position, it’s better to use the same group name each time.

My idea is to use roles to display different HTML pages to users based on their category and use groups for permissions.

Models

The permissions field in the model’s “class Meta” section is used to define permissions. You can declare as many permissions as you need in a tuple, with each permission described as a nested tuple with the permission name and permission display value. For instance, we might create permission that allows a user to mark a book as returned, as shown:

class BookInstance(models.Model):
  
  …
  
  class Meta:
    
    …
    
    permissions = (("can_mark_returned", "Set book as returned"),)

The permission might then be assigned to a “Librarian” group in the Admin site.

As seen above, open catalog/models.py and add the permission. To update the database, you’ll need to re-run your migrations as follows.

python3 manage.py makemigrations
python3 manage.py migrate

Templates

The current user’s permissions are saved in the perms template variable. You can use the exact variable name within the linked Django “app” to check whether the current user has specific permission — for example, perms.catalog.can mark returned will be True if the user has this permission, and False otherwise. We usually use the template percent if percent tag to check for permission.

{% if perms.catalog.can_mark_returned %}


{% endif %}

Views

The permission required decorator can be used to test permissions in a function view, and the PermissionRequiredMixin can be used to test permissions in a class-based view. The pattern is the same as that for login authentication. However, you may need to add several permissions in some cases.

Decorator for the function view:

from django.contrib.auth.decorators import permission_required

@permission_required('catalog.can_mark_returned')
@permission_required('catalog.can_edit')

def my_view(request):
  …

Example of A permission-required mixin for class kind of views.

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
  permission_required = 'catalog.can_mark_returned'
  # Or multiple permissions
  permission_required = ('catalog.can_mark_returned', 'catalog.can_edit')
  # Note that 'catalog.can_edit' is just an example
  # The catalog application doesn't have such permission!

Note that the behavior described above has a slight default difference. For a logged-in user with permission violation, the following is the default behavior:

@permission required takes you to the login page (HTTP Status 302). On the other hand, PermissionRequiredMixin returns a 403 Forbidden error whose HTTP Status is Forbidden.

The PermissionRequiredMixin behavior is what you want: If a user is logged in but does not have the appropriate permissions, return 403. Use @login_required and @permission_required with raise_exception=True for a function view, as shown:

from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required('catalog.can_mark_returned', raise_exception=True)
def my_view(request):
  
  …

Example how to Authenticate and Apply Permissions

I’ll teach you how to let people log in with their accounts on your site, as well as how to limit what they can do and see based on whether or not they’re authenticated and have the necessary permissions.

Django provides an authentication and authorization system that allows you to validate user credentials and define what actions each user is permitted to execute. Built-in models for Users and Groups have permissions or flags that designate whether a user may execute a task, forms, and views for logging in users. It also determines view tools for restricting contingency in the framework.

This article will also cover how to activate user authentication on a school website, create custom login and logout pages, add permissions to your models, and manage page access.

I’ll utilize authentication/permissions to show both users’ and librarians’ listings of books that have been borrowed.

The authentication mechanism is quite versatile, and you can create your URLs, forms, views, and templates by just contacting the given API to log in to the user. However, this tutorial will use Django’s authentication views and forms for our login and logout pages.

I’ll still need to make some templates, but it should be pretty straightforward.

I’ll show you how to create permissions and verify login status and permissions in both views and templates.

Note: When we used the django-admin startproject command to create the app, all necessary configurations were done for us. When we first used python manage.py migrate, user database tables and model permissions were automatically created.

The setup is in the project file (school/school/settings.py) under the INSTALLED APPS and MIDDLEWARE sections, as shown below:

INSTALLED_APPS = [
  …
  
  'django.contrib.auth', 
  
  #Core authentication framework and its default models.
  
  'django.contrib.contenttypes', 
  
  #Django content type system (allows permissions to be associated with models).
  ….
  
  MIDDLEWARE = [
    
  …
    
  'django.contrib.sessions.middleware.SessionMiddleware', #Manages sessions across requests
    
  …
 
  'django.contrib.auth.middleware.AuthenticationMiddleware', #Associates users with requests using sessions.
    
  .…

User and group creation

First, create your first user (a superuser), which can be created with the command:

python manage.py createsuperuser

I’ll need to build a test user to represent a regular site user because our superuser is already authenticated and has all capabilities. Next, I’ll create our local library groups and website logins through the admin site because it’s one of the quickest ways to do so.

Users can also be created programmatically, as seen in the example below. For instance, if you’re creating an interface that allows users to create their logins (you shouldn’t offer them access to the admin site), you’ll need to do this on their behalf.

When beginning a project, it is highly advised that you create a custom user model. Then, if the necessity arises, you’ll be able to customize it in the future quickly.

I’ll start by creating a group, then a user. Even though we don’t have any permissions for our library members yet, adding them to the group rather than separately to each member will make it easier if we need to later.

Start the development server and go to the admin site (http://127.0.0.1:8000/admin/) in your local web browser. Use the credentials for your superuser account to log in to the site.

All of your models are listed on the top level of the Admin site, categorized by “Django application.” Click the Users or Groups buttons in the Authentication and Authorization section to access their existing records.

Group Creation

Let’s start by making a new group for our library users.

To create a new Group, click the Add button (next to Group) and give it the name “Librarian”.

We don’t need any permissions for the group, so just press SAVE, and you will be taken to a list of groups.

Since we do not require any permissions for the group, go ahead and press SAVE. The last step will now show the entire list of groups.

User Creation

Next is the user creation step. But, first, return to the admin site’s home page.

To enter the Add User dialogue box, click the Add button next to Users.

For your test user, enter an appropriate Username and Password/Password Confirmation.

To create the user, press SAVE.

The admin site will create a new user and lead you to a Modify user screen where you may change your username and fill out the optional sections for the User model. The first name, last name, email address, and the user’s status and rights are all included in these fields (only the Active flag should be set).

You can also specify the user’s groups and rights further down and see relevant dates about the person, such as their join date and last log in.

Select the Librarian group from the Available groups’ list in the Groups section, then press the right-arrow between the boxes to move it to the Chosen groups box.

We don’t need to do anything else here, so select SAVE once more to return to the user list.

You now have a “librarian” account that you may use for testing. You can do this once the pages to allow them to log in have been implemented.

Configuring the authentication views

Django comes with practically everything you’ll need to build authentication pages that handle login, logout, and password management. It includes a URL mapper, views, and forms, but not templates, which we must develop ourselves.

I’ll show you how to integrate the default system into the School website and create templates in this section. Then, I’ll include them in the project’s main URLs.

URLs for projects

To the bottom of the project urls.py file (school/school/urls.py), add the following:

# Add Django site authentication URLs (for login, logout, password management)

urlpatterns += [
path('accounts/', include('django.contrib.auth.urls')),
]

Go to http://127.0.0.1:8000/accounts/ (notice the trailing forward slash!) and type in your username and password. Django will display an error message stating that it was unable to locate this URL, as well as a list of all the URLs it attempted. You can view the URLs that will function, for example:

accounts/ login/ [name='login']
accounts/ logout/ [name='logout']
accounts/ password_change/ [name='password_change']
accounts/ password_change/done/ [name='password_change_done']
accounts/ password_reset/ [name='password_reset']
accounts/ password_reset/done/ [name='password_reset_done']
accounts/ reset/// [name='password_reset_confirm']
accounts/ reset/done/ [name='password_reset_complete']

Note: Using the above approach adds the following URLs, which can reverse the URL mappings, with names in square brackets. You don’t need to do anything further because the above URL mapping will automatically map the URLs listed below.

Now go to http://127.0.0.1:8000/accounts/login/ and try to log in. It will fail again, but this time with an error stating that the required template (registration/login.html) is missing from the template search path. In the yellow part at the top, you’ll see the following lines:

  • TemplateDoesNotExist is an exception type.
  • registration/login.html is an exceptional value.

The next step is to add the login.html file to the registration directory on the search path.

Directory of Templates

The newly added URLs (and indirectly, views) seek to find their related templates in the /registration/ directory somewhere in the templates search path.

I’ll place our HTML pages in the templates/registration/ directory for this site. This directory should be the same as the catalog and school folders in your project root directory. So first, make these folders right away.

In addition, we must include the templates directory in the template search path to see the template loader. (/school/school/settings.py)

Open the project settings. After that, import the os module and add the following line near the top of the file.

import os # needed by code below

Update the ‘DIRS’ line in the TEMPLATES section as follows:

...
TEMPLATES = [
  {
   ...
   'DIRS': [os.path.join(BASE_DIR, 'templates')],
   'APP_DIRS': True,
   …

Login template

Make a new HTML file called /school/templates/registration/login.html and fill it with the following information:

{% extends "base_generic.html" %}

{% block content %}

  {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
  {% endif %}

  {% if next %}
    {% if user.is_authenticated %}
      <p>Your account doesn't have access to this page. To proceed,
      please login with an account that has access.</p>
    {% else %}
      <p>Please login to see this page.</p>
    {% endif %}
  {% endif %}

  <form method="post" action="{% url 'login' %}">
    {% csrf_token %}
    <table>
      <tr>
        <td>{{ form.username.label_tag }}</td>
        <td>{{ form.username }}</td>
      </tr>
      <tr>
        <td>{{ form.password.label_tag }}</td>
        <td>{{ form.password }}</td>
      </tr>
    </table>
    <input type="submit" value="login" />
    <input type="hidden" name="next" value="{{ next }}" />
  </form>

  {# Assumes you setup the password_reset view in your URLconf #}
  <p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

This template extends our base template and replaces the content block. The rest of the code is standard form handling code. For the time being, all you need to know is that this will bring up a form where you may enter your username and password. Subsequently, if you input invalid values, you will be prompted to enter proper ones when the page refreshes.

Once you’ve saved your template, go back to the login page (http://127.0.0.1:8000/accounts/login/), and you should see something like this:

You’ll be forwarded to another page if you log in with the correct credentials (by default, http://127.0.0.1:8000/accounts/profile/). The issue is that Django assumes that when you log in, you want to go to your profile page, which may or may not be the case. You’ll get another error because you haven’t defined this page yet!

Add the text below at the bottom of the project settings (/school/school/settings.py). By default, you should now be taken to the site homepage when you log in.

Redirect to home URL after login (Default redirects to /accounts/profile/)
LOGIN_REDIRECT_URL = '/'

Template for logging out

If you go to the logout URL (http://127.0.0.1:8000/accounts/logout/), you’ll notice something strange: your user will be logged out, but you’ll be taken to the Admin logout page instead. That’s not what you want, even if the login link on that page gets you to the Admin login screen. Unfortunately, the admin login is only accessible to people with is_staff permission.

/school/templates/registration/logged_out.html should be created and opened. Fill in the blanks with the following text:

{% extends "base_generic.html" %}

{% block content %}
  <p>Logged out!</p>
  <a href="{% url 'login'%}">Click here to login again.</a>
{% endblock %}

It is a pretty basic template. It just shows you a notification alerting you that you have been logged out, along with a link to return to the login screen. If you go back to the logout URL, you should see the following page:

Template for password reset

The default password reset system sends a reset link to the user through email. You’ll need to build forms to collect the user’s email address, send the email, allow them to change their password, and keep track of when everything is finished.

As a starting point, the following templates can be used.

It is the form used to obtain the user’s email address for sending the password reset email. Create /school/templates/registration/password_reset_form.html, and give it the following contents:

{% extends "base_generic.html" %}

{% block content %}

{% csrf_token %} {% if form.email.errors %} {{ form.email.errors }} {% endif %}

{{ form.email }}

{% endblock %}

Testing new authentication pages

The login pages should now automatically function after adding the URL configuration and producing all of these templates!

You can try logging in and out of your superuser account using these URLs to test the new authentication pages:

  • http://127.0.0.1:8000/accounts/login/
  • http://127.0.0.1:8000/accounts/logout/

You can test the password reset feature by clicking the link on the login page. Remember that Django will only send reset emails to addresses (users) that it already has on file!

Validation with authenticated users

This section looks at how we can restrict what content users see based on whether or not they are logged in.

Using templates for testing

The user template variable can be used in templates to acquire information about the currently logged-in user (this is added to the template context by default when you set up the project as we did in our skeleton).

To check whether the user can see specific content, you’ll usually start using the user.is_authenticated template variable. Next, we’ll edit our sidebar to show a “Login” link if the user is logged out and a “Logout” link if they are logged in.

Copy the following text into the sidebar block, directly before the endblock template tag, in the base template (/school/catalog/templates/base_generic.html).

  <ul class="sidebar-nav">

    ...

   {% if user.is_authenticated %}
     <li>User: {{ user.get_username }}</li>
     <li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
   {% else %}
     <li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
   {% endif %}
  </ul>

We utilize if-else-endif template tags to conditionally display content depending on whether {{user.is_authenticated }} is true, as you can see. We know we have a valid user if the user is authorized, so we call {{user.is_authenticated }} to display their name.

We generate the login and logout link URLs using the URL template tag and the names of the corresponding URL configurations. Take note of how we’ve added ?next={{request.path}} to the end of the URLs. It adds a URL parameter at the end of the linked URL containing the current page’s address (URL).

Testing in views

Applying the login required decorator to your view function, as shown below, is the easiest way to restrict access to your functions if you’re using function-based views. Your view code will normally run if the user is logged in. If the user is not logged in, the current absolute path will be passed as the next URL parameter to the login URL defined in the project settings (settings.LOGIN_URL).

If the user successfully logs in, they will be returned to this page, but they will be authenticated this time.

    ...

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    ...

Deriving from LoginRequiredMixin is also the simplest way to restrict access to logged-in users in your class-based views. This mixin must be declared before the main view class in the superclass list.

…

from django.contrib.auth.mixins import LoginRequiredMixin

class MyView(LoginRequiredMixin, View):
    …

class MyView(LoginRequiredMixin, View):
    login_url = '/login/'
    redirect_field_name = 'redirect_to'
…

The login required decorator has the same redirect behavior like this one. You can also specify a URL parameter name instead of “next” to insert the current absolute path (redirect field name) and an alternative location to redirect the user to if they are not authenticated (login_url).

Conclusion

In this article, I have introduced permissions and groups and compared roles with groups, covering all essentials. Please keep in mind that roles (such as a teacher) and groups (such as teacher group 1 and teacher group 2, where the teacher1 group only has limited ability to view first-reports for students but cannot modify the students’ score) are two separate things.

With a simple example, I covered the principles and how they work.

I sincerely hope that this article has helped you better understand permissions and groups. Please feel free to ask any questions you may have, and thank you.

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More