Странное поведение с разрешениями моделей/объектов и наборами представлений

У меня наблюдается странное поведение, когда 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 (администратором), может управлять всеми моделями пользователей.

Для достижения этой цели моя логика следующая:

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

Тайна:

Тестирование с пользователем, у которого есть:

  • ПК == 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

Это позволяет:

  • Пользователи с правами на объекты для доступа к отдельным объектам, на которые у них есть права
  • Пользователи с правами модели для доступа ко всем объектам модели
Вернуться на верх