Django-rest-framework: Dokumentasikan cara menangani izin dan pemfilteran untuk bidang terkait.

Dibuat pada 23 Okt 2014  ·  26Komentar  ·  Sumber: encode/django-rest-framework

Saat ini hubungan tidak secara otomatis menerapkan kumpulan izin dan pemfilteran yang sama yang diterapkan ke tampilan. Jika Anda memerlukan izin atau filter pada hubungan, Anda harus menanganinya secara eksplisit.

Secara pribadi saya tidak melihat cara yang baik untuk menangani hal ini secara otomatis, tetapi ini jelas sesuatu yang setidaknya dapat kita lakukan dengan mendokumentasikan dengan lebih baik.

Saat ini pendapat saya adalah bahwa kita harus mencoba membuat contoh kasus sederhana dan mendokumentasikan bagaimana Anda akan menanganinya secara eksplisit. Kode otomatis apa pun untuk menangani ini harus diserahkan kepada pembuat paket pihak ketiga untuk ditangani. Ini memungkinkan kontributor lain untuk mengeksplorasi masalah dan melihat apakah mereka dapat menemukan solusi bagus yang berpotensi dimasukkan ke dalam inti.

Di masa depan, masalah ini mungkin dipromosikan dari 'Dokumentasi' menjadi 'Peningkatan', tetapi kecuali ada proposal konkret yang didukung oleh paket bagian ketiga, maka itu akan tetap dalam status ini.

Documentation

Komentar yang paling membantu

Saya telah menggunakan mixin Serializer sederhana ini untuk memfilter kumpulan kueri di bidang terkait:

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)

Penggunaannya juga sederhana:

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)

Bagaimana itu? Apakah Anda melihat ada masalah dengannya?

Semua 26 komentar

Saya mencoba menggali kode, tetapi saya tidak dapat menemukan cara mudah untuk melakukan ini. Idealnya, Anda harus dapat memanggil izin "has_object_permission" untuk setiap objek terkait. Saat ini, pembuat serial tidak memiliki akses ke objek izin.

Saat ini, pembuat serial tidak memiliki akses ke objek izin.

Kecuali bahwa ini tidak sesederhana itu.

_Yang_ objek izin? Ini adalah hubungan dengan objek _other_, jadi izin dan kelas filter pada tampilan saat ini tidak selalu merupakan aturan yang sama yang ingin Anda terapkan pada hubungan objek.

Untuk hubungan hyperlink, Anda secara teori dapat menentukan pandangan yang mereka tunjuk (+) dan menentukan pemfilteran/izin berdasarkan itu, tetapi itu pasti akan berakhir sebagai desain berpasangan yang mengerikan. Untuk hubungan non-hyperlink Anda bahkan tidak bisa melakukan itu. Tidak ada jaminan bahwa setiap model diekspos satu kali pada tampilan kanonik tunggal, jadi Anda tidak dapat mencoba secara otomatis menentukan izin yang ingin Anda gunakan untuk hubungan non-hyperlink.

(+) Sebenarnya mungkin tidak mungkin melakukan itu dengan cara _masuk akal_, tapi mari kita berpura-pura untuk saat ini.

Mungkin memiliki "memiliki__permission"? Setiap objek izin kemudian dapat membedakan objek terkait mana yang dapat dilihat atau tidak.

Bagaimana orang menggunakan penyaringan? Apakah mereka menggunakannya hanya untuk menyembunyikan objek yang tidak memiliki izin pengguna? Karena jika itu kasus penggunaan, maka mungkin filter tidak diperlukan.

Salah satu masalah yang dirujuk #1646 berkaitan dengan membatasi pilihan yang ditampilkan pada halaman API yang dapat dijelajahi untuk bidang terkait.

Saya menyukai API yang dapat dijelajahi dan menganggapnya sebagai alat yang hebat tidak hanya untuk saya sebagai pengembang backend tetapi juga untuk pengembang front-end/pengguna REST API. Saya ingin mengirimkan produk dengan API yang dapat dijelajahi AKTIF (yaitu.. itu berjalan bahkan ketika situs tidak sendirian dalam mode DEBUG). Bagi saya untuk dapat melakukan itu, saya tidak dapat mengalami kebocoran informasi melalui halaman API yang dapat dijelajahi. (Ini tentu saja selain persyaratan bahwa halaman tersebut umumnya siap produksi dan aman).

Artinya, tidak ada lagi informasi tentang keberadaan bidang terkait yang dapat dipelajari melalui halaman HTML daripada yang dapat dipelajari melalui POSTing.

Saya akhirnya membuat kelas mixin untuk serializer saya yang menggunakan Tampilan bidang terkait untuk menyediakan pemfilteran.

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

Saya memecahkan ini menggunakan View mixin: #1935 daripada mencampur Serializers dan Views. Daripada membutuhkan kamus, saya hanya menggunakan daftar secured_fields pada View.

Saya telah menggunakan mixin Serializer sederhana ini untuk memfilter kumpulan kueri di bidang terkait:

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)

Penggunaannya juga sederhana:

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)

Bagaimana itu? Apakah Anda melihat ada masalah dengannya?

Bagi saya, saya suka menyimpan logika tentang pengguna dan permintaan dari Serializer dan membiarkannya di View.

Masalahnya adalah dengan bidang terkait. Seorang pengguna dapat memiliki akses ke tampilan tetapi
tidak untuk semua objek terkait.

Pada Wed, Nov 5, 2014 at 6:16, Alex Rothberg [email protected]
menulis:

Bagi saya, saya ingin menjaga logika tentang pengguna dan permintaan keluar dari
Serializer dan biarkan itu di View.


Balas email ini secara langsung atau lihat di GitHub
https://github.com/tomchristie/Django-rest-framework/issues/1985#issuecomment -61873766
.

Tolong, baca ini, sepertinya ini topik terkait. Bagaimana kita bisa memisahkan pemfilteran objek bidang terkait Meta (ModelSerializer) untuk metode OPSI dan metode POST atau PUT?

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

Jika kita menetapkan model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.none()), maka kita tidak dapat menyimpan objek saat ini dengan instance model terkait, karena sealizer PrimaryKeyRelatedField "queryset digunakan untuk pencarian instance model saat memvalidasi input bidang".

Jika model = serializers.PrimaryKeyRelatedField(queryset=Model.objects.all()) (sebagai default untuk ModelSerializer kami dapat mengomentari ini), maka semua objek "terkait" (mereka tidak benar-benar terkait, karena OPTIONS menampilkan tindakan (POST, PUT) properti untuk kelas Model utama, bukan instance dengan objek terkait) ditampilkan dalam pilihan untuk bidang "model" (metode OPSI).

memperbarui. @cancan101 +1 . Tapi tidak hanya "pengguna". Saya pikir, ini adalah ide yang buruk untuk mencampur logika dan serializer, karena saya melihat queryset di serializers: "serializers.PrimaryKeyRelatedField(queryset=".

tentu saja, itu bagus:

kelas ModelSerializer:
kelas Meta:
model=model

karena Serializer harus mengetahui bagaimana dan bidang mana yang secara otomatis dibuat dari Model.

Namun demikian, saya bisa saja salah.

Ini tampaknya berhasil:

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 Itu membuatnya menjadi bidang hanya-baca...

Mengalami masalah yang tampaknya terkait dengan utas ini. Ketika backend pemfilteran (Django Filter dalam kasus saya) diaktifkan, API yang dapat dijelajahi menambahkan tombol Filters ke antarmuka dan sejauh yang saya tahu bahwa dropdown tidak menghormati set kueri di bidang. Sepertinya saya seperti itu seharusnya.

Contoh:

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)

Contoh di atas membatasi dropdown proyek Item add/edit form ke proyek yang benar tetapi semuanya masih ditampilkan di dropdown Filters .

pendekatan nailgun telah bekerja cukup baik untuk saya, tetapi hanya untuk hubungan Satu-ke-Banyak. Sekarang, saya memiliki satu model di mana hubungan saya adalah ManyToManyField. Dalam hal ini, pendekatan Mixin tidak berfungsi. Adakah ide bagaimana menyelesaikannya untuk ini?

@fibbs mengubah pendekatan nailgun dengan menambahkan yang berikut:

            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) 

Rupanya seseorang menyumbangkan solusi bersih untuk ini dan sekarang mungkin tanpa meretas metode init: https://medium.com/Django-rest-framework/limit-related-data-choices-with-Django-rest-framework-c54e96f5815e

Saya bertanya-tanya mengapa utas ini belum diperbarui / ditutup?

seseorang itu adalah aku ;)
Poin bagus, saya akan menutup ini karena #3605 sudah menambahkan sesuatu tentang itu di dokumentasi.
Kami masih akan mempertimbangkan perbaikan lebih lanjut untuk bagian itu jika seseorang bisa datang dengan sesuatu.

Sangat bagus bahwa metode get_queryset() sekarang ada untuk RelatedFields. Akan luar biasa memilikinya untuk Serializer bersarang juga!

Mungkin. Ingin melanjutkannya? :)

Wow...luar biasa, jika ini didokumentasikan, bisakah Anda mengarahkan saya ke arah yang benar? Butuh waktu lama bagiku untuk memikirkan yang satu ini!

Berikut ringkasan masalah saya untuk perbaikan:

Untuk contoh ini, nama model saya adalah "deployedEnvs" dan "Host".
deployEnvs berisi kunci Asing ke model Host (yaitu banyak deployEnvs dapat menunjuk ke Host yang sama). Saya membutuhkan serializer untuk menampilkan bidang fqdn HOST daripada PK untuk Host (saya menggunakan bidang terkait siput untuk itu yang cukup sederhana). Saya juga membutuhkan saat membuat entri deployEnv (POST), untuk dapat menentukan Host dengan mencari nilai FK untuk bidang HOST oleh FQDN yang relevan. Contoh: buat deployEnv dengan host bidang (setel ke pencocokan fqdn dari objek host yang relevan) dengan mencari PK untuk objek Host dengan bidang host.fqdn yang cocok.

Sayangnya saya tidak dapat membatasi hasil yang dikembalikan di bilah tarik-turun hanya untuk pilihan objek Host yang dimiliki oleh pengguna saat ini.

Inilah kode perbaikan saya yang disesuaikan untuk menggunakan 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')

Saya sekitar 5 buku ke dalam Django (saya dapat membuat daftar semuanya jika Anda mau), dan tidak ada teks referensi yang menunjukkan bagaimana bekerja dengan area/fungsi tertentu dari Kerangka ini. Awalnya saya pikir saya melakukan sesuatu yang salah, bukan? Apakah ada cara yang lebih baik untuk melakukan apa yang saya coba lakukan? Jangan ragu untuk menghubungi saya OOB jadi saya tidak akan memalsukan komentar untuk masalah ini. Terima kasih kepada semua yang telah meluangkan waktu untuk membaca komentar saya (sebagai pemula Django, ini sangat sulit untuk diketahui).

@Lcstyle

Setiap Field dalam DRF (termasuk Serializers sendiri) memiliki 2 metode inti untuk membuat serialisasi data masuk dan keluar (yaitu antara tipe JSON dan Python):

  1. to_representation - data akan "keluar"
  2. to_internal_value - data masuk "masuk"

Keluar dari garis besar model yang Anda berikan, di bawah ini adalah garis besar tentang cara kerja RelatedFields, dengan SlugRelatedField menjadi versi khusus:

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)

Pada kenyataannya, Anda biasanya ingin memasukkan banyak cek ke dalam metode get_queryset atau to_internal_value , untuk hal-hal seperti keamanan (jika menggunakan sesuatu seperti django-guardian atau rules ) dan juga untuk memastikan objek ORM yang sebenarnya ada.

Contoh yang lebih lengkap mungkin terlihat seperti ini

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

Sehubungan dengan apa yang ditulis @cancan101 beberapa waktu lalu:

Mengalami masalah yang tampaknya terkait dengan utas ini. Ketika backend pemfilteran (Django Filter dalam kasus saya) diaktifkan, API yang dapat dijelajahi menambahkan tombol Filter ke antarmuka dan sejauh yang saya tahu bahwa dropdown tidak menghormati set kueri di bidang. Sepertinya saya seperti itu seharusnya.

Ini masih benar sejauh yang saya bisa lihat. Ini dapat diperbaiki melalui bidang Filterset untuk bidang kunci asing yang membocorkan data, tetapi @tomchristie Saya masih berpikir ini harus diselesaikan 'secara otomatis' dan pilihan model filter harus menghormati metode get_queryset dari deklarasi bidang khusus di serializer.

Bagaimanapun itu akan membutuhkan dokumentasi tambahan.

Saya mendokumentasikan di bawah ini bagaimana menyelesaikan ini melalui kumpulan filter khusus:

Contoh Model Pekerjaan:

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

Kumpulan tampilan model dasar:

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

FilterSet Kustom (mengganti filter_fields melalui filter_class di set tampilan model dasar)

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 dapat dipanggil seperti yang didokumentasikan di sini: 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

lihat kode ini:

Apakah ini tidak memperbaiki masalah kebocoran data yang Anda maksud? Bidang pencarian saya ada di api yang dapat dijelajahi, tetapi hasilnya masih terbatas pada kumpulan kueri yang difilter oleh pemilik.

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 Saya tidak mencoba memfilter host, saya mencoba memfilter instance bidang terkait (misalnya pemilik Host)

Saya melihat masalah khusus ini yang ingin saya selesaikan di REST saya ... biasanya contohnya didasarkan pada request.user . Saya ingin menangani kasus yang sedikit lebih kompleks.

Katakanlah saya memiliki Company yang memiliki Employees dan Company memiliki atribut employee of the month:

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

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

Saya ingin antarmuka REST membatasi employee_of_the_month oleh Employee dengan company.id sama dengan Company .

Inilah yang saya dapatkan sejauh ini,

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)

...apakah metode ini sesuatu yang bisa diabstraksikan? Ini sedikit didasarkan pada @nailgun https://github.com/encode/Django-rest-framework/issues/1985#issuecomment -61871134

Saya juga berpikir bahwa saya juga bisa validate() bahwa employee_of_the_month memenuhi queryset yang dibangun di atas dengan mencoba melakukan get() terhadap queryset dengan employee_of_the_month.id

Melihat #3605, saya melihat ini juga dapat dilakukan dengan serializer khusus untuk bidang tersebut -- mari gunakan CEO alih-alih 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)           

Ini dirancang khusus untuk tidak mengembalikan objek untuk dipilih kecuali jika kita melihat perusahaan tertentu. Dengan kata lain, sebuah perusahaan baru tidak dapat memiliki CEO sampai memiliki karyawan, yang tidak dapat Anda miliki sampai Perusahaan dibuat.

Satu-satunya penyesalan saya dengan pendekatan ini adalah sepertinya ini bisa dibuat KERING/generik.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat