Beyond Individual Records: A Comprehensive Guide to Django Global and Model-Level Permissions

Throughout our exploration of Django and Django REST Framework (DRF)—spanning authentication classes, permission classes, viewset mixins, view-level restrictions, Check This Out and testing utilities—we have examined various ways to secure applications. However, before you can restrict a user from editing a specific record (object-level security), your application must answer a broader question: Does this user have permission to interact with this type of resource at all?

In Django, this broad, system-wide or table-wide authorization layer is handled by global permissions, more formally known as model-level permissions. Global permissions govern whether a user can create, view, modify, or delete any instance of a specific model, serving as the foundational first line of defense in your application architecture.

This comprehensive guide explores how Django’s built-in global permission system operates, how to define custom permissions in model metadata, how to manage them via users and groups, and how to enforce them across views and APIs.

1. The Core Architecture of Django Model Permissions

When you include 'django.contrib.auth' in your INSTALLED_APPS setting, Django automatically provisions a robust permission and content-type infrastructure. For every model defined across your project’s apps, Django automatically generates four default global permissions upon migration:

  1. add_<model>: Permits creation of new model instances.
  2. change_<model>: Permits editing and updating existing model instances.
  3. delete_<model>: Permits deleting model instances.
  4. view_<model>: Permits viewing or reading model instances.

These permissions follow a strict string convention:

$$\text{<app\_label>.<action>\_<model\_name>}$$

For example, if you have an application named blog with a model named Article, Django automatically registers the following global permission codenames:

  • blog.add_article
  • blog.change_article
  • blog.delete_article
  • blog.view_article

These permissions are stored centrally in the database using Django’s built-in Permission model, which links directly to the ContentType framework to track which model owns which permission.

2. Defining Custom Global Permissions

While Django’s default CRUD permissions cover standard operations, real-world business logic often requires specialized global actions that do not fit neatly into add, change, delete, or view—such as approving a financial transaction, publishing a blog post, or locking a user account.

You can define custom global permissions directly inside a model’s Meta class using the permissions attribute:

Python

from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    is_published = models.BooleanField(default=False)

    class Meta:
        permissions = [
            ("can_publish", "Can publish articles"),
            ("can_archive", "Can archive articles"),
        ]

When you run python manage.py makemigrations and python manage.py migrate, Django registers blog.can_publish and blog.can_archive into the database, making them available for assignment to users and groups just like the default CRUD permissions.

3. Assigning Global Permissions to Users and Groups

Global permissions can be granted directly to individual users or categorized and assigned collectively via Groups.

Programmatic Assignment

You can manage permissions programmatically using Django’s built-in relationship managers (user_permissions and permissions):

Python

from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType
from blog.models import Article

# 1. Fetch a user
user = User.objects.get(username='alice')

# 2. Assign a default model permission directly to a user
content_type = ContentType.objects.get_for_model(Article)
add_permission = Permission.objects.get(
    codename='add_article',
    content_type=content_type,
)
user.user_permissions.add(add_permission)

# 3. Use Groups for scalable Role-Based Access Control (RBAC)
editors_group, created = Group.objects.get_or_create(name='Editors')

# Retrieve the custom global permission we defined in Meta
publish_permission = Permission.objects.get(
    codename='can_publish',
    content_type=content_type,
)
editors_group.permissions.add(publish_permission)

# Add user to the group (inherits all group permissions automatically)
user.groups.add(editors_group)

Superusers (is_superuser=True) automatically bypass all permission checks, possessing global authorization for every action across the entire project.

4. Checking Global Permissions in Code

Once permissions are assigned, you can verify whether a user possesses a specific global permission using the .has_perm() method available on user instances:

Python

# Check if a user can publish articles
if request.user.has_perm('blog.can_publish'):
    # Allow publishing workflow
    pass
else:
    # Deny action
    pass

Checking Permissions in Templates

Django’s built-in auth context processor automatically exposes a perms variable to all templates rendered with a RequestContext. You can check global permissions cleanly in HTML templates:

HTML

{% if perms.blog.can_publish %}
    <button class="btn btn-primary">Publish Article</button>
{% endif %}

5. Enforcing Global Permissions in Views

Securing endpoints against unauthorized access using global permissions is straightforward across both function-based and class-based views.

Function-Based Views (@permission_required)

Use Django’s built-in @permission_required decorator to restrict function-based views:

Python

from django.contrib.auth.decorators import permission_required
from django.shortcuts import render

@permission_required('blog.can_publish', raise_exception=True)
def publish_dashboard_view(request):
    return render(request, 'blog/publish_dashboard.html')

Class-Based Views (PermissionRequiredMixin)

For class-based views, inherit from PermissionRequiredMixin and define the permission_required attribute:

Python

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import CreateView
from .models import Article
from .forms import ArticleForm

class ArticleCreateView(PermissionRequiredMixin, CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/article_form.html'
    
    # Specify the required global permission string
    permission_required = 'blog.add_article'
    raise_exception = True

6. Global Permissions in Django REST Framework (DjangoModelPermissions)

When building APIs with Django REST Framework, writing manual permission checks for standard CRUD actions can become repetitive. DRF provides DjangoModelPermissions, a built-in permission class that automatically maps global Django model permissions directly to standard HTTP verbs:

  • GET / HEAD / OPTIONS: Requires view permission.
  • POST: Requires add permission.
  • PUT / PATCH: Requires change permission.
  • DELETE: Requires delete permission.

Python

from rest_framework import viewsets
from rest_framework.permissions import DjangoModelPermissions
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    # Enforces Django model-level global permissions automatically
    permission_classes = [DjangoModelPermissions]

When an unauthenticated or unauthorized client sends a request to this ViewSet, DRF inspects the user’s global model permissions and returns a clean 401 Unauthorized or 403 Forbidden response accordingly.

7. Best Practices for Global Permissions

  1. Leverage Groups for RBAC: Never assign raw permissions directly to hundreds of individual users. Create logical roles using Django Group models (e.g., Editors, Authors, Billing Managers) and assign global permissions to those groups.
  2. Keep Global Permissions High-Level: Use global permissions to gate entire feature sets or management panels. For data isolation (e.g., ensuring a user can only edit their own posts), complement global model permissions with object-level permission checks.
  3. Use Consistent App Labels: Ensure your custom permission names cleanly follow Django’s <app_label>.<codename> convention to avoid lookup errors in decorators and mixins.
  4. Test Permission Boundaries: Write automated unit tests verifying that users lacking specific global model permissions are correctly blocked from accessing protected views and API endpoints.

Conclusion

Django global and model-level permissions form the bedrock of robust application authorization. By understanding how Django automatically generates CRUD permissions, defining custom actions in model metadata, managing roles via groups, and enforcing checks using decorators, mixins, and DRF integration classes, you establish a secure, go to my blog scalable framework for managing access across your entire application.