【问题标题】:How to write a custom permission class to check object-level permissions before the api-level ones?如何编写自定义权限类以在 api 级权限之前检查对象级权限?
【发布时间】:2020-05-19 02:07:52
【问题描述】:

TL;DR

如何编写自定义权限类以在 api 级权限 (has_permission) 之前检查对象级权限 (has_object_permission)?

定义

假设我们正在构建一个在线注释工具。我们有属于项目和用户的图像,它们正在对图像进行注释。用户有一个role 字段——来宾具有只读访问权限,开发人员可以更改图像字段,所有者可以更改项目和图像。

模型:

class Image(models.Model):
    data = JSONField()
    project = models.ForeignKey(Project)

class Project(models.Model):
    pass

class User(AbstractUser):
    ROLE_GUEST, ROLE_DEVELOPER, ROLE_OWNER = range(3)
    ROLE_CHOICES = [(ROLE_GUEST, "Guest"), (ROLE_DEVELOPER, "Developer"), (ROLE_OWNER, "Owner")]
    role = models.IntegerField(default=ROLE_GUEST, choices=ROLE_CHOICES)

假设我们希望限制对某些项目的访问。我们正在添加 m2m 模型 UserProjectRole:

class UserProjectRole(models.Model):
    user = models.ForeignKey(User)
    project = models.ForeignKey(Project)
    project_role = ... # same as in User

现在我们希望设置权限,如果用户请求访问有关某些项目的 API 端点:

  • 如果此用户和此项目没有 UserProjectRole,则权限默认为他的全局role
  • 如果有这样的UserProjectRole,权限是使用他的project_role而不是role

问题

我们正在实现自定义权限类:

# permissions.py
from rest_framework import permissions

class AtLeastDeveloper(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user.is_staff or request.user.role >= User.ROLE_DEVELOPER

class AtLeastOwner(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user.is_staff or request.user.role >= User.ROLE_OWNER

class InProjectPermissions(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
         if isinstance(obj, Project):
             project = obj
         elif isinstance(obj, Image):
             project = obj.project
         else:
             raise ValueError(obj)
         upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
         if upr is None:
             return False
         if isinstance(obj, Project):
             return upr.role == User.ROLE_OWNER
         return upr.role >= User.ROLE_DEVELOPER
# views.py

class ProjectGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
     permission_classes = [IsAuthenticated, AtLeastOwner, InProjectPermissions]
     ...

class ImageGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
     permission_classes = [IsAuthenticated, AtLeastDeveloper, InProjectPermissions]
     ...

这里的问题是has_permission 被称为之前 has_object_permissions(如https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions 中所述)。因此,如果用户的全局role不够用,就会被拒绝访问(IsAtLeastSomething.has_permissions会返回False,因此InProjectPermissions.has_object_permissions不会被执行)。

请告知如何编写自定义权限类以在 api 级权限之前检查对象级权限。

【问题讨论】:

  • 可以从您的 has_permission 方法中手动调用 has_object_permission 吗?
  • @Hafnernuss 否,因为 has_permission 无权访问 obj
  • 是的,但您可以自己检索对象,因为您可以访问 has_permission 中的视图/请求。

标签: django django-rest-framework permissions


【解决方案1】:

TLDR; - 尝试覆盖 check_object_permissions(request, obj)

根据 DRF 文档:

在运行主体之前查看列表中的每个权限 被检查。如果任何权限检查失败 exceptions.PermissionDenied 或 exceptions.NotAuthenticated 异常 将被提升,并且视图的主体不会运行。

这意味着我们在permission_classes中提到的所有权限必须通过。如果其中任何一个也失败了,那么它就不会进行身份验证。

  1. 因此,对于我们的自定义需求,我们可以覆盖 check_object_permissions(request, obj) 并编写我们的自定义身份验证。

【讨论】:

    【解决方案2】:

    所以,这就是我想出的。

    可以按照@Hafnernuss 的建议,通过view.get_object(id) 访问相关对象(从request.GET/POST 获取id),但这正是我希望避免:手动处理结构内部。

    @umair-mohammad 建议覆盖view.check_object_permissions,但我再次希望避免这样做,因为这意味着我的所有视图都应该从一个基类继承。在大多数情况下,这没问题,感谢您的回答 - 这确实解决了问题 - 但我真的想将此逻辑分离到 permissions.py 中而不触及视图。

    所以,让我们改写基础权限类:

    class AbstractPermission(permissions.BasePermission):
        def has_permission(self, request, view):
            return True
    
        def has_permission_by_global_role(self, request, view):
            raise NotImplementedError()
    
        def has_object_permission(self, request, view, obj):
            if isinstance(obj, Project):
                 project = obj
             elif isinstance(obj, Image):
                 project = obj.project
             else:
                 raise ValueError(obj)
    
             # if user has global role big enough -- permit
             if self.has_permission_by_global_role(request, view):
                  return True
    
             # otherwise -- assess his 'local' role (as before)
             upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
             if upr is None:
                 return False
             if isinstance(obj, Project):
                 return upr.role == User.ROLE_OWNER
             return upr.role >= User.ROLE_DEVELOPER
    

    然后我们继承我们的 AtLeastSomething 类,将 has_permission 更改为 has_permission_by_global_role

    class AtLeastOwner(AbstractPermission):
        def has_permission_by_global_role(self, request, view):
            return request.user.is_staff or request.user.role >= User.ROLE_OWNER
    
    ...
    

    因此,在views.py 中,我们只需要根据允许的全局角色指定permission_classes ——在我们的例子中,即为项目的AtLeastOwner 和用于图像的AtLeastDeveloper。

    我相信这是最 Pythonic 的方法。通过为每个模型提供类似.allowed_roles() 的方法,可以进一步使这个更干净。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-24
      • 2017-07-12
      • 2019-10-09
      • 1970-01-01
      • 2021-10-20
      • 2014-07-19
      • 1970-01-01
      相关资源
      最近更新 更多