Django REST API:使特定权限级别的字段只读

Posted

技术标签:

【中文标题】Django REST API:使特定权限级别的字段只读【英文标题】:Django REST API: Make field read-only for certain permission level 【发布时间】:2016-12-28 17:21:04 【问题描述】:

如何使某些字段对于特定用户权限级别为只读?

有一个 Django REST API 项目。有一个带有 2 个字段的 Foo 序列化程序 - foobar。有 2 个权限 - USERADMIN

序列化器定义为:

class FooSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = FooModel
        fields = ['foo', 'bar']

如何确保“bar”字段对于USER 是只读的,对于ADMIN 是可写的?

我会像这样使用:

class FooSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = FooModel
        fields = ['foo', 'bar']
        read_only_fields = ['bar']

但是如何使它有条件(取决于许可)?

【问题讨论】:

你读过django-rest-framework.org/api-guide/permissions吗? 是的,已阅读参考资料。但还没有找到可以提供信息的信息 - 如何隐藏/显示不同权限的特定字段。 【参考方案1】:

您可以使用视图的get_serializer_class() 方法为不同的用户使用不同的序列化器:

class ForUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = ExampleModel
        fields = ('id', 'name', 'bar')
        read_only_fields =  ('bar',)

class ForAdminSerializer(serializers.ModelSerializer):
    class Meta:
        model = ExampleModel
        fields = ('id', 'name', 'bar', 'for_admin_only_field')

class ExampleView(viewsets.ModelViewSet):    
    ...
    def get_serializer_class(self):
        if self.request.user.is_admin:
            return ForAdminSerializer
        return ForUserSerializer

【讨论】:

这是我选择的解决方案,除非有更好的(通用)方法来做到这一点。但是看了参考页(包括django-rest-framework.org/api-guide/permissions),好像没有。【参考方案2】:

尽管 Fian 的回答似乎是最明显的 documented 方式,但还有一种替代方法可以利用其他文档化代码,并且可以在实例化时将参数传递给序列化程序。

第一个难题是dynamically modifying a serializer at the point of instantiation 上的文档。该文档没有解释如何从视图集中调用此代码或如何在字段启动后修改它们的只读状态 - 但这并不是很难。

第二部分 - get_serializer method 也被记录在案 - (在“其他方法”下的 get_serializer_class 页面下方)所以它应该是安全的(来源非常简单,希望这意味着修改导致意外副作用的可能性较小)。检查 GenericAPIView 下的源代码(ModelViewSet - 以及它看起来的所有其他内置视图集类 - 从 GenericAPIView 继承,它定义了 get_serializer。

将两者放在一起,您可以执行以下操作:

在序列化程序文件中(对我来说是 base_serializers.py):

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""

def __init__(self, *args, **kwargs):
    # Don't pass the 'fields' arg up to the superclass
    fields = kwargs.pop('fields', None)

    # Adding this next line to the documented example
    read_only_fields = kwargs.pop('read_only_fields', None)

    # Instantiate the superclass normally
    super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

    if fields is not None:
        # Drop any fields that are not specified in the `fields` argument.
        allowed = set(fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

    # another bit we're adding to documented example, to take care of readonly fields 
    if read_only_fields is not None:
        for f in read_only_fields:
            try:
                self.fields[f].read_only = True
            exceptKeyError:
                #not in fields anyway
                pass

然后在你的视图中你可能会做这样的事情:

class MyUserViewSet(viewsets.ModelViewSet):
    # ...permissions and all that stuff

    def get_serializer(self, *args, **kwargs):

        # the next line is taken from the source
        kwargs['context'] = self.get_serializer_context()

        # ... then whatever logic you want for this class e.g:
        if self.request.user.is_staff and self.action == "list":
            rofs = ('field_a', 'field_b')
            fs = ('field_a', 'field_c')
        #  add all your further elses, elifs, drawing on info re the actions, 
        # the user, the instance, anything passed to the method to define your read only fields and fields ...
        #  and finally instantiate the specific class you want (or you could just
        # use get_serializer_class if you've defined it).  
        # Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
        kwargs['read_only_fields'] = rofs
        kwargs['fields'] = fs
        return MyUserSerializer(*args, **kwargs)

应该就是这样!使用 MyUserViewSet 现在应该使用您想要的参数来实例化您的 UserSerializer - 并假设您的用户序列化器继承自您的 DynamicFieldsModelSerializer,它应该知道该怎么做。

也许值得一提的是,DynamicFieldsModelSerializer 当然可以很容易地适应做一些事情,比如接受 read_only_exceptions 列表并将其用于白名单而不是黑名单字段(我倾向于这样做)。如果未通过,我还发现将字段设置为空元组很有用,然后只需删除对 None 的检查...我将继承的序列化程序上的字段定义设置为“all”。这意味着在实例化序列化程序时没有未传递的字段意外存活,而且我也不必将序列化程序调用与继承的序列化程序类定义进行比较以了解包含的内容......例如在 init 的 DynamicFieldsModelSerializer:

# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....

NB如果我只想要两个或三个映射到不同用户类型的类和/或我不想要任何特别的动态序列化程序行为,我很可能会坚持 Fian 提到的方法.

但是,在我的例子中,我想根据操作以及发出请求的用户的管理员级别来调整字段,这导致了许多冗长且烦人的序列化程序类名称。创建许多序列化程序类只是为了调整字段列表和只读字段开始觉得很难看。这种方法还意味着字段列表与视图中的相关业务逻辑分开。这是否是一件好事可能值得商榷,但是当逻辑变得更加复杂时,我认为它会使代码更少,而不是更多,可维护。当然,如果您还想在启动序列化程序时做其他“动态”事情,那么使用我上面概述的方法会更有意义。

【讨论】:

in def __init__(self, * args, **kwargs): 当我说 `self.request.user` 我得到一个错误,它说self has no attribute request 那么如何让用户如何提出请求?【参考方案3】:

您可以在序列化程序类中扩展get_fields 方法。在您的情况下,它看起来像这样:

class FooSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = FooModel
        fields = ["foo", "bar"]
    
    def get_fields(self):
        fields = super().get_fields()  # Python 3 syntax
        request = self.context.get("request", None)
        if request and request.user and request.user.is_superuser is False:
            fields["bar"].read_only = True
        return fields

【讨论】:

我在文档中找不到这个。你能指出你在哪里找到它吗? 似乎在 drf 3 中发生了很多变化,这不再是一个解决方案。您可能需要深入研究源代码,看看是否仍然可以这样做

以上是关于Django REST API:使特定权限级别的字段只读的主要内容,如果未能解决你的问题,请参考以下文章

multi-tenant-schemas:使用django rest框架的动态api路由

Django Rest Framework IsAuthenticated 权限错误匿名用户

通过Rest API或客户端DLL创建具有'Contributors'类的Azure Devops(安全)组,并具有权限

Django rest framework ---- 权限

如何在 Django REST 框架中为多对多字段定义“IsOwner”自定义权限?

如何使 Vue.js 将散列密码发布到 Django REST API AbstractBaseUser 自定义模型?