Understanding Django Permissions and Groups

Blog / Django · March 21, 2023 · Updated June 10, 2026 · 7 min read
Understanding Django Permissions and Groups

Django ships with a complete authorization system out of the box. It is built on two ideas: permissions (fine-grained flags such as "can change a car") and groups (named bundles of permissions that represent roles). Every model automatically gets four permissions — add, change, delete and view — and you test them with user.has_perm('app_label.codename'). Superusers pass every check automatically, and for genuine per-row rules you add a library such as django-guardian.

This guide covers the whole picture for Django 5.x and Python 3.11+: the auth model, assigning permissions to users and groups, custom permissions, enforcing access in views, templates and Django REST Framework, and object-level permissions. At MicroPyramid we have shipped Django authorization across 50+ projects in 12+ years, so these are the patterns we reach for in production. If you want a hand, see our Django development services and broader Python development services.

The Django auth permission model

When you run python manage.py migrate, Django scans every installed model and, via the post_migrate signal, creates four model-level permissions for each one:

  • add_<model> — create a row
  • change_<model> — edit a row
  • delete_<model> — remove a row
  • view_<model> — read a row (added in Django 2.1; many older tutorials omit it)

Each permission is a row in the auth_permission table linked to the model's ContentType. The string you pass to has_perm() is always "<app_label>.<codename>", where the codename is <action>_<modelname> in lowercase. So a Car model in an app called drivers produces drivers.add_car, drivers.change_car, drivers.delete_car and drivers.view_car.

# drivers/models.py
from django.db import models


class Car(models.Model):
    name = models.CharField(max_length=100)


# After `manage.py migrate`, Django auto-creates four permissions for Car:
#   drivers.add_car
#   drivers.change_car
#   drivers.delete_car
#   drivers.view_car      # added in Django 2.1

# The argument to has_perm() is always "<app_label>.<codename>"
user.has_perm("drivers.view_car")
user.has_perm("drivers.change_car")

Assigning permissions to users

Direct permissions live on the user.user_permissions many-to-many relation. Use add(), set(), remove() and clear() to manage them, and has_perm() / has_perms() to check them. Two short-circuits matter:

  • Superusers (is_superuser=True) return True for every has_perm() call, even with no explicit permission.
  • Inactive users (is_active=False) return False for permission checks.
from django.contrib.auth.models import Permission
from django.contrib.auth import get_user_model

User = get_user_model()
user = User.objects.get(username="alice")
perm = Permission.objects.get(content_type__app_label="drivers", codename="change_car")

# Manage direct permissions
user.user_permissions.add(perm)
user.user_permissions.set([perm])
user.user_permissions.remove(perm)
user.user_permissions.clear()

# Check one or many permissions
user.has_perm("drivers.change_car")
user.has_perms(["drivers.change_car", "drivers.delete_car"])

# Superuser short-circuit: always True
admin = User.objects.get(username="root")
admin.has_perm("drivers.change_car")  # True even without the explicit perm

The permission cache gotcha

Django caches a user's permissions on the in-memory user instance for the lifetime of the request. If you check a permission, grant it, then check again on the same object, you still get the old cached answer. Re-fetch the user from the database to clear the cache (or grant the permission before the first check).

user.has_perm("drivers.change_car")   # False -> result is now cached
user.user_permissions.add(perm)       # grant it during this request
user.has_perm("drivers.change_car")   # STILL False (stale cache)

# Fix: reload the user so the permission caches are rebuilt
user = User.objects.get(pk=user.pk)
user.has_perm("drivers.change_car")   # True

Groups: role-based access control

A Group is simply a named collection of permissions. Attach permissions to the group once, then add users to the group; every member inherits the group's permissions on top of their own direct ones.

Prefer groups over per-user permissions for almost everything. Roles like "Editor", "Manager" or "Support" map cleanly to groups: you grant the permission set in one place, and onboarding or changing a role is a single user.groups.add(...) call rather than dozens of individual grants. Reserve direct user_permissions for rare, person-specific exceptions.

from django.contrib.auth.models import Group, Permission
from django.contrib.auth import get_user_model

User = get_user_model()

editors = Group.objects.create(name="Editors")

# Attach a permission set to the ROLE, not to each person
perms = Permission.objects.filter(
    content_type__app_label="drivers",
    codename__in=["add_car", "change_car", "view_car"],
)
editors.permissions.set(perms)

# Put a user in the group; they inherit every permission it holds
alice = User.objects.get(username="alice")
alice.groups.add(editors)

alice.has_perm("drivers.change_car")  # True, inherited from "Editors"

Custom permissions

CRUD flags rarely capture real business rules like "can publish" or "can export". Declare extra permissions in the model's Meta.permissions as (codename, human_readable_name) pairs. You can also use Meta.default_permissions to trim or disable the automatic add/change/delete/view set — pass an empty tuple () to create none.

After editing Meta, run python manage.py makemigrations and python manage.py migrate; Django writes the new rows into auth_permission. To add permissions to an already-migrated model without touching its fields, create a data migration with RunPython and Permission.objects.create(...). Keep codenames in the verb_noun form (for example publish_car) to stay consistent with the built-in naming.

class Car(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        # Extra, non-CRUD permissions for domain actions
        permissions = [
            ("publish_car", "Can publish car listing"),
            ("export_car", "Can export car data"),
        ]
        # Control the auto-created set; use () to create none of them
        default_permissions = ("add", "change", "delete", "view")


# Check a custom permission like any other
user.has_perm("drivers.publish_car")

Enforcing permissions in views

For function-based views, stack @login_required with @permission_required. Pass raise_exception=True so an authenticated-but-unauthorised user gets a 403 instead of being bounced to the login page. For class-based views, mix in LoginRequiredMixin and PermissionRequiredMixin and set the permission_required attribute (a single string or a list). When you need arbitrary logic rather than a named permission, UserPassesTestMixin with a test_func() is the escape hatch.

from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import UpdateView

from .models import Car


# Function-based view
@login_required
@permission_required("drivers.change_car", raise_exception=True)
def edit_car(request, pk):
    ...


# Class-based view
class CarUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
    model = Car
    fields = ["name"]
    permission_required = "drivers.change_car"
    raise_exception = True

In templates, the auth context processor exposes a perms object, so you can show or hide UI based on permissions. Remember: hiding a button is not access control — always enforce the same rule in the view.

{% if perms.drivers.change_car %}
  <a href="{% url 'edit_car' car.pk %}">Edit</a>
{% endif %}

{% if perms.drivers.publish_car %}
  <button type="submit" name="publish">Publish</button>
{% endif %}

Object-level (row-level) permissions with django-guardian

Django's core auth backend is model-level only: has_perm("drivers.change_car") either lets a user change every car or none. It even accepts an obj argument but ignores it. When you need "a user can edit only their own records" or per-object sharing, add django-guardian, which stores permissions per object instance.

Install it, add its backend after the default one, then use assign_perm(), remove_perm() and get_objects_for_user().

# pip install django-guardian
# settings.py
INSTALLED_APPS += ["guardian"]
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",        # model-level
    "guardian.backends.ObjectPermissionBackend",        # object-level
]

# usage
from guardian.shortcuts import assign_perm, remove_perm, get_objects_for_user

car = Car.objects.get(pk=1)
alice = User.objects.get(username="alice")

# Grant a permission on ONE specific row
assign_perm("drivers.change_car", alice, car)

alice.has_perm("drivers.change_car", car)  # True for this car only
alice.has_perm("drivers.change_car")       # still False at the model level

# Every car this user is allowed to change
cars = get_objects_for_user(alice, "drivers.change_car")

remove_perm("drivers.change_car", alice, car)

Permissions in Django REST Framework

DRF reuses Django's permission system through permission_classes, set per view or globally in DEFAULT_PERMISSION_CLASSES. DjangoModelPermissions maps HTTP methods to model permissions (GET -> view, POST -> add, PUT/PATCH -> change, DELETE -> delete). For row-level rules, subclass BasePermission and implement has_object_permission().

from rest_framework import viewsets, permissions

from .models import Car
from .serializers import CarSerializer


class IsOwner(permissions.BasePermission):
    """Object-level rule: only the owner may modify the object."""

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.owner_id == request.user.id


class CarViewSet(viewsets.ModelViewSet):
    queryset = Car.objects.all()
    serializer_class = CarSerializer
    # Reuses Django model permissions, plus the per-object owner check
    permission_classes = [permissions.DjangoModelPermissions, IsOwner]

Choosing the right approach

Approach Granularity Best for Provided by
Model-level permissions Per model and action CRUD gating, the Django admin Django core
Groups / roles A bundle of permissions Teams and role-based access (RBAC) Django core (Group)
Object-level permissions Per row / instance "Owner can edit their own", sharing django-guardian
Custom BasePermission Per request / object in an API Business rules in DRF endpoints Django REST Framework

Most projects combine them: model permissions and groups for the broad strokes, then guardian or a DRF has_object_permission() check where individual records need protecting.

Frequently Asked Questions

Permissions vs groups — when should I use which?

Use a permission for the smallest unit of access: one action on one model, such as drivers.change_car. Use a group to bundle permissions into a named role (Editor, Manager, Support) and assign people to the role instead of granting permissions one by one. Groups scale, because changing the role updates every member at once. Reserve direct user_permissions for rare, person-specific exceptions.

Why doesn't has_perm() see a permission I just added?

Django caches a user's permissions on the user instance for the duration of the request. If you call has_perm(), then add a permission, then call has_perm() again on the same object, you get the stale cached result. Re-fetch the user from the database with User.objects.get(pk=user.pk) to rebuild the cache, or perform the grant before the first check.

How do I add custom permissions in Django?

Declare them in the model's Meta.permissions as (codename, label) pairs, for example ("publish_car", "Can publish car listing"). Then run makemigrations and migrate so Django creates the rows. Use Meta.default_permissions to control or disable the automatic add/change/delete/view set. To add permissions to an already-migrated model without changing fields, write a data migration with RunPython.

How do I implement per-object (row-level) permissions?

Django core is model-level only — has_perm() ignores the obj argument. For "owner can edit their own record" rules, install django-guardian, add its backend, and call assign_perm("app.change_model", user, instance). Query the allowed rows with get_objects_for_user(). In Django REST Framework, implement has_object_permission() on a custom BasePermission instead.

How do I check permissions in a Django template?

The auth context processor exposes a perms object. Write {% if perms.drivers.change_car %} ... {% endif %} to show or hide interface elements. Always enforce the same rule in the view as well, because hiding a button is not access control.

How do permissions work in Django REST Framework?

Set permission_classes on the view or globally. DjangoModelPermissions reuses Django's model permissions and maps HTTP methods to them (GET to view, POST to add, PUT/PATCH to change, DELETE to delete). For row-level rules, subclass BasePermission and implement has_object_permission(self, request, view, obj).

Share this article