模型/对象权限和视图集的怪异行为(Django REST框架)



我遇到了一些奇怪的行为,djangorestframework在尝试浏览可浏览的API时返回404,但在结尾附加?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")),
...
]

问题是:

我正试图以这样的方式添加权限:

  • 用户只能检索、更新或删除自己的用户模型实例(使用创建时分配给其模型实例的每个对象权限(
  • 具有全模型检索、更新或删除权限(例如,使用管理面板分配(的用户,可能是也可能不是django超级用户(管理员(,可以RUD所有用户模型

为了实现这一点,我的逻辑如下:

  1. 有一个权限类,它只检查用户是否具有每个对象的权限:
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. 有一个类,它检查模型范围的权限,但在has_object_permission方法中执行此操作:
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

神秘之处:

使用具有以下的用户进行测试

  • PK==3

  • PK==3(其自己的模型(的用户模型实例的每个对象RUD权限

  • 查看用户的模型范围权限

  • 导航到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):
...

收件人:

...
perms = ['myapp_label.view_user']
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...

奇怪的是,这修复了它,api/v1/appprefix/user/2开始返回HTTP200

仍然没有解决只有在使用可浏览的API时才会出现的奇怪的HTTP404错误,但我找到了一个解决方案来解决我最初试图解决的问题-允许具有模型权限的用户访问所有对象,而将其余对象仅限于他们有权限的对象。

我已将权限类更改为以下内容:

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:
if isinstance(e, Http404): # added these 2 lines
e = NotFound()         # see EDIT note
raise e
return has_perm

这允许:

  • 具有对象权限的用户可以访问他们有权限访问的单个对象
  • 具有访问模型所有对象的模型权限的用户

编辑:

经过更多的测试,我发现它实际上遇到了与原始帖子相同的问题。我用调试器运行了它,它似乎是一个bug。我没有时间进一步研究这个问题,但由于某种原因,当Http404从重写方法中引发时,它会向下传播到Django的普通404页面呈现器。而如果它是从超级方法引发的,它会传播到BrowsebleAPI 404渲染器,在那里它会被转换为NotFound异常并进行渲染。因此,在重写的方法中将Http404异常转换为DRF的原生NotFound异常(请参阅代码(修复了这个问题,因为在这两种情况下,NotFound都由可浏览的API呈现器处理。

最新更新