DRF:如何基于字段权限限制嵌套序列化器中的字段?

时间:2019-08-06 07:58:14

标签: django serialization django-rest-framework permissions

我试图根据用户权限限制序列化程序中的fields列表。我有一个通用例程,可以对所有序列化程序执行此操作。它适用于父级序列化程序,但不适用于嵌套序列化程序。

我有一个客户模型和一个客户资料(称为“联系人”),如下所示。客户端配置文件模型是用户模型(一对一关系)的扩展。

class Client(AddressPhoneModelMixin, DateFieldsModelMixin, models.Model):
    name = models.CharField(max_length=100)
    status = models.CharField(max_length=25)

    class Meta:
        permissions = (
            # Object-level
            ('view_all_clients', 'Can view all clients'),
            ('change_all_clients', 'Can change all clients'),
            # Field-level
            ('view_client_id', 'Can view client ID'),
            ('view_client_name', 'Can view client name'),
            ...others omitted...
        )
class ClientProfile(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
    )
    client = models.ForeignKey(
        Client,
        on_delete=models.PROTECT,
        related_name='contacts',
    )
    receive_invoices = models.BooleanField(default=False)

我的对象级权限逻辑在列表视图中:

class ClientList(ListAPIView):
    permission_classes = (IsAuthenticated,)
    serializer_class   = ClientSerializer

    def get_queryset(self):
        user     = self.request.user
        queryset = None

        if user.has_perm('view_client') or user.has_perm('clients.view_all_clients'):
            queryset = Client.objects.all().exclude(status__in=['deleted', 'archived'])

        if user.has_perm('view_client'):        # View only "assigned" clients
            if user.type == 'client':
                # See if user is a "contact".
                queryset = queryset.distinct().filter(contacts__user=self.request.user)
            else:
                # See if user is assigned to projects for the client(s).
                queryset = queryset.distinct().filter(projects__project_users__user=self.request.user)

        if queryset is None:
            raise PermissionDenied('You do not have permission to view clients.')
        return self.get_serializer_class().setup_eager_loading(queryset)

从序列化程序“ fields”属性中删除字段是通过序列化程序__init__方法完成的(来自我在SO的示例中):

class ClientContactsSerializer(serializers.ModelSerializer):
    url  = serializers.HyperlinkedIdentityField(view_name='clients:clientprofile-detail')
    user = UserSerializer()

    class Meta:
        model = ClientProfile
        fields = (
            'url',
            'receive_invoices',
            'user',
        )

    def __init__(self, *args, **kwargs):
        super(ClientContactsSerializer, self).__init__(*args, **kwargs)
        check_field_permissions(self, 'view')
class ClientSerializer(AddressPhoneSerializerMixin, serializers.ModelSerializer):
    url      = serializers.HyperlinkedIdentityField(view_name='clients:client-detail')
    contacts = ClientContactsSerializer(many=True, read_only=True)
    projects = ClientProjectsSerializer(many=True, read_only=True)

    class Meta:
        model = Client
        fields = (
            'url',
            'id',
            'name',
            ...omitted for brevity...
            'contacts',
            'projects',
        )

    def __init__(self, *args, **kwargs):
        super(ClientSerializer, self).__init__(*args, **kwargs)
        check_field_permissions(self, 'view')

    @staticmethod
    def setup_eager_loading(queryset):
        queryset = queryset.select_related('country')
        return queryset.prefetch_related('contacts', 'contacts__user', 'contacts__user__country', 'projects')

最后,这是check_field_permissions函数:

def check_field_permissions(serializer, action='view'):
    request = serializer.context.get('request', None)
    fields  = serializer.get_fields()
    model   = serializer.Meta.model

    app_name   = model._meta.app_label
    model_name = model._meta.model_name

    if request is not None and app_name is not None and model_name is not None:
        user = request.user

        for field_name in fields:
            if hasattr(serializer.fields[field_name], 'child'):
                continue
            if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
                serializer.fields.pop(field_name)

在Client列表的页面加载过程中逐步调试,我可以看到上面的函数首先为clientprofile调用,并且请求为None。第二次,它为客户端调用,并且请求是有效的请求对象。

第一个问题,__init__是否是限制要序列化的字段列表的正确位置?

第二,如何在嵌套的序列化程序(客户端配置文件)中获取请求对象?

1 个答案:

答案 0 :(得分:0)

在阅读了无可争议的DRF权威汤姆·克里斯蒂(Tom Christie)的帖子后,我得以解决我的问题。他指出,每个嵌套的序列化程序实际上都具有上下文对象(以及请求和用户)。您只需要处理父__init__中的嵌套序列化程序,而不是它们自己的__init__中的嵌套序列化程序。

这是我修改后的check_field_permissions()函数:

def check_field_permissions(serializer, action='view'):
    request = serializer.context.get('request', None)
    fields  = serializer.get_fields()
    model   = serializer.Meta.model

    app_name   = model._meta.app_label
    model_name = model._meta.model_name

    if request is not None and app_name is not None and model_name is not None:
        user         = request.user
        extra_fields = []

        for field_name in fields:
            if field_name == 'url':
                extra_fields.append(field_name)
                continue
            if hasattr(serializer.fields[field_name], 'child'):
                check_field_permissions(serializer.fields[field_name].child, action)
                extra_fields.append(field_name)
                continue
            if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
                serializer.fields.pop(field_name)

        # If only "url" and child fields remain, remove all fields.
        if len(serializer.fields) == len(extra_fields):
            for field_name in extra_fields:
                serializer.fields.pop(field_name)

现在是递归的。如果它命中具有“ child”属性的字段,则说明它是嵌套的序列化程序字段。它使用作为arg传递的子序列化器来调用自身。

另一个变化是__init__已从ClientContactsSerializer中删除,因为它不需要调用check_field_permissions()。