Django-rest-framework: Documentez comment gérer les autorisations et le filtrage des champs associés.

Créé le 23 oct. 2014  ·  26Commentaires  ·  Source: encode/django-rest-framework

Actuellement, les relations n'appliquent pas automatiquement le même ensemble d'autorisations et de filtrage que celui appliqué aux vues. Si vous avez besoin d'autorisations ou de filtres sur les relations, vous devez les traiter explicitement.

Personnellement, je ne vois pas de bons moyens de gérer cela automatiquement, mais c'est évidemment quelque chose que nous pourrions au moins faire en mieux documenter.

À l'heure actuelle, mon opinion est que nous devrions essayer de proposer un cas d'exemple simple et documenter la manière dont vous traiteriez cela de manière explicite. Tout code automatique permettant de gérer cela doit être laissé à la discrétion des auteurs de packages tiers. Cela permet à d'autres contributeurs d'explorer le problème et de voir s'ils peuvent trouver de bonnes solutions qui pourraient potentiellement être incluses dans le noyau.

À l'avenir, ce problème pourrait passer de « Documentation » à « Amélioration », mais à moins qu'il n'y ait des propositions concrètes qui soient soutenues par un package tiers, il restera dans cet état.

Documentation

Commentaire le plus utile

J'ai utilisé ce simple mixin Serializer pour filtrer les ensembles de requêtes dans les champs connexes :

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)

L'utilisation est simple aussi :

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)

Comment c'est? Voyez-vous des problèmes avec cela?

Tous les 26 commentaires

J'ai essayé de creuser dans le code, mais je n'ai pas trouvé de moyen simple de le faire. Idéalement, vous devriez pouvoir appeler les permissions "has_object_permission" pour chaque objet associé. À l'heure actuelle, le sérialiseur n'a pas accès à l'objet d'autorisation.

À l'heure actuelle, le sérialiseur n'a pas accès à l'objet d'autorisation.

Sauf que ce n'est pas aussi simple que cela.

_Quel_ objet d'autorisation ? Ce sont des relations avec _other_ objets, donc les classes d'autorisation et de filtre sur la vue actuelle ne seront pas nécessairement les mêmes règles que vous voudriez appliquer aux relations d'objet.

Pour les relations hyperliées, vous pourriez en théorie déterminer la vue vers laquelle elles pointaient (+) et déterminer le filtrage/les autorisations en fonction de cela, mais cela finirait certainement par être une horrible conception étroitement couplée. Pour les relations sans hyperlien, vous ne pouvez même pas faire cela. Il n'y a aucune garantie que chaque modèle soit exposé une fois sur une seule vue canonique, vous ne pouvez donc pas essayer de déterminer automatiquement les autorisations que vous souhaitez utiliser pour les relations sans lien hypertexte.

(+) En fait, il n'est probablement pas possible de le faire d'une manière _sensible_, mais faisons semblant pour le moment.

Peut-être avoir un "ha__permission"? Chaque objet d'autorisation serait alors en mesure de dire quels objets liés sont visibles ou non.

Comment les gens utilisent-ils le filtrage ? L'utilisent-ils uniquement pour masquer des objets dont l'utilisateur n'a pas les autorisations ? Parce que si c'est le cas d'utilisation, alors peut-être que les filtres ne sont pas nécessaires.

L'un des problèmes référencés #1646 concerne la limitation des choix affichés sur les pages d'API navigables pour les champs connexes.

J'adore l'API explorable et je pense que c'est un excellent outil non seulement pour moi en tant que développeur back-end, mais aussi pour les développeurs / utilisateurs frontaux de l'API REST. J'adorerais expédier le produit avec l'API navigable activée (c'est-à-dire qu'elle fonctionne même lorsque le site n'est plus en mode DEBUG). Pour que je puisse le faire, je ne peux pas faire en sorte que des fuites d'informations se produisent via les pages d'API navigables. (Ceci en plus de l'exigence que ces pages soient généralement prêtes pour la production et sécurisées).

Cela signifie que pas plus d'informations sur l'existence de champs liés ne devraient être apprises via les pages HTML qu'elles ne le seraient via le POST.

J'ai fini par créer une classe mixin pour mes sérialiseurs qui utilise la vue du champ connexe pour fournir le filtrage.

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

J'ai résolu ce problème en utilisant un mixin View: #1935 plutôt que de mélanger des sérialiseurs et des vues. Plutôt que d'avoir besoin d'un dictionnaire, j'ai juste utilisé une liste de secured_fields sur la vue.

J'ai utilisé ce simple mixin Serializer pour filtrer les ensembles de requêtes dans les champs connexes :

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)

L'utilisation est simple aussi :

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)

Comment c'est? Voyez-vous des problèmes avec cela?

Pour moi, j'aime garder la logique sur les utilisateurs et les demandes hors du sérialiseur et laisser cela dans la vue.

Le problème est avec les domaines connexes. Un utilisateur peut avoir accès à la vue mais
pas à tous les objets liés.

Le mer. 5 novembre 2014 à 18h16, Alex Rothberg [email protected]
a écrit:

Pour moi, j'aime garder la logique sur les utilisateurs et les demandes hors du
Sérialiseur et laissez-le dans la vue.

-
Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

S'il vous plaît, lisez ceci, il semble que ce soit un sujet lié. Comment pouvons-nous séparer les objets de champs liés au filtrage de Meta (ModelSerializer) pour la méthode OPTIONS et une méthode POST ou PUT ?

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

Si nous définissons model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.none()), alors nous ne pouvons pas enregistrer l'objet actuel avec l'instance de modèle associée, car le sealizer PrimaryKeyRelatedField "requête utilisé pour les recherches d'instance de modèle lors de la validation de l'entrée de champ".

Si model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.all()) (par défaut pour ModelSerializer, nous pouvons le commenter), alors tous les objets "liés" (ils ne sont pas vraiment liés, car les OPTIONS affichent les actions (POST, PUT) propriétés de la classe Model principale, pas d'instance avec des objets associés) affichées dans les choix du champ "modèle" (méthode OPTIONS).

mettre à jour. @cancan101 +1 . Mais pas seulement "utilisateur". Je pense que c'est une mauvaise idée de mélanger la logique et les sérialiseurs, car je vois le jeu de requêtes dans les sérialiseurs : "serializers.PrimaryKeyRelatedField(queryset=".

bien sur c'est bon :

classe ModelSerializer :
classe Meta :
modèle=Modèle

car Serializer doit savoir comment et quels champs se créent automatiquement à partir du modèle.

Néanmoins, je peux me tromper.

Cela semble fonctionner:

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 Cela en fait un champ en lecture seule... mais cela fonctionne.

J'ai rencontré un problème qui semble lié à ce fil. Lorsqu'un backend de filtrage (Django Filter dans mon cas) est activé, l'API explorable ajoute un bouton Filters à l'interface et, pour autant que je sache, cette liste déroulante ne respecte pas le jeu de requêtes défini sur le champ. Il me semble que cela devrait.

Exemple:

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)

L'exemple ci-dessus limite la liste déroulante des projets du formulaire d'ajout/modification d'article aux projets corrects, mais tout s'affiche toujours dans la liste déroulante Filters .

L'approche de nailgun a plutôt bien fonctionné pour moi, mais uniquement pour les relations One-to-Many. Maintenant, j'ai un modèle où ma relation est un ManyToManyField. Dans ce cas, l'approche Mixin ne fonctionne pas. Une idée de comment le résoudre pour ceux-ci?

@fibbs change l'approche de nailgun en ajoutant ce qui suit :

            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) 

Apparemment, quelqu'un a apporté une solution propre à cela et c'est maintenant possible sans pirater les méthodes d'initialisation : https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e

Je me demande pourquoi ce fil n'a pas été mis à jour/fermé ?

ce quelqu'un étant moi ;)
Bon point, je vais fermer ceci car #3605 ajoute déjà quelque chose à ce sujet dans la documentation.
Nous envisagerons toujours d'apporter d'autres améliorations à cette partie si quelqu'un peut proposer quelque chose.

C'est formidable que la méthode get_queryset() existe maintenant pour RelatedFields. Ce serait génial de l'avoir aussi pour les sérialiseurs imbriqués !

Probablement. Vous voulez aller de l'avant ? :)

Wow... incroyable, si cela est documenté, pouvez-vous s'il vous plaît m'orienter dans la bonne direction ? J'ai mis du temps à comprendre celui-ci !

Voici un résumé de mon problème pour l'édification :

Pour cet exemple, mes noms de modèle sont "deployedEnvs" et "Host".
deployEnvs contient une clé étrangère vers le modèle Host (c'est-à-dire que de nombreux deployEnvs peuvent pointer vers le même hôte). J'avais besoin que le sérialiseur affiche le champ fqdn de HOST plutôt que le PK de l'hôte (j'ai utilisé le champ lié au slug pour ce qui était assez simple). J'avais également besoin, lors de la création d'une entrée deployEnv (POST), de pouvoir spécifier l'hôte en recherchant la valeur FK pour le champ HOST by FQDN pertinent. Exemple : créez deployEnv avec le champ hôte (défini sur le nom de domaine complet correspondant à l'objet hôte pertinent) en recherchant le PK pour l'objet hôte avec le champ correspondant à host.fqdn.

Malheureusement, je n'ai pas pu limiter les résultats renvoyés dans la barre déroulante aux seuls choix d'objets hôtes appartenant à l'utilisateur actuel.

Voici mon code correctif adapté pour utiliser 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')

J'ai environ 5 livres dans Django (je peux tous les lister si vous le souhaitez), et aucun des textes de référence ne montre comment travailler avec cette zone/fonctionnalité particulière du Framework. Au début, je pensais que je faisais quelque chose de mal, n'est-ce pas ? Existe-t-il une meilleure façon de faire ce que j'essaie de faire? N'hésitez pas à me contacter OOB afin que je ne finisse pas par truquer les commentaires pour ce problème. Merci à tous d'avoir pris le temps de lire mon commentaire (en tant que débutant Django, c'était vraiment difficile à comprendre).

@Lcstyle

Chaque Field dans DRF (y compris les Serializers eux-mêmes) a 2 méthodes principales pour sérialiser les données entrantes et sortantes (c'est-à-dire entre les types JSON et Python) :

  1. to_representation - les données sont "sorties"
  2. to_internal_value - données entrantes

En partant des grandes lignes des modèles que vous avez fournis, voici un aperçu du fonctionnement de RelatedFields, SlugRelatedField étant une version spécialisée :

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)

En réalité, vous voulez normalement mettre un tas de chèques dans les méthodes get_queryset ou to_internal_value , pour des choses comme la sécurité (si vous utilisez quelque chose comme django-guardian ou rules ) et aussi pour s'assurer que l'objet ORM réel existe.

Un exemple plus complet pourrait ressembler à ceci

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

En ce qui concerne ce que @cancan101 a écrit il y a quelque temps :

J'ai rencontré un problème qui semble lié à ce fil. Lorsqu'un backend de filtrage (Django Filter dans mon cas) est activé, l'API navigable ajoute un bouton Filtres à l'interface et, pour autant que je sache, cette liste déroulante ne respecte pas le jeu de requêtes défini sur le champ. Il me semble que cela devrait.

C'est toujours vrai d'après ce que je peux voir. Cela peut être résolu via un champ Filterset pour le champ de clé étrangère qui fuit des données, mais @tomchristie, je pense toujours que cela devrait être résolu "automatiquement" et que le choix du modèle de filtre devrait respecter la méthode get_queryset de la déclaration de champ personnalisé dans le sérialiseur.

Dans tous les cas, il faudrait une documentation supplémentaire.

Je documente ci-dessous comment résoudre ce problème via un ensemble de filtres personnalisés :

Exemple de modèle de travail :

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

Ensemble de vues du modèle de base :

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 (remplace filter_fields via filter_class dans l'ensemble de vues du modèle de base)

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

ensemble de requêtes appelable comme documenté ici : 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

regarde ce code :

Cela ne résout-il pas le problème de fuite de données auquel vous faites référence ? Mes champs de recherche sont présents dans l'API explorable, mais les résultats sont toujours limités à l'ensemble de requêtes filtré par propriétaire.

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 Je

Je regarde ce problème particulier que je voudrais résoudre dans mon REST ... généralement, les exemples sont basés sur request.user . J'aimerais traiter un cas un peu plus complexe.

Disons que j'ai un Company qui a Employees et que le Company a un attribut d'employé du mois :

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

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

J'aimerais que l'interface REST limite employee_of_the_month par Employee avec le même company.id que le Company .

C'est ce que j'ai trouvé jusqu'à présent,

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)

...cette méthode est-elle quelque chose qui pourrait être abstrait ? Il est basé un peu sur @nailgun « s https://github.com/encode/django-rest-framework/issues/1985#issuecomment -61.871.134

Je pense aussi que je pourrais aussi validate() que employee_of_the_month satisfasse le jeu de requêtes construit ci-dessus en essayant de faire un get() contre le jeu de requêtes avec le employee_of_the_month.id

En regardant #3605, je vois que cela peut également être fait avec un sérialiseur personnalisé pour le champ - utilisons PDG au lieu de Employé du mois :

 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)           

Ceci est conçu spécifiquement pour ne renvoyer aucun objet à sélectionner, sauf si nous regardons une entreprise spécifique. En d'autres termes, une nouvelle entreprise ne peut pas avoir de PDG tant qu'elle n'a pas d'employés, ce que vous ne pouvez pas avoir tant que l'entreprise n'est pas créée.

Mon seul regret avec cette approche est qu'il semble que cela pourrait être rendu plus SEC/générique.

Cette page vous a été utile?
0 / 5 - 0 notes