I am experiencing some weird behavior where djangorestframework
returns a 404 when trying to browse the browsable API, but attaching a ?format=json
at the end returns a normal response.
Using:
Django==4.0.3
django-guardian==2.4.0
djangorestframework==3.13.1
djangorestframework-guardian==0.3.0
A simplified version of my project setup:
#### API views
...
class UserRUDViewSet(
drf_mixins.RetrieveModelMixin,
drf_mixins.UpdateModelMixin,
drf_mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""Viewset combining the RUD views for the User model"""
serializer_class = serializers.UserSerializer
queryset = models.User.objects.all()
permission_classes = [permissions.RudUserModelPermissions | permissions.RudUserObjectPermissions]
...
#### app API urls
...
_api_prefix = lambda x: f"appprefix/{x}"
api_v1_router = routers.DefaultRouter()
...
api_v1_router.register(_api_prefix("user"), views.UserRUDViewSet, basename="user")
#### project urls
from app.api.urls import api_v1_router as app_api_v1_router
...
api_v1_router = routers.DefaultRouter()
api_v1_router.registry.extend(app_api_v1_router.registry)
...
urlpatterns = [
...
path("api/v1/", include((api_v1_router.urls, "project_name"), namespace="v1")),
...
]
The problem:
I am trying to add permissions in such a way that:
- A user can only retrieve, update or delete its own User model instance (using per-object permissions which are assigned to his model instance on creation)
- A user with model-wide retrieve, update or delete permissions (for example assigned using the admin panel), who may or may not also be a django superuser (admin) can RUD all user models.
To achieve this my logic is as follows:
- Have a permissions class which only checks if a user has per-object permission:
class RudUserObjectPermissions(drf_permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def has_permission(self, request, view):
return True
- Have a class which checks for model-wide permissions but does this in the
has_object_permission
method:
class RudUserModelPermissions(drf_permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
...
# Same as the other permissions class
}
# has_permission() == true if we are to get anywhere - no need to override
# Originally tried like this
# def has_object_permission(self, request, view, obj):
# return super().has_permission(request, view)
# Copied from the drf_permissions. DjangoObjectPermissions class
def has_object_permission(self, request, view, obj):
# Changed the commented out lines only
queryset = self._queryset(view)
model_cls = queryset.model
user = request.user
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
if request.method in drf_permissions.SAFE_METHODS:
raise drf_permissions.Http404
read_perms = self.get_required_object_permissions('GET', model_cls)
# if not user.has_perms(read_perms, obj):
if not user.has_perms(read_perms):
raise drf_permissions.Http404
return False
return True
The mystery:
Testing with a user who has:
PK == 3
per-object RUD permissions for User model instance with PK == 3 (its own model)
Model wide permissions for viewing users
Navigating to
api/v1/appprefix/user/3
: Returns HTTP 200, as expectedNavigating to
api/v1/appprefix/user/2
: Returns HTTP 404 (user with pk 2 exists)Navigating to
api/v1/appprefix/user/2?format=json
: Returns HTTP 200, as expected
What I have tried:
Changing:
...
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...
To:
...
perms = ['myapp_label.view_user']
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...
Weirdly this fixes it and api/v1/appprefix/user/2
starts returning HTTP 200