djangodjango-rest-frameworkdjango-permissions

Django Model Permissions. Why is this hard?


Django creates permissions for every model you make, such as:

can_view_{model_name} can_add_{model_name} can_edit_{model_name}

Out of the box, these are only applicable to Django Admin. Ok well if I want to apply them at the model level, why can't I do:

class MyModel(models.Model)
    def can_view(self, user):
        if user.has_perm('my_app.can_view_my_model'):
            return True
        return False

And then any time the ORM tries to lookup that model, it has to check the permission first.

Instead, I have to go into each View and manually check:

class MyModelDetail(APIView):

    @transaction.atomic
    def get(self, request):
        try:
            if not request.user.has_perm("my_app.can_view_my_model"):
                raise APIException("You do not have permission to view this model")

And repeat that across all views that look up my model.

Is there an easier way?


Solution

  • The Django REST framework has BasePermission classes [drf-doc] that can be used for this. Such permission could look like:

    from rest_framework import permissions
    
    
    class AdminModelPermission(permissions.BasePermission):
        METHOD_MAPPING = {
            'GET': 'view',
            'POST': 'add',
            'PUT': 'edit',
            'PATCH': 'edit',
            'DELETE': 'delete',
        }
    
        def has_permission(self, request, view):
            meta = view.get_queryset().model._meta
            action = self.METHOD_MAPPING.get(request.method)
            if action is not None:
                return request.user.has_perm(
                    f'{meta.app_label}.{action}_{meta.model_name}'
                )
            return True
    
        def has_object_permission(self, request, view, obj):
            action = self.METHOD_MAPPING.get(request.method)
            if action is not None:
                method = getattr(obj, f'can_{action}', None)
                if method is not None:
                    return method(request.user)
            return True

    and then plug this in in a GenericAPIView or GenericViewSet or ModelViewSet, like:

    from rest_framework import viewsets
    
    
    class MyModelViewSet(viewsets.ModelViewSet):
        queryset = MyModel.objects.all()
        permission_classes = (AdminModelPermission,)

    If we now for example fire a DELETE request, it will first check if the user has a app_label.delete_mymodel permission. If so, it will fetch the object, and if the model itself has a .can_delete(…) method, it will call that with the logged in user to check if it can delete that specific object. If so, then finally it will proceed.

    It is however important to at least fetch the object through the .get_object(…), since that is where the Django REST framework checks the .has_object_permission(…) of all installed permissions, and furthermore let it do the dispatching itself to invoke the .has_permission(…) method.