Skip to main content

Django: Custom Permissions

Django: Custom Permissions

Custom permissions in Django allow developers to define specific access controls for models, enabling fine-grained authorization beyond the default add, change, and delete permissions. Integrated with Django’s Model-View-Template (MVT) architecture and the django.contrib.auth package, custom permissions enhance security and flexibility in managing user roles. This tutorial explores Django custom permissions, covering their creation, implementation, and practical applications for secure access control in web applications.


01. What Are Custom Permissions?

Custom permissions are user-defined permissions tied to specific models, allowing control over unique actions (e.g., "publish article" or "archive post"). Built into Django’s authentication system, they extend the default permissions (add_, change_, delete_) and are ideal for applications like content management systems, e-commerce platforms, or dashboards requiring role-specific access.

Example: Basic Custom Permission

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

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    class Meta:
        permissions = [
            ('can_publish', 'Can publish articles'),
        ]
# Apply migrations
python manage.py makemigrations
python manage.py migrate

Output:

Permission 'can_publish' created for Article model.

Explanation:

  • permissions - Defines custom permissions in the model’s Meta class.
  • Migrations create the permission in the database.

02. Key Custom Permissions Concepts

Custom permissions integrate with Django’s authentication and authorization framework. The table below summarizes key concepts and their roles:

Component Description Use Case
Custom Permissions User-defined permissions for specific actions Control unique model operations
Permission Checks Verify user permissions in views or Admin Restrict access to authorized users
Groups Assign custom permissions to user roles Simplify role-based access
Admin Integration Manage permissions via Admin interface Assign permissions to users/groups


2.1 Defining Custom Permissions

Example: Multiple Custom Permissions

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

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)

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

    def __str__(self):
        return self.title

Output:

Permissions 'can_publish', 'can_archive', 'can_feature' added after migrations.

Explanation:

  • Multiple permissions defined for different actions.
  • Each permission has a codename and description.

2.2 Checking Permissions in Views

Example: Permission-Protected View

# myapp/views.py
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render

@permission_required('myapp.can_publish', raise_exception=True)
def publish_article(request):
    return render(request, 'myapp/publish.html', {'message': 'Article published'})

# myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('publish/', views.publish_article, name='publish_article'),
]

# myapp/templates/myapp/publish.html
{% extends "base.html" %}
{% block content %}
    <h2>Publish Article</h2>
    <p>{{ message }}</p>
{% endblock %}

Output:

Only users with 'can_publish' access http://127.0.0.1:8000/publish/; others get 403.

Explanation:

  • permission_required - Restricts view access to authorized users.
  • raise_exception=True - Returns a 403 Forbidden error for unauthorized access.

2.3 Assigning Custom Permissions

Example: Assigning Permissions to Users and Groups

# myapp/management/commands/setup_permissions.py
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group, Permission, User

class Command(BaseCommand):
    help = 'Sets up permissions and groups'

    def handle(self, *args, **kwargs):
        # Create Editors group with permissions
        editor_group, _ = Group.objects.get_or_create(name='Editors')
        permissions = Permission.objects.filter(codename__in=['can_publish', 'can_feature'])
        editor_group.permissions.set(permissions)

        # Assign user to group and add direct permission
        user = User.objects.get(username='testuser')
        user.groups.add(editor_group)
        user.user_permissions.add(Permission.objects.get(codename='can_archive'))

        self.stdout.write(self.style.SUCCESS('Permissions setup complete'))
# Run the command
python manage.py setup_permissions

Output:

Editors group assigned 'can_publish', 'can_feature'; testuser also gets 'can_archive'.

Explanation:

  • permissions.set - Assigns permissions to a group.
  • user_permissions.add - Grants a specific permission directly to a user.

2.4 Custom Permissions in Admin

Example: Admin Permission Logic

# myapp/admin.py
from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'author']

    def has_change_permission(self, request, obj=None):
        if not obj:
            return True
        return request.user == obj.author or request.user.has_perm('myapp.can_publish')

    def has_delete_permission(self, request, obj=None):
        return request.user.has_perm('myapp.can_archive')

Output:

Only authors or users with 'can_publish' can edit; only 'can_archive' users can delete.

Explanation:

  • has_change_permission - Restricts editing to authors or users with can_publish.
  • has_delete_permission - Limits deletion to users with can_archive.

2.5 Manual Permission Checks

Example: Conditional Logic with Permissions

# myapp/views.py
from django.shortcuts import render
from .models import Article

def article_dashboard(request):
    articles = Article.objects.all()
    can_publish = request.user.has_perm('myapp.can_publish')
    can_archive = request.user.has_perm('myapp.can_archive')
    return render(request, 'myapp/dashboard.html', {
        'articles': articles,
        'can_publish': can_publish,
        'can_archive': can_archive,
    })

# myapp/templates/myapp/dashboard.html
{% extends "base.html" %}
{% block content %}
    <h2>Article Dashboard</h2>
    {% for article in articles %}
        <p>{{ article.title }}
            {% if can_publish %}
                <a href="{% url 'publish_article' %}">Publish</a>
            {% endif %}
            {% if can_archive %}
                <a href="{% url 'archive_article' %}">Archive</a>
            {% endif %}
        </p>
    {% endfor %}
{% endblock %}

Output:

Dashboard shows 'Publish' or 'Archive' links based on user permissions.

Explanation:

  • has_perm - Checks permissions in view logic.
  • Template conditionally displays actions based on permissions.

2.6 Incorrect Permissions Setup

Example: Undefined Permission Check

# myapp/views.py (Incorrect)
from django.contrib.auth.decorators import permission_required

@permission_required('myapp.can_delete')  # Incorrect: Permission not defined
def delete_article(request):
    return render(request, 'myapp/delete.html')

Output:

PermissionDoesNotExist error raised.

Explanation:

  • Referencing undefined permissions causes errors.
  • Solution: Define permissions in Meta and apply migrations.

03. Effective Usage

3.1 Recommended Practices

  • Define clear, action-specific custom permissions and integrate with groups for role-based access.

Example: Comprehensive Custom Permissions

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

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)

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

# myapp/management/commands/setup_permissions.py
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group, Permission

class Command(BaseCommand):
    help = 'Sets up permissions and groups'

    def handle(self, *args, **kwargs):
        editor_group, _ = Group.objects.get_or_create(name='Editors')
        permissions = Permission.objects.filter(codename__in=['can_publish', 'can_feature'])
        editor_group.permissions.set(permissions)
        self.stdout.write(self.style.SUCCESS('Permissions setup complete'))

# myapp/views.py
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render

@permission_required('myapp.can_publish')
def publish_article(request):
    return render(request, 'myapp/publish.html', {'message': 'Article published'})

@permission_required('myapp.can_archive')
def archive_article(request):
    return render(request, 'myapp/archive.html', {'message': 'Article archived'})

# myapp/admin.py
from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'author']
    def has_change_permission(self, request, obj=None):
        if not obj:
            return True
        return request.user == obj.author or request.user.has_perm('myapp.can_publish')

# myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('publish/', views.publish_article, name='publish_article'),
    path('archive/', views.archive_article, name='archive_article'),
]

Output:

Editors group with 'can_publish', 'can_feature'; restricted views and Admin access.
  • Custom permissions control specific actions.
  • Group setup simplifies role management.
  • Admin and views enforce permission-based access.

3.2 Practices to Avoid

  • Avoid using undefined or overly broad permissions.

Example: Overly Broad Permission

# myapp/views.py (Incorrect)
from django.contrib.auth.models import User, Permission

def grant_all_permissions(request, user_id):
    user = User.objects.get(id=user_id)
    user.user_permissions.add(*Permission.objects.all())  # Incorrect: Grants all permissions
    return redirect('dashboard')

Output:

User gains unrestricted access, compromising security.
  • Granting all permissions risks unauthorized access.
  • Solution: Assign specific custom permissions or use groups.

04. Common Use Cases

4.1 Role-Based Content Publishing

Restrict publishing actions to users with specific permissions.

Example: Publish Permission

# myapp/views.py
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render
from .models import Article

@permission_required('myapp.can_publish')
def publish_dashboard(request):
    articles = Article.objects.filter(author=request.user)
    return render(request, 'myapp/publish_dashboard.html', {'articles': articles})

# myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('publish-dashboard/', views.publish_dashboard, name='publish_dashboard'),
]

# myapp/templates/myapp/publish_dashboard.html
{% extends "base.html" %}
{% block content %}
    <h2>Publish Dashboard</h2>
    {% for article in articles %}
        <p>{{ article.title }} - <a href="{% url 'publish_article' %}">Publish</a></p>
    {% endfor %}
{% endblock %}

Output:

Only users with 'can_publish' access http://127.0.0.1:8000/publish-dashboard/.

Explanation:

  • Custom permission restricts publishing functionality.
  • Dashboard filters articles by user for role-specific access.

4.2 Content Archiving Control

Limit archiving actions to authorized users.

Example: Archive Permission

# myapp/views.py
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render
from .models import Article

@permission_required('myapp.can_archive')
def archive_dashboard(request):
    articles = Article.objects.all()
    return render(request, 'myapp/archive_dashboard.html', {'articles': articles})

# myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('archive-dashboard/', views.archive_dashboard, name='archive_dashboard'),
]

# myapp/templates/myapp/archive_dashboard.html
{% extends "base.html" %}
{% block content %}
    <h2>Archive Dashboard</h2>
    {% for article in articles %}
        <p>{{ article.title }} - <a href="{% url 'archive_article' %}">Archive</a></p>
    {% endfor %}
{% endblock %}

Output:

Only users with 'can_archive' access http://127.0.0.1:8000/archive-dashboard/.

Explanation:

  • Custom permission controls archiving actions.
  • Dashboard displays all articles for authorized users.

Conclusion

Django’s custom permissions, integrated with the Model-View-Template architecture, provide a powerful mechanism for fine-grained access control. Key takeaways:

  • Define custom permissions in model Meta for specific actions.
  • Use permission_required and has_perm for access control.
  • Assign permissions via groups or directly to users for role-based management.
  • Avoid undefined or overly broad permissions to ensure security.

With Django’s custom permissions, you can build secure, role-specific access controls for web applications efficiently!

Comments