Django-rest-framework: Документируйте, как обрабатывать разрешения и фильтрацию для связанных полей.

Созданный на 23 окт. 2014  ·  26Комментарии  ·  Источник: encode/django-rest-framework

В настоящее время отношения не применяют автоматически тот же набор разрешений и фильтрации, которые применяются к представлениям. Если вам нужны разрешения или фильтрация отношений, вам нужно разобраться с этим явным образом.

Лично я не вижу хороших способов справиться с этим автоматически, но, очевидно, это то, что мы могли бы, по крайней мере, сделать с помощью более качественного документирования.

Прямо сейчас я считаю, что мы должны попытаться придумать простой пример и подробно задокументировать, как вы с этим справитесь. Любой автоматический код для решения этой проблемы следует оставить для обработки сторонними разработчиками пакетов. Это позволяет другим участникам изучить проблему и посмотреть, могут ли они предложить какие-либо хорошие решения, которые потенциально могут быть включены в ядро.

В будущем эта проблема может быть переведена из «Документация» в «Улучшение», но, если нет каких-либо конкретных предложений, подкрепленных пакетом третьей части, она останется в этом состоянии.

Documentation

Самый полезный комментарий

Я использовал этот простой миксин Serializer для фильтрации наборов запросов в связанных полях:

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» для каждого связанного объекта. Прямо сейчас сериализатор не имеет доступа к объекту разрешения.

Прямо сейчас сериализатор не имеет доступа к объекту разрешения.

Только вот это не так просто.

_Какой_ объект разрешений? Это отношения с _другими_ объектами, поэтому классы разрешений и фильтров в текущем представлении не обязательно будут теми же правилами, которые вы хотели бы применить к отношениям объектов.

Для гиперссылок теоретически вы могли бы определить представление, на которое они указали (+), и определить фильтрацию / разрешения на основе этого, но это, безусловно, закончится как ужасно тесно связанный дизайн. Для отношений без гиперссылок вы даже не можете этого сделать. Нет гарантии, что каждая модель будет представлена ​​один раз в одном каноническом представлении, поэтому вы не можете попытаться автоматически определить разрешения, которые вы хотите использовать для отношений без гиперссылок.

(+) На самом деле, вероятно, на самом деле невозможно сделать это каким-либо _смысленным_ способом, но давайте пока сделаем вид.

Может быть, есть "has__permission "? Тогда каждый объект разрешений сможет определить, какие связанные объекты доступны для просмотра или нет.

Как люди используют фильтрацию? Они используют его только для того, чтобы скрыть объекты, у которых нет прав доступа? Потому что, если это так, то, возможно, фильтры не нужны.

Одна из упомянутых проблем # 1646 связана с ограничением выбора, отображаемого на доступных для просмотра страницах API для связанных полей.

Мне нравится просматриваемый API, и я считаю его отличным инструментом не только для меня как внутреннего разработчика, но и для внешних разработчиков / пользователей REST API. Я хотел бы отправить продукт с включенным API с возможностью просмотра (т. Е. Он работает, даже если сайт не одинок в режиме DEBUG). Чтобы я мог это сделать, я не могу допустить утечки информации через просматриваемые страницы API. (Это, конечно, в дополнение к требованию, чтобы эти страницы, как правило, были готовы к работе и были безопасны).

Это означает, что с помощью HTML-страниц нельзя получить больше информации о существовании связанных полей, чем можно было бы узнать с помощью POSTing.

В итоге я создал класс миксина для своих сериализаторов, который использует View связанного поля для обеспечения фильтрации.

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: # 1935 вместо того, чтобы смешивать сериализаторы и представления. Вместо того, чтобы нуждаться в словаре, я просто использовал список secured_fields в представлении.

Я использовал этот простой миксин Serializer для фильтрации наборов запросов в связанных полях:

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)

Как это? Вы видите с этим какие-то проблемы?

Что касается меня, мне нравится хранить логику о пользователях и запросах вне сериализатора и оставлять это в представлении.

Проблема связана со связанными полями. Пользователь может иметь доступ к представлению, но
не ко всем связанным объектам.

В среду, 5 ноября 2014 г., в 18:16, Алекс Ротберг [email protected]
написал:

Мне нравится держать логику в отношении пользователей и запросов вне
Сериализатор и оставьте его в представлении.

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

Пожалуйста, прочтите это, кажется, это родственная тема. Как мы можем разделить фильтрацию объектов связанных полей Meta (ModelSerializer) для метода OPTIONS и метода POST или PUT?

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

Если мы установим model = serializers.PrimaryKeyRelatedField (queryset = Model.objects.none ()), то мы не сможем сохранить текущий объект со связанным экземпляром модели, потому что Sealizer PrimaryKeyRelatedField «набор запросов, используемый для поиска экземпляра модели при проверке ввода поля».

Если model = serializers.PrimaryKeyRelatedField (queryset = Model.objects.all ()) (по умолчанию для ModelSerializer мы можем прокомментировать это), тогда все «связанные» объекты (на самом деле они не связаны, потому что OPTIONS отображают действия (POST, PUT) свойства для основного класса модели, а не экземпляра со связанными объектами), отображаемые в вариантах выбора для поля «модель» (метод OPTIONS).

Обновить. @ cancan101 +1. Но не только «пользователь». Я думаю, что это плохая идея сочетать логику и сериализаторы, поскольку я вижу набор запросов в сериализаторах: "serializers.PrimaryKeyRelatedField (queryset =".

конечно, хорошо:

класс ModelSerializer:
класс Мета:
model = Модель

потому что сериализатор должен знать, как и какие поля автоматически создаются из модели.

Тем не менее, я мог ошибаться.

Кажется, это работает:

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 Filter) включен, просматриваемый 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 .

Подход nailgun хорошо сработал для меня, но только для отношений «один ко многим». Теперь у меня есть одна модель, в которой мои отношения - это 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) 

По-видимому, кто-то внес в это чистое решение, и теперь это возможно без взлома методов инициализации: https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e

Интересно, почему эта ветка не обновлялась / не закрывалась?

что кто-то есть я;)
Хороший момент, я собираюсь закрыть это, поскольку # 3605 уже добавляет кое-что об этом в документации.
Мы по-прежнему будем рассматривать дальнейшие улучшения этой части, если кто-то сможет что-то придумать.

Замечательно, что метод get_queryset() теперь существует для связанных полей. Хотя было бы здорово иметь его и для вложенных сериализаторов!

Наверное. Хотите продолжить? :)

Вау ... замечательно, если это задокументировано, не могли бы вы указать мне правильное направление? Мне потребовалось много времени, чтобы понять это!

Вот краткое изложение моей проблемы для назидания:

В этом примере моими названиями моделей являются «deployedEnvs» и «Host».
deployedEnvs содержит внешний ключ для модели хоста (т.е. многие deployedEnvs могут указывать на один и тот же хост). Мне нужен сериализатор для отображения поля fqdn HOST, а не PK для хоста (я использовал поле, связанное с slug, для этого было довольно просто). Мне также нужно было при создании записи deployedEnv (POST), чтобы иметь возможность указать хост, ища значение FK для соответствующего HOST по полю FQDN. Пример: создайте deployedEnv с полевым хостом (настроенным на соответствие fqdn соответствующего хост-объекта) путем поиска PK для хост-объекта с полем match host.fqdn.

К сожалению, я не мог ограничить возвращаемые результаты в раскрывающемся списке только вариантами для объектов хоста, которые принадлежат текущему пользователю.

Вот мой код исправления, адаптированный для использования 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 (я могу перечислить их все, если хотите), и ни один из справочных текстов не показывает, как работать с этой конкретной областью / функциональностью Framework. Сначала я подумал, что делаю что-то не так, не так ли? Есть ли лучший способ сделать то, что я пытаюсь сделать? Не стесняйтесь обращаться ко мне OOB, чтобы я не обманул комментарии по этой проблеме. Спасибо всем, что нашли время прочитать мой комментарий (как новичку в django, это было действительно сложно понять).

@Lcstyle

У каждого Field в DRF (включая сами 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_queryset или to_internal_value для таких вещей, как безопасность (если вы используете что-то вроде django-guardian или rules ), а также чтобы убедиться, что фактический объект 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 Filter) включен, просматриваемый API добавляет кнопку Filters в интерфейс, и, насколько я могу судить, раскрывающийся список не учитывает набор запросов, заданный в поле. Мне кажется, так и должно быть.

Насколько я понимаю, это все еще верно. Это можно исправить с помощью настраиваемого поля 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

Custom FilterSet (заменяет filter_fields через filter_class в наборе представлений базовой модели)

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',),
        }

queryset, вызываемый, как описано здесь: http://django-filter.readthedocs.io/en/latest/ref/filters.html#modelchoicefilter

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)

@macolo

взгляните на этот код:

Разве это не решает проблему утечки данных, о которой вы говорите? Мои поля поиска присутствуют в доступном для просмотра 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 . Я хотел бы рассмотреть более сложный случай.

Допустим, у меня есть Company которого есть Employees а у 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)

... этот метод можно абстрагировать? Он основан немного на @nailgun «s https://github.com/encode/django-rest-framework/issues/1985#issuecomment -61871134

Я также думаю, что могу также validate() что employee_of_the_month удовлетворяет построенному выше набору запросов, пытаясь выполнить get() против набора запросов с помощью employee_of_the_month.id

Глядя на # 3605, я вижу, что это также можно сделать с помощью настраиваемого сериализатора для поля - давайте использовать CEO вместо Employee of the Month:

 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)           

Это разработано специально, чтобы не возвращать объекты для выбора, если мы не смотрим на конкретную компанию. Другими словами, у новой компании не может быть генерального директора, пока в ней не появятся сотрудники, а у вас не может быть до тех пор, пока компания не будет создана.

Единственное, о чем я сожалею об этом подходе, это похоже на то, что это можно сделать DRYer / generic.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги