Django-rest-framework: Dokumentieren Sie, wie Sie mit Berechtigungen umgehen und nach verwandten Feldern filtern.

Erstellt am 23. Okt. 2014  ·  26Kommentare  ·  Quelle: encode/django-rest-framework

Derzeit wenden Beziehungen nicht automatisch dieselben Berechtigungen und Filter an, die auf Ansichten angewendet werden. Wenn Sie Berechtigungen benötigen oder nach Beziehungen filtern, müssen Sie sich explizit damit befassen.

Persönlich sehe ich keine guten Möglichkeiten, damit automatisch umzugehen, aber es ist offensichtlich etwas, das wir zumindest besser dokumentieren könnten.

Im Moment bin ich der Meinung, dass wir versuchen sollten, einen einfachen Fallbeispiel zu finden und zu dokumentieren, wie Sie damit explizit umgehen würden. Jeglicher automatischer Code für den Umgang damit sollte den Paketautoren von Drittanbietern überlassen werden. Dies ermöglicht anderen Mitwirkenden, das Problem zu untersuchen und zu sehen, ob sie gute Lösungen finden können, die möglicherweise in den Kern aufgenommen werden könnten.

In Zukunft könnte diese Ausgabe von "Dokumentation" auf "Verbesserung" hochgestuft werden, aber es sei denn, es gibt konkrete Vorschläge, die durch ein drittes Teilpaket unterstützt werden, bleibt es in diesem Zustand.

Documentation

Hilfreichster Kommentar

Ich habe dieses einfache Serializer-Mixin verwendet, um Abfragesätze in verwandten Feldern zu filtern:

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)

Auch die Nutzung ist einfach:

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)

Wie ist das? Siehst du da irgendwelche Probleme?

Alle 26 Kommentare

Ich habe versucht, den Code zu durchsuchen, aber ich konnte keine einfache Möglichkeit finden, dies zu tun. Idealerweise sollten Sie in der Lage sein, die "has_object_permission" der Berechtigungen für jedes zugehörige Objekt aufzurufen. Im Moment hat der Serializer keinen Zugriff auf das Berechtigungsobjekt.

Im Moment hat der Serializer keinen Zugriff auf das Berechtigungsobjekt.

Nur dass das nicht ganz so einfach ist.

_Welches_ Berechtigungsobjekt? Dies sind Beziehungen zu _anderen_ Objekten, daher müssen die Berechtigungs- und Filterklassen in der aktuellen Ansicht nicht unbedingt dieselben Regeln sein, die Sie auf die Objektbeziehungen anwenden möchten.

Für Hyperlink-Beziehungen könnten Sie theoretisch die Ansicht bestimmen, auf die sie (+) zeigten, und basierend darauf die Filterung/Berechtigungen bestimmen, aber es würde sicherlich als schrecklich eng gekoppeltes Design enden. Bei Beziehungen ohne Hyperlink können Sie das nicht einmal tun. Es gibt keine Garantie dafür, dass jedes Modell einmal in einer einzigen kanonischen Ansicht verfügbar gemacht wird. Sie können also nicht automatisch versuchen, die Berechtigungen zu bestimmen, die Sie für nicht mit Hyperlinks verknüpfte Beziehungen verwenden möchten.

(+) Eigentlich wahrscheinlich nicht wirklich möglich, das auf _sinnvolle_ Weise zu tun, aber lass uns für den Moment so tun, als ob.

Vielleicht haben Sie ein "has__permission"? Jedes Berechtigungsobjekt könnte dann erkennen, welche verwandten Objekte sichtbar sind oder nicht.

Wie verwenden die Leute die Filterung? Verwenden sie es nur, um Objekte auszublenden, für die der Benutzer keine Berechtigungen hat? Denn wenn das der Anwendungsfall ist, werden Filter vielleicht nicht benötigt.

Eines der referenzierten Probleme #1646 befasst sich mit der Einschränkung der Auswahlmöglichkeiten, die auf den durchsuchbaren API-Seiten für verwandte Felder angezeigt werden.

Ich liebe die durchsuchbare API und denke, dass sie nicht nur für mich als Backend-Entwickler ein großartiges Werkzeug ist, sondern auch für die Frontend-Entwickler / Benutzer der REST-API. Ich würde das Produkt gerne mit eingeschalteter durchsuchbarer API versenden (d. h. es läuft auch, wenn die Site nicht mehr im DEBUG-Modus ist). Damit ich das tun kann, kann ich nicht zulassen, dass Informationen über die durchsuchbaren API-Seiten durchsickern. (Dies natürlich zusätzlich zu der Voraussetzung, dass diese Seiten im Allgemeinen produktionsbereit und sicher sind).

Das bedeutet, dass über die HTML-Seiten nicht mehr Informationen über die Existenz verwandter Felder lernbar sein sollten, als durch POSTing.

Am Ende habe ich eine Mixin-Klasse für meine Serialisierer erstellt, die die Ansicht des zugehörigen Felds verwendet, um die Filterung bereitzustellen.

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

Ich habe dies mit einem View-Mixin gelöst: #1935, anstatt Serializer und Views zu mischen. Anstatt ein Wörterbuch zu benötigen, habe ich einfach eine Liste von secured_fields in der Ansicht verwendet.

Ich habe dieses einfache Serializer-Mixin verwendet, um Abfragesätze in verwandten Feldern zu filtern:

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)

Auch die Nutzung ist einfach:

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)

Wie ist das? Siehst du da irgendwelche Probleme?

Für mich mag ich es, die Logik über Benutzer und Anfragen aus dem Serializer herauszuhalten und diese in der Ansicht zu belassen.

Das Problem liegt bei verwandten Feldern. Ein Benutzer kann Zugriff auf die Ansicht haben, aber
nicht auf alle zugehörigen Objekte.

Am Mittwoch, den 5. November 2014 um 18:16 Uhr, Alex Rothberg [email protected]
schrieb:

Für mich halte ich die Logik über Benutzer und Anfragen gerne aus dem
Serializer und lassen Sie das in der Ansicht.


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

Bitte lesen Sie dies, es scheint ein verwandtes Thema zu sein. Wie können wir das Filtern von verwandten Feldobjekten von Meta (ModelSerializer) für die OPTIONS-Methode und eine POST- oder PUT-Methode trennen?

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

Wenn wir model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.none()) setzen, können wir das aktuelle Objekt nicht mit der zugehörigen Modellinstanz speichern, da Sealizer PrimaryKeyRelatedField "Abfragesatz für Modellinstanz-Lookups beim Validieren der Feldeingabe verwendet" wird.

Wenn model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.all()) (als Standard für ModelSerializer können wir dies kommentieren), dann alle "verwandten" Objekte (sie sind nicht wirklich verbunden, da OPTIONS Aktionen anzeigen (POST, PUT) Eigenschaften für die Hauptklasse Model, nicht Instanz mit verwandten Objekten), die in den Auswahlmöglichkeiten für das Feld "Modell" (Methode OPTIONS) angezeigt werden.

aktualisieren. @cancan101 +1 . Aber nicht nur "Benutzer". Ich denke, dies ist eine schlechte Idee, Logik und Serialisierer zu mischen, da ich queryset in Serializern sehe: "serializers.PrimaryKeyRelatedField(queryset=".

natürlich gut:

Klasse ModelSerializer:
Klasse Meta:
model=Modell

weil Serializer wissen muss, wie und welche Felder automatisch aus Model erstellt werden.

Trotzdem könnte ich mich irren.

Das scheint zu funktionieren:

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 Das macht es zu einem schreibgeschützten Feld ... aber es funktioniert.

bin auf ein Problem gestoßen, das mit diesem Thread zusammenhängt. Wenn ein Filter-Back-End (in meinem Fall Django-Filter) aktiviert ist, fügt die durchsuchbare API der Benutzeroberfläche eine Schaltfläche Filters , und soweit ich das beurteilen kann, berücksichtigt diese Dropdown-Liste nicht den im Feld festgelegten Abfragesatz. Es scheint mir so, als ob es sollte.

Beispiel:

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)

Das obige Beispiel beschränkt die Projekt-Dropdown-Liste des Formulars zum Hinzufügen/Bearbeiten von Artikeln auf die richtigen Projekte, aber alles wird weiterhin in der Dropdown-Liste Filters angezeigt.

Der Ansatz von nailgun hat für mich ziemlich gut funktioniert, aber nur für One-to-Many-Beziehungen. Jetzt habe ich ein Modell, bei dem meine Beziehung ein ManyToManyField ist. In diesem Fall funktioniert der Mixin-Ansatz nicht. Irgendeine Idee, wie man es für diese löst?

@fibbs ändert den Ansatz von Nailgun, indem Sie Folgendes hinzufügen:

            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) 

Anscheinend hat jemand eine saubere Lösung dazu beigetragen und es ist jetzt möglich, init-Methoden zu hacken: https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e

Ich frage mich, warum dieser Thread nicht aktualisiert / geschlossen wurde?

dass jemand ich bin ;)
Guter Punkt, ich werde dies schließen, da #3605 bereits etwas dazu in der Dokumentation hinzufügt.
Wir werden noch über weitere Verbesserungen in diesem Teil nachdenken, wenn jemand etwas mitbringen kann.

Es ist großartig, dass es jetzt die Methode get_queryset() für RelatedFields gibt. Es wäre großartig, es auch für verschachtelte Serializer zu haben!

Wahrscheinlich. Willst du damit weitermachen? :)

Wow ... erstaunlich, wenn das dokumentiert ist, können Sie mich bitte in die richtige Richtung weisen? Ich habe lange gebraucht, um das herauszufinden!

Hier ist eine Zusammenfassung meines Problems zur Erbauung:

Für dieses Beispiel lauten meine Modellnamen "deployedEnvs" und "Host".
bereitgestellteEnvs enthält einen Fremdschlüssel für das Host-Modell (dh viele bereitgestellteEnvs können auf denselben Host zeigen). Ich brauchte den Serializer, um das fqdn-Feld von HOST und nicht das PK für den Host anzuzeigen (ich habe dafür ein slug-bezogenes Feld verwendet, was ziemlich einfach war). Ich benötigte auch beim Erstellen eines deploytenEnv-Eintrags (POST), um den Host angeben zu können, indem ich den FK-Wert für das relevante HOST by FQDN-Feld nachschaute. Beispiel: Erstellen Sie deployEnv mit dem Feld host (auf übereinstimmenden fqdn des relevanten Hostobjekts eingestellt), indem Sie den PK für das Hostobjekt mit dem match host.fqdn-Feld nachschlagen.

Leider konnte ich die zurückgegebenen Ergebnisse in der Dropdown-Leiste nicht auf die Auswahl für Hostobjekte beschränken, die dem aktuellen Benutzer gehören.

Hier ist mein Fixcode, der an die Verwendung von slugRelatedField angepasst ist

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

Ich bin ungefähr 5 Bücher in Django (ich kann sie alle auflisten, wenn Sie möchten), und keiner der Referenztexte zeigt, wie man mit diesem speziellen Bereich / der Funktionalität des Frameworks arbeitet. Zuerst dachte ich, ich mache etwas falsch, oder? Gibt es einen besseren Weg, das zu tun, was ich versuche zu tun? Zögern Sie nicht, mich mit OOB zu kontaktieren, damit ich die Kommentare zu diesem Problem nicht verfälsche. Vielen Dank an alle, die sich die Zeit genommen haben, meinen Kommentar zu lesen (als Django-Neuling war das wirklich schwer herauszufinden).

@Lcstyle

Jedes Field in DRF (einschließlich Serializers selbst) hat 2 Kernmethoden zum Serialisieren von Daten in und aus (dh zwischen JSON- und Python-Typen):

  1. to_representation - Daten gehen "aus"
  2. to_internal_value - Daten kommen "ein"

Ausgehend von den groben Umrissen der von Ihnen bereitgestellten Modelle finden Sie im Folgenden einen Überblick über die Funktionsweise von RelatedFields, wobei SlugRelatedField eine spezialisierte Version ist:

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)

In Wirklichkeit möchten Sie normalerweise eine Reihe von Schecks in die Methoden get_queryset oder to_internal_value , für Dinge wie die Sicherheit (wenn Sie etwas wie django-guardian oder rules ) und auch um sicherzustellen, dass das eigentliche ORM-Objekt existiert.

Ein vollständigeres Beispiel könnte so aussehen

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

In Bezug auf das, was @cancan101 vor einiger Zeit geschrieben hat:

bin auf ein Problem gestoßen, das mit diesem Thread zusammenhängt. Wenn ein Filter-Back-End (in meinem Fall Django-Filter) aktiviert ist, fügt die durchsuchbare API der Schnittstelle eine Schaltfläche Filter hinzu, und soweit ich das beurteilen kann, berücksichtigt dieses Dropdown-Menü nicht den im Feld festgelegten Abfragesatz. Es scheint mir so, als ob es sollte.

Dies ist, soweit ich das beurteilen kann, immer noch wahr. Dies kann über ein benutzerdefiniertes Filterset Feld für das Fremdschlüsselfeld behoben werden, das Daten verliert , aber get_queryset berücksichtigen sollte der benutzerdefinierten Felddeklaration im Serializer.

In jedem Fall bräuchte es zusätzliche Unterlagen.

Ich dokumentiere unten, wie man dies über einen benutzerdefinierten Filtersatz löst:

Beispiel für ein Workentry-Modell:

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

Basismodell-Ansichtssatz:

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

Benutzerdefiniertes FilterSet (überschreibt filter_fields über filter_class im Ansichtssatz des Basismodells)

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 aufrufbar wie hier dokumentiert: 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

schau dir diesen Code an:

Behebt dies nicht das Datenleckproblem, auf das Sie sich beziehen? Meine Suchfelder sind in der durchsuchbaren API vorhanden, aber die Ergebnisse sind immer noch auf das nach Besitzer gefilterte Abfrageset beschränkt.

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 Ich versuche nicht, die Hosts zu filtern, ich versuche, Instanzen eines verwandten Felds zu filtern (z. B. die Besitzer eines Hosts)

Ich betrachte dieses spezielle Problem, das ich in meinem REST lösen möchte ... normalerweise basieren die Beispiele auf request.user . Ich würde gerne einen etwas komplexeren Fall bearbeiten.

Nehmen wir an, ich habe einen Company , der Employees und der Company hat ein Attribut des Mitarbeiters des Monats:

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

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

Ich möchte, dass die REST-Schnittstelle employee_of_the_month auf Employee mit den gleichen company.id wie Company .

Das ist mir bisher eingefallen,

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)

...ist diese Methode etwas, das abstrahiert werden könnte? Es basiert ein wenig auf @nailguns https://github.com/encode/django-rest-framework/issues/1985#issuecomment -61871134

Ich denke auch, dass ich auch validate() dass employee_of_the_month den oben erstellten Abfragesatz erfüllt, indem ich versuche, einen get() gegen den Abfragesatz mit den employee_of_the_month.id auszuführen

Wenn ich mir #3605 anschaue, sehe ich, dass dies auch mit einem benutzerdefinierten Serializer für das Feld möglich ist – verwenden wir CEO anstelle von 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)           

Dies wurde speziell entwickelt, um keine Objekte zur Auswahl zurückzugeben, es sei denn, wir suchen nach einem bestimmten Unternehmen. Mit anderen Worten, ein neues Unternehmen kann keinen CEO haben, bis es Mitarbeiter hat, was Sie nicht haben können, bis das Unternehmen gegründet ist.

Ich bedaure nur bei diesem Ansatz, dass es so aussieht, als könnte dies DRYer / generisch gemacht werden.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen