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
BasePermissionand overridehas_permission. - Return
Trueto allow access,Falseto deny. - Use
permission_classeson a view for local control. - Set
DEFAULT_PERMISSION_CLASSESinREST_FRAMEWORKsettings for global control.