Django-rest-framework: 记录如何处理相关字段的权限和过滤。

创建于 2014-10-23  ·  26评论  ·  资料来源: encode/django-rest-framework

目前,关系不会自动应用与视图相同的一组权限和过滤。 如果您需要权限或过滤关系,则需要明确处理它。

就我个人而言,我没有看到任何自动处理这个问题的好方法,但显然我们至少可以通过更好地记录来做一些事情。

现在我的观点是我们应该尝试提出一个简单的示例案例并记录您将如何明确处理它。 任何处理这个问题的自动代码都应该留给第三方包作者来处理。 这允许其他贡献者探索问题,看看他们是否可以提出任何可能包含在核心中的好的解决方案。

将来这个问题可能会从“文档”提升到“增强”,但除非有任何具体的建议得到第三方包的支持,否则它会保持这种状态。

Documentation

最有用的评论

我使用这个简单的 Serializer mixin 来过滤相关字段中的查询集:

class FilterRelatedMixin(object):
    def __init__(self, *args, **kwargs):
        super(FilterRelatedMixin, self).__init__(*args, **kwargs)
        for name, field in self.fields.iteritems():
            if isinstance(field, serializers.RelatedField):
                method_name = 'filter_%s' % name
                try:
                    func = getattr(self, method_name)
                except AttributeError:
                    pass
                else:
                    field.queryset = func(field.queryset)

用法也很简单:

class SocialPageSerializer(FilterRelatedMixin, serializers.ModelSerializer):
    account = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = models.SocialPage

    def filter_account(self, queryset):
        request = self.context['request']
        return queryset.filter(user=request.user)

如何? 你看到它有什么问题吗?

所有26条评论

我尝试挖掘代码,但我找不到一种简单的方法来做到这一点。 理想情况下,您应该能够为每个相关对象调用权限的“has_object_permission”。 现在,序列化程序无权访问权限对象。

现在,序列化程序无权访问权限对象。

除了这并不那么简单。

_Which_ 权限对象? 这些是与 _other_ 对象的关系,因此当前视图上的权限和过滤器类不一定与您要应用于对象关系的规则相同。

对于超链接关系,您理论上可以确定它们指向 (+) 的视图并基于此确定过滤/权限,但它最终肯定会成为一个可怕的紧耦合设计。 对于非超链接关系,您甚至无法做到这一点。 不能保证每个模型在单个规范视图上都暴露一次,因此您不能尝试自动确定要用于非超链接关系的权限。

(+) 实际上可能实际上不可能以任何 _sensible_ 方式做到这一点,但让我们暂时假设。

也许有一个“has__permission”?然后每个权限对象将能够判断哪些相关对象是可见的或不可见的。

人们如何使用过滤? 他们是否仅使用它来隐藏用户没有权限的对象? 因为如果这是用例,那么可能不需要过滤器。

引用的问题之一 #1646 涉及限制相关字段的可浏览 API 页面上显示的选择。

我喜欢可浏览的 API,并认为它不仅对我作为后端开发人员而且对 REST API 的前端开发人员/用户来说都是一个很好的工具。 我很乐意在打开可浏览 API 的情况下发布产品(即......即使站点在调试模式下不再孤单,它也会运行)。 为了能够做到这一点,我不能通过可浏览的 API 页面发生信息泄漏。 (当然,除了这些页面通常是生产就绪和安全的要求之外)。

这意味着通过 HTML 页面可以了解的有关相关字段存在的信息不应该比通过 POST 可以了解的更多。

我最终为我的序列化程序创建了一个 mixin 类,它使用相关字段的视图来提供过滤。

class RelatedFieldPermissionsSerializerMixin(object):
    """
    Limit related fields based on the permissions in the related object's view.

    To use, mixin the class, and add a dictionary to the Serializer's Meta class
    named "related_queryset_filters" mapping the field name to the string name 
    of the appropriate view class.  Example:

    class MySerializer(serializers.ModelSerializer):
        class Meta:
            related_queryset_filters = {
                'user': 'UserViewSet',
            }

    """
    def __init__(self, *args, **kwargs):
        super(RelatedFieldPermissionsSerializerMixin, self).__init__(*args, **kwargs)
        self._filter_related_fields_for_html()

    def _filter_related_fields_for_html(self):
        """
        Ensure thatk related fields are ownership filtered for
        the browseable HTML views.
        """
        import views
        try:
            # related_queryset_filters is a map of the fieldname and the viewset name (str)
            related_queryset_filters = self.Meta.related_queryset_filters
        except AttributeError:
            related_queryset_filters = {}
        for field, viewset in related_queryset_filters.items():
            try:
                self.fields[field].queryset = self._filter_related_qs(self.context['request'], getattr(views, viewset))
            except KeyError:
                pass

    def _filter_related_qs(self, request, ViewSet):
        """
        Helper function to filter related fields using
        existing filtering logic in ViewSets.
        """
        view = ViewSet()
        view.request = request
        view.action = 'retrieve'
        queryset =  view.get_queryset()
        try:
            return view.queryset_ownership_filter(queryset)
        except AttributeError:
            return queryset

我使用 View mixin 解决了这个问题:#1935 而不是混合序列化程序和视图。 我不需要字典,而是在视图上使用了secured_fields的列表。

我使用这个简单的 Serializer mixin 来过滤相关字段中的查询集:

class FilterRelatedMixin(object):
    def __init__(self, *args, **kwargs):
        super(FilterRelatedMixin, self).__init__(*args, **kwargs)
        for name, field in self.fields.iteritems():
            if isinstance(field, serializers.RelatedField):
                method_name = 'filter_%s' % name
                try:
                    func = getattr(self, method_name)
                except AttributeError:
                    pass
                else:
                    field.queryset = func(field.queryset)

用法也很简单:

class SocialPageSerializer(FilterRelatedMixin, serializers.ModelSerializer):
    account = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = models.SocialPage

    def filter_account(self, queryset):
        request = self.context['request']
        return queryset.filter(user=request.user)

如何? 你看到它有什么问题吗?

对我来说,我喜欢将有关用户和请求的逻辑保留在序列化程序之外,并将其留在视图中。

问题出在相关领域。 用户可以访问视图,但
不是所有相关的对象。

2014 年 11 月 5 日星期三下午 6:16,Alex Rothberg通知@ github.com
写道:

对我来说,我喜欢将有关用户和请求的逻辑排除在外
序列化程序并将其留在视图中。


直接回复此邮件或在 GitHub 上查看
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

请阅读此内容,似乎是相关主题。 我们如何为 OPTIONS 方法和 POST 或 PUT 方法分离过滤相关字段对象的 Meta(ModelSerializer)?

https://groups.google.com/forum/#!topic/django -rest-framework/jMePw1vS66A

如果我们设置model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.none()),那么我们不能用相关的模型实例保存当前对象,因为密封器PrimaryKeyRelatedField“在验证字段输入时用于模型实例查找的查询集”。

如果 model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.all()) (作为 ModelSerializer 的默认值,我们可以对此进行评论),则所有“相关”对象(它们实际上并不相关,因为 OPTIONS 显示操作(POST,PUT)主模型类的属性,而不是具有相关对象的实例)显示在“模型”字段的选择中(OPTIONS 方法)。

更新。 @cancan101 +1。 但不仅仅是“用户”。 我认为,这是混合逻辑和序列化程序的坏主意,因为我在序列化程序中看到查询集:“serializers.PrimaryKeyRelatedField(queryset=”。

当然,这是好的:

类 ModelSerializer:
元类:
型号=型号

因为序列化程序必须知道如何以及从模型自动创建哪些字段。

尽管如此,我可能是错的。

这似乎有效:

class BlogSerializer(serializers.ModelSerializer):

    entries = serializers.SerializerMethodField()

    class Meta:
        model = Blog

    def get_entries(self, obj):
        queryset = obj.entries.all()
        if 'request' in self.context:
            queryset = queryset.filter(author=self.context['request'].user)
        serializer = EntrySerializer(queryset, many=True, context=self.context)
        return serializer.data

@dustinfarris这使它成为只读字段......但它确实有效。

遇到一个似乎与此线程相关的问题。 当启用过滤后端(在我的例子中是 Django 过滤器)时,可浏览的 API 会向界面添加一个Filters按钮,据我所知,下拉列表不尊重字段上设置的查询集。 在我看来应该是这样。

例子:

class Item(models.Model):
    project = models.ForeignKey(Project)

class ItemSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        request = kwargs.get('context', {}).get('request')
        self.fields['project'].queryset = request.user.project_set.all()
        super(ItemSerializer, self).__init__(*args, **kwargs)

上面的示例将项目添加/编辑表单的项目下拉列表限制为正确的项目,但所有内容仍显示在Filters下拉列表中。

钉枪的方法对我来说效果很好,但仅适用于一对多关系。 现在,我有一个模型,其中我的关系是 ManyToManyField。 在这种情况下,Mixin 方法不起作用。 知道如何解决这些问题吗?

@fibbs通过添加以下内容钉枪的方法:

            if isinstance(field, serializers.ManyRelatedField):
                method_name = 'filter_%s' % name
                try:
                    func = getattr(self, method_name)
                except AttributeError:
                    pass
                else:
                    field.child_relation.queryset = func(field.child_relation.queryset) 

显然有人为此提供了一个干净的解决方案,现在无需破解 init 方法即可实现: https :

我想知道为什么这个线程还没有更新/关闭?

那个人是我 ;)
好点,我将关闭它,因为#3605 已经在文档中添加了一些相关内容。
如果有人可以提供一些东西,我们仍然会考虑对该部分进行进一步改进。

很高兴get_queryset()方法现在存在于相关字段中。 不过,将它用于嵌套的序列化程序也很棒!

大概。 想要继续吗? :)

哇......太棒了,如果有记录,你能指出我正确的方向吗? 我花了很长时间才弄明白这个!

这是我的教育问题的摘要:

对于此示例,我的模型名称是“deployedEnvs”和“Host”。
deployEnvs 包含一个到 Host 模型的外键(即许多 deployEnvs 可以指向同一个主机)。 我需要序列化程序来显示 HOST 的 fqdn 字段而不是主机的 PK(我使用了非常简单的 slug 相关字段)。 在创建 deployEnv 条目 (POST) 时,我还需要能够通过 FQDN 字段查找相关 HOST 的 FK 值来指定主机。 示例:通过查找具有匹配 host.fqdn 字段的 Host 对象的 PK 来创建带有字段 host(设置为匹配相关主机对象的 fqdn)的 deployEnv。

不幸的是,我无法将下拉栏中的返回结果限制为仅选择当前用户拥有的主机对象。

这是我的修复代码,适用于使用 slugRelatedField

class UserHostsOnly(serializers.SlugRelatedField):
    def get_queryset(self):
        user = self.context['request'].user
        queryset = Host.objects.filter(owner=user)
        return queryset

class deployEnvSerializer(serializers.ModelSerializer):
    host = UserHostsOnly(slug_field='fqdn')

我对 Django 有大约 5 本书(如果你愿意,我可以将它们全部列出),并且没有任何参考文本显示如何使用框架的这个特定区域/功能。 一开始我以为我做错了什么,是吗? 有没有更好的方法来做我想做的事情? 请随时与我 OOB 联系,这样我就不会在此问题上捏造评论。 感谢大家花时间阅读我的评论(作为 django 新手,这真的很难弄清楚)。

@Lcstyle

DRF 中的每个Field (包括Serializers本身)都有 2 个用于序列化数据输入和输出的核心方法(即在 JSON 和 Python 类型之间):

  1. to_representation - 数据“流出”
  2. to_internal_value - 数据“进来”

除了您提供的模型的粗略轮廓之外,以下是相关字段如何工作的概述,其中 SlugRelatedField 是一个专门版本:

class UserHostsRelatedField(serializers.RelatedField):
    def get_queryset(self):
        # do any permission checks and filtering here
        return Host.objects.filter(user=self.context['request'].user)

    def to_representation(self, obj):
        # this is the data that "goes out"
        # convert a Python ORM object into a string value, that will in turn be shown in the JSON
        return str(obj.fqdn)

    def to_internal_value(self, data):
        # turn an INCOMING JSON value into a Python value, in this case a Django ORM object
        # lets say the value 'ADSF-1234'  comes into the serializer, you want to grab it from the ORM
        return self.get_queryset().get(fqdn=data)

实际上,您通常希望在get_querysetto_internal_value方法中放置一堆检查,例如安全性(如果使用类似django-guardianrules ) 并确保实际的 ORM 对象存在。

更完整的示例可能如下所示

from rest_framework.exceptions import (
    ValidationError,
    PermissionError,
)
class UserHostsRelatedField(serializers.RelatedField):
    def get_queryset(self):
        return Host.objects.filter(user=self.context['request'].user)

    def to_representation(self, obj):
        return str(obj.fqdn)

    def to_internal_value(self, data):
        if not isinstance(data, str):
            raise ValidationError({'error': 'Host fields must be strings, you passed in type %s' % type(data)})
        try:
            return self.get_queryset().get(fqdn=data)
        except Host.DoesNotExist:
            raise PermissionError({'error': 'You do not have access to this resource'})

关于@cancan101 前段时间写的内容:

遇到一个似乎与此线程相关的问题。 当启用过滤后端(在我的情况下为 Django 过滤器)时,可浏览的 API 会向界面添加一个过滤器按钮,据我所知,下拉列表不尊重字段上设置的查询集。 在我看来应该是这样。

就我所见,这仍然是正确的。 这可以通过外键字段的自定义Filterset字段来解决,泄漏数据,但@tomchristie我仍然认为这应该“自动”解决,并且过滤器模型选择应该尊重get_queryset方法序列化程序中的自定义字段声明。

在任何情况下,它都需要额外的文件。

我在下面记录了如何通过自定义过滤器集解决这个问题:

示例工作模型:

class WorkEntry(models.Model):
   date = models.DateField(blank=False, null=True, default=date.today)
   who = models.ForeignKey(User, on_delete=models.CASCADE)
   ...

基本模型视图集:

class WorkEntryViewSet(viewsets.ModelViewSet):
   queryset = WorkEntry.objects.all().order_by('-date')
   # only work entries that are owned by request.user are returned
   filter_backends = (OnlyShowWorkEntriesThatAreOwnedByRequestUserFilterBackend, ...)
   # 
   filter_fields = (
      # this shows a filter dropdown that contains User.objects.all() - data leakage!
      'who',
   )
   # Solution: this overrides filter_fields above
   filter_class = WorkentryFilter

自定义过滤器集(通过基础模型视图集中的 filter_class 覆盖filter_fields

class WorkentryFilter(FilterSet):
    """
    This sets the available filters and filter types
    """
    # foreignkey fields need to be overridden otherwise the browseable API will show User.objects.all()
    # data leakage!
    who = ModelChoiceFilter(queryset=who_filter_function)

    class Meta:
        model = WorkEntry
        fields = {
            'who': ('exact',),
        }

此处记录的可调用查询集: http :

def who_filter_function(request):
    if request is None:
        return User.objects.none()
   # this solves the data leakage via the filter dropdown
   return User.objects.filter(pk=request.user.pk)

@马科洛

看看这段代码:

这不能解决您所指的数据泄漏问题吗? 我的搜索字段存在于可浏览的 api 中,但结果仍然仅限于所有者过滤的查询集。

class HostsViewSet(DefaultsMixin, viewsets.ModelViewSet):
    search_fields = ('hostname','fqdn')
    def get_queryset(self):
        owner = self.request.user
        queryset = Host.objects.filter(owner=owner)
        return queryset

@Lcstyle我不是要过滤主机,而是要过滤相关字段的实例(例如主机的所有者)

我正在研究这个我想在我的 REST 中解决的特定问题......通常这些示例基于request.user 。 我想处理一个稍微复杂的案例。

假设我有一个CompanyEmployees并且Company有一个雇员属性:

class Company(Model):
   employee_of_the_month = ForeignKey(Employee)
   ...

class Employee(Model):
    company = ForeignKey(Company)

我希望 REST 接口将employee_of_the_month限制Employee并使用与company.id相同的Company

这是我目前想到的

class CompanySerializer(ModelSerializer):
   employee_of_the_month_id = PrimaryKeyRelatedField(
     source='employee_of_the_month',
     queryset=Employee.objects.all())

   def __init__(self, *args, **kwargs):                                        
        super(CompanySerializer, self).__init__(*args, **kwargs)              
        view = self.context.get('view', None)                                   
        company_id = None                                                     
        if view and isinstance(view, mixins.RetrieveModelMixin):                
            obj = view.get_object()                                             
            if isinstance(obj, Company):   #  We could get the model from the queryset.                                     
                company_id = obj.id                                           
        q = self.fields['employee_of_the_month_id'].queryset
        self.fields['employee_of_the_month_id'].queryset = q.filter(company_id=company_id)

...这个方法是可以抽象的吗? 它基于@nailgunhttps://github.com/encode/django-rest-framework/issues/1985#issuecomment -61871134

我还认为我也可以validate()使employee_of_the_month通过尝试对带有employee_of_the_month.id查询集执行get()来满足上面构建的查询集

看看#3605,我发现这也可以使用字段的自定义序列化程序来完成——让我们使用 CEO 而不是每月的员工:

 class CEOField(serializers.PrimaryKeyRelatedField):                 

      def get_queryset(self):                                                     
          company_id = None                                                     
          view = self.context.get('view', None)                                   
          if view and isinstance(view, mixins.RetrieveModelMixin):                
              obj = view.get_object()                                             
              if isinstance(obj, Company):                                      
                  dashboard_id = obj.id                                           
          return Employee.objects.filter(company_id=company_id)           

这是专门为不返回任何对象而设计的,除非我们正在寻找特定的公司。 换句话说,一家新公司只有在有了员工之后才能拥有 CEO,而在公司创建之前,你不可能拥有 CEO。

我对这种方法唯一的遗憾是这似乎可以成为 DRYer/generic。

此页面是否有帮助?
0 / 5 - 0 等级