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?
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.