Странное поведение с разрешениями моделей/объектов и наборами представлений
У меня наблюдается странное поведение, когда djangorestframework возвращает 404 при попытке просмотра просматриваемого API, но прикрепление ?format=json в конце возвращает нормальный ответ.
Использование:
Django==4.0.3
django-guardian==2.4.0
djangorestframework==3.13.1
djangorestframework-guardian==0.3.0
Упрощенная версия установки моего проекта:
#### 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")),
...
]
Проблема:
Я пытаюсь добавить разрешения таким образом, чтобы:
- Пользователь может получить, обновить или удалить только свой собственный экземпляр модели User (используя разрешения для каждого объекта, которые назначаются его экземпляру модели при создании)
- Пользователь с правами на получение, обновление или удаление всей модели (например, назначенными с помощью панели администратора), который может быть или не быть суперпользователем django (администратором), может управлять всеми моделями пользователей.
Для достижения этой цели моя логика следующая:
- 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_permissionmethod:
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
Тайна:
Тестирование с пользователем, у которого есть:
ПК == 3
пер-объектные разрешения RUD для экземпляра модели User с PK == 3 (собственная модель)
Широкомодельные разрешения для просмотра пользователей
Переход к
api/v1/appprefix/user/3: Возвращает HTTP 200, как и ожидалосьПереход к
api/v1/appprefix/user/2: Возвращает HTTP 404 (пользователь с pk 2 существует)Переход к
api/v1/appprefix/user/2?format=json: Возвращает HTTP 200, как и ожидалось
Что я пробовал:
Изменение:
...
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):
...
Странно, но это исправляет ситуацию, и api/v1/appprefix/user/2 начинает возвращать HTTP 200
Все еще не решена странная ошибка HTTP404, которая возникает только при использовании просматриваемого API, но я нашел решение проблемы, которую пытался решить изначально - разрешить пользователям с правами модели доступ ко всем объектам, ограничивая остальных только объектами, на которые у них есть права.
Я изменил класс разрешений на следующий:
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.exceptions import PermissionDenied, NotFound
from django.http import Http404
class GlobalOrObjectPermission(DjangoObjectPermissions):
perms_map = {...}
def has_permission(self, request, view):
# Always let the request to proceed. The endpoint only serves
# individual objects so this is OK
return True
def has_object_permission(self, request, view, obj):
try:
has_perm = super().has_object_permission(request, view, obj)
except (
PermissionDenied,
# has_object_permission() raises http.Http404 instead of
# drf_exceptions.NotFound when user does not have read permissions
# but this could change so check for both exceptions
Http404,
NotFound,
) as e:
has_perm = super().has_permission(request, view)
# If user does not have model permissions, raise the original
# object permission error, which can be HTTP403 or HTTP404
if not has_perm:
raise e
return has_perm
Это позволяет:
- Пользователи с правами на объекты для доступа к отдельным объектам, на которые у них есть права
- Пользователи с правами модели для доступа ко всем объектам модели