Implementing and Understanding Permission Control in Django REST Framework

Permission Usage Scenarios

User tiers often determine access levels in an application. This section explores how to restrict endpoints based on user types such as regular, VIP, and SVIP.

A typical database schema might look like this:

from django.db import models

class UserInfo(models.Model):
    user_type_choices = (
        (1, 'Regular'),
        (2, 'VIP'),
        (3, 'SVIP'),
    )
    user_type = models.IntegerField(choices=user_type_choices)
    username = models.CharField(max_length=32, unique=True)
    password = models.CharField(max_length=64)

class UserToken(models.Model):
    user = models.OneToOneField(to='UserInfo', on_delete=models.CASCADE)
    token = models.CharField(max_length=64)

Initially, enforcement can happen directly inside the view. The following example permits only SVIP users to see order data:

class OrderView(APIView):
    def get(self, request, *args, **kwargs):
        # Direct type check inside the handler
        if request.user.user_type != 3:
            return HttpResponse("Access denied")

        payload = {'code': 1000, 'msg': None, 'data': None}
        try:
            payload['data'] = ORDER_DICT
        except Exception:
            pass
        return JsonResponse(payload)

URL routing:

url(r'^api/v1/order/$', views.OrderView.as_view()),

A authentication endpoint issues tokens after verifying credentials:

class AuthView(APIView):
    authentication_classes = []

    def post(self, request, *args, **kwargs):
        result = {'code': 1000, 'msg': None}
        try:
            username = request._request.POST.get('username')
            password = request._request.POST.get('password')
            user = models.UserInfo.objects.filter(
                username=username, password=password
            ).first()
            if not user:
                result['code'] = 1001
                result['msg'] = "Invalid credentials"
                return JsonResponse(result)
            token = md5(username)
            models.UserToken.objects.update_or_create(
                user=user, defaults={'token': token}
            )
            result['token'] = token
        except Exception:
            result['code'] = 1002
            result['msg'] = 'Request error'
        return JsonResponse(result)

Its URL entry:

url(r'^api/v1/auth/$', views.AuthView.as_view()),

Global authentication configuration:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ['api.utils.auth.Authentication'],
}

The custom authentication class extracts and validates a token:

class Authentication(BaseAuthentication):
    def authenticate(self, request):
        token = request._request.GET.get('token')
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed('Authentication failed')
        return (token_obj.user, token_obj)

    def authenticate_header(self, request):
        return 'Basic realm="api"'

While functional, embedding permission logic inside views becomes repetitive. A cleaner solution moves access rules into dedicated permission classes.

Define custom permissions that inherit from BasePermission:

from rest_framework.permissions import BasePermission

class SVIPPermission(BasePermission):
    message = "SVIP membership required"

    def has_permission(self, request, view):
        return request.user.user_type == 3

class NonSVIPPermission(BasePermission):
    def has_permission(self, request, view):
        return request.user.user_type != 3

Apply them declaratively on views:

from api.utils.permission import SVIPPermission, NonSVIPPermission

class OrderView(APIView):
    permission_classes = [SVIPPermission]

    def get(self, request, *args, **kwargs):
        payload = {'code': 1000, 'msg': None, 'data': None}
        try:
            payload['data'] = ORDER_DICT
        except Exception:
            pass
        return JsonResponse(payload)

class UserInfoView(APIView):
    permission_classes = [NonSVIPPermission]

    def get(self, request, *args, **kwargs):
        return HttpResponse('User information')

URL mapping:

url(r'^api/v1/info/$', views.UserInfoView.as_view()),

How Permission Checks Execute Internally

The dispatch method initiates the request life cycle. It prepares the request and calls initial:

def dispatch(self, request, *args, **kwargs):
    self.args = args
    self.kwargs = kwargs
    request = self.initialize_request(request, *args, **kwargs)
    self.request = request
    self.headers = self.default_response_headers

    try:
        self.initial(request, *args, **kwargs)
        handler = getattr(
            self, request.method.lower(), self.http_method_not_allowed
        ) if request.method.lower() in self.http_method_names else self.http_method_not_allowed
        response = handler(request, *args, **kwargs)
    except Exception as exc:
        response = self.handle_exception(exc)

    self.response = self.finalize_response(request, response, *args, **kwargs)
    return self.response

Inside initial, permission validation occurs after authentication:

def initial(self, request, *args, **kwargs):
    self.format_kwarg = self.get_format_suffix(**kwargs)
    neg = self.perform_content_negotiation(request)
    request.accepted_renderer, request.accepted_media_type = neg
    version, scheme = self.determine_version(request, *args, **kwargs)
    request.version, request.versioning_scheme = version, scheme
    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)

The check_permissions method iterates through permission instances and raises a exception if any has_permission returns False:

def check_permissions(self, request):
    for permission in self.get_permissions():
        if not permission.has_permission(request, self):
            self.permission_denied(
                request, message=getattr(permission, 'message', None)
            )

The message attribute on the permission class provides a custom denial reason.

get_permissions instantiates each class listed in permission_classes:

def get_permissions(self):
    return [permission() for permission in self.permission_classes]

This design explains why assigning a list of permission classes to a view activates them.

Global Permission Configuration

Tracing permission_classes back to its origin reveals API settings capability:

api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)

To enforce a default permission policy across all views, add DEFAULT_PERMISSION_CLASSES to the REST framework settings:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        'api.utils.auth.FirstAuthentication',
        'api.utils.auth.Authentication',
    ],
    "UNAUTHENTICATED_USER": lambda: "Anonymous",
    "UNAUTHENTICATED_TOKEN": None,
    "DEFAULT_PERMISSION_CLASSES": ['api.utils.permission.SVIPPermission'],
}

With a global SVIP requirement, views that should relax or invert the rule can override permission_classes locally:

class OrderView(APIView):
    def get(self, request, *args, **kwargs):
        payload = {'code': 1000, 'msg': None, 'data': None}
        try:
            payload['data'] = ORDER_DICT
        except Exception:
            pass
        return JsonResponse(payload)

class UserInfoView(APIView):
    permission_classes = [NonSVIPPermission]

    def get(self, request, *args, **kwargs):
        return HttpResponse('User information')

Built-in BasePermission

Django REST Framework provides a simple BasePermission class that all custom permissions should extend:

class BasePermission(object):
    def has_permission(self, request, view):
        return True

    def has_object_permission(self, request, view, obj):
        return True

Key takeaways:

  • Custom permissions must inherit from BasePermission and override has_permission.
  • Return True to allow access, False to deny.
  • Use permission_classes on a view for local control.
  • Set DEFAULT_PERMISSION_CLASSES in REST_FRAMEWORK settings for global control.

Tags: Django REST Framework permissions Authentication API Security

Posted on Mon, 02 Nov 2026 16:14:38 +0000 by grazzman