Django-rest-framework: Documente como lidar com permissões e filtragem para campos relacionados.

Criado em 23 out. 2014  ·  26Comentários  ·  Fonte: encode/django-rest-framework

Atualmente, os relacionamentos não aplicam automaticamente o mesmo conjunto de permissões e filtros aplicados às visualizações. Se você precisa de permissões ou filtro em relacionamentos, você precisa lidar com isso explicitamente.

Pessoalmente, não vejo nenhuma boa maneira de lidar com isso automaticamente, mas é obviamente algo que poderíamos fazer pelo menos documentando melhor.

No momento, minha opinião é que devemos tentar criar um caso de exemplo simples e documentar como você lidaria com isso explicitamente. Qualquer código automático para lidar com isso deve ser deixado para os autores de pacotes terceirizados manipularem. Isso permite que outros contribuidores explorem o problema e vejam se eles podem encontrar boas soluções que possam ser potencialmente incluídas no núcleo.

No futuro, este problema pode ser promovido de 'Documentação' para 'Aprimoramento', mas a menos que haja propostas concretas que sejam apoiadas por um pacote de terceiros, ele permanecerá neste estado.

Documentation

Comentários muito úteis

Usei este mixin Serializer simples para filtrar querysets em campos relacionados:

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)

O uso também é simples:

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)

Como é? Você vê algum problema com isso?

Todos 26 comentários

Tentei vasculhar o código, mas não consegui encontrar uma maneira fácil de fazer isso. Idealmente, você deve ser capaz de chamar as permissões '"has_object_permission" para cada objeto relacionado. No momento, o serializador não tem acesso ao objeto de permissão.

No momento, o serializador não tem acesso ao objeto de permissão.

Exceto que isso não é tão simples assim.

_Qual_ objeto de permissão? Esses são relacionamentos com _outros_ objetos, então a permissão e as classes de filtro na visão atual não serão necessariamente as mesmas regras que você deseja aplicar aos relacionamentos de objeto.

Para relacionamentos com hiperlinks, você poderia, em teoria, determinar a visão para a qual eles apontavam (+) e determinar a filtragem / permissões com base nisso, mas certamente acabaria como um design horrível e fortemente acoplado. Para relacionamentos sem hiperlink, você nem pode fazer isso. Não há garantia de que cada modelo seja exposto uma vez em uma única visualização canônica, portanto, você não pode tentar determinar automaticamente as permissões que deseja usar para relacionamentos sem hiperlink.

(+) Na verdade, provavelmente não é realmente possível fazer isso de qualquer maneira _sensível_, mas vamos fingir por enquanto.

Talvez tenha um "has__permission "? Cada objeto de permissão seria então capaz de dizer quais objetos relacionados são visíveis ou não.

Como as pessoas usam a filtragem? Eles estão usando apenas para ocultar objetos que o usuário não tem permissão? Porque se esse for o caso de uso, talvez os filtros não sejam necessários.

Um dos problemas mencionados # 1646 trata da limitação das opções mostradas nas páginas da API navegável para campos relacionados.

Eu amo a API navegável e acho que é uma ótima ferramenta não apenas para mim como desenvolvedor de back-end, mas também para os desenvolvedores / usuários de front end da API REST. Eu adoraria enviar o produto com a API navegável LIGADA (ou seja, funciona mesmo quando o site não está mais no modo DEBUG). Para que eu possa fazer isso, não posso permitir que o vazamento de informações aconteça por meio das páginas da API navegáveis. (Isso, é claro, além do requisito de que essas páginas estejam geralmente prontas para a produção e seguras).

O que isso significa é que nenhuma outra informação sobre a existência de campos relacionados pode ser aprendida por meio das páginas HTML do que por meio de POST.

Acabei criando uma classe de mixin para meus serializadores que usa o campo relacionado View para fornecer a filtragem.

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

Resolvi isso usando um mixin View: # 1935 em vez de misturar serializadores e views. Em vez de precisar de um dicionário, usei apenas uma lista de secured_fields na Visualização.

Usei este mixin Serializer simples para filtrar querysets em campos relacionados:

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)

O uso também é simples:

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)

Como é? Você vê algum problema com isso?

Para mim, gosto de manter a lógica sobre usuários e solicitações fora do Serializer e deixar isso na Visualização.

O problema é com campos relacionados. Um usuário pode ter acesso à visualização, mas
não para todos os objetos relacionados.

Na quarta-feira, 5 de novembro de 2014 às 18:16, Alex Rothberg [email protected]
escreveu:

Para mim, gosto de manter a lógica sobre usuários e solicitações fora do
Serializer e deixe isso no View.

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

Por favor, leia isto, parece que é um tópico relacionado. Como podemos separar a filtragem de objetos de campos relacionados de Meta (ModelSerializer) para o método OPTIONS e um método POST ou PUT?

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

Se definirmos model = serializers.PrimaryKeyRelatedField (queryset = Model.objects.none ()), então não podemos salvar o objeto atual com a instância de modelo relacionada, porque o selador PrimaryKeyRelatedField "queryset usado para pesquisas de instância de modelo ao validar a entrada do campo".

Se model = serializers.PrimaryKeyRelatedField (queryset = Model.objects.all ()) (como padrão para ModelSerializer, podemos comentar isso), então todos os objetos "relacionados" (eles não estão realmente relacionados, porque OPTIONS exibem ações (POST, PUT) propriedades da classe principal do modelo, não instância com objetos relacionados) exibidas nas opções para o campo "modelo" (método OPÇÕES).

atualizar. @ cancan101 +1. Mas não apenas "usuário". Acho que é uma má ideia misturar lógica e serializadores, como vejo queryset em serializadores: "serializers.PrimaryKeyRelatedField (queryset =".

claro, é bom:

classe ModelSerializer:
classe Meta:
modelo = modelo

porque o Serializer deve saber como e quais campos criar automaticamente a partir do Model.

No entanto, posso estar errado.

Isso parece funcionar:

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 Isso o torna um campo somente leitura ... mas funciona.

Encontrou um problema que parece relacionado a este tópico. Quando um backend de filtragem (Django Filter no meu caso) está habilitado, a API navegável adiciona um botão Filters à interface e, pelo que eu posso dizer, aquele dropdown não respeita o queryset definido no campo. Parece-me que deveria.

Exemplo:

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)

O exemplo acima limita a lista suspensa de projeto do formulário de adição / edição de item aos projetos corretos, mas tudo ainda é mostrado na lista suspensa Filters .

A abordagem da nailgun funcionou muito bem para mim, mas apenas para relações Um-para-Muitos. Agora, tenho um modelo em que meu relacionamento é ManyToManyField. Nesse caso, a abordagem Mixin não funciona. Alguma ideia de como resolver isso por isso?

@fibbs muda a abordagem da Nailgun adicionando o seguinte:

            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) 

Aparentemente, alguém contribuiu com uma solução limpa para isso e agora é possível sem hackear métodos init: https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e

Eu me pergunto por que este tópico não foi atualizado / fechado?

aquele alguém sendo eu;)
Bom ponto, vou encerrar isso porque # 3605 já adiciona algo sobre isso na documentação.
Ainda estaremos considerando melhorias adicionais para essa parte, se alguém puder vir com algo.

É ótimo que o método get_queryset() agora exista para RelatedFields. Seria incrível tê-lo para serializadores aninhados também!

Provavelmente. Quer continuar com isso? :)

Uau ... incrível, se isso estiver documentado, você pode me indicar a direção certa? Levei muito tempo para descobrir isso!

Aqui está um resumo do meu problema para edificação:

Para este exemplo, meus nomes de modelo são "deployedEnvs" e "Host".
deployedEnvs contém uma chave estrangeira para o modelo Host (ou seja, muitos deployedEnvs podem apontar para o mesmo host). Eu precisava que o serializador exibisse o campo fqdn do HOST em vez do PK do host (usei o campo relacionado ao slug para isso, que era bem simples). Eu também precisava, ao criar uma entrada deployedEnv (POST), ser capaz de especificar o host procurando o valor FK para o HOST relevante pelo campo FQDN. Exemplo: crie deployedEnv com o campo host (definido para corresponder fqdn do objeto host relevante) procurando o PK para o objeto Host com o campo host.fqdn correspondente.

Infelizmente, não pude limitar os resultados retornados na barra suspensa apenas para opções de objetos de host que pertencem ao usuário atual.

Aqui está meu código de correção adaptado para usar 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')

Tenho cerca de 5 livros sobre Django (posso listá-los todos se você quiser), e nenhum dos textos de referência mostra como trabalhar com esta área / funcionalidade específica do Framework. A princípio pensei que estava fazendo algo errado, certo? Existe uma maneira melhor de fazer o que estou tentando fazer? Sinta-se à vontade para entrar em contato comigo, OOB, para que eu não acabe confundindo os comentários sobre esse problema. Obrigado a todos por lerem meu comentário (como um django novato, isso foi realmente difícil de descobrir).

@Lcstyle

Cada Field em DRF (incluindo Serializers próprios) tem 2 métodos principais para serializar dados de entrada e saída (ou seja, entre os tipos JSON e Python):

  1. to_representation - dados indo "para fora"
  2. to_internal_value - dados entrando "em"

Saindo do esboço dos modelos que você forneceu, abaixo está um esboço de como o RelatedFields funciona, com SlugRelatedField sendo uma versão especializada:

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)

Na realidade, você normalmente deseja colocar um monte de cheques nos métodos get_queryset ou to_internal_value , para coisas como segurança (se estiver usando algo como django-guardian ou rules ) e também para certificar-se de que o objeto ORM real existe.

Um exemplo mais completo pode ser parecido com este

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

Em relação ao que @ cancan101 escreveu há algum tempo:

Encontrou um problema que parece relacionado a este tópico. Quando um backend de filtragem (Django Filter no meu caso) está habilitado, a API navegável adiciona um botão Filters à interface e, pelo que eu posso dizer, o dropdown não respeita o queryset definido no campo. Parece-me que deveria.

Isso ainda é verdade, tanto quanto posso ver. Isso pode ser remediado por meio de um campo Filterset para o campo de chave estrangeira que está vazando dados, mas @tomchristie ainda acho que isso deve ser resolvido 'automaticamente' e a escolha do modelo de filtro deve respeitar o método get_queryset da declaração do campo personalizado no serializador.

Em qualquer caso, seria necessária documentação adicional.

Estou documentando a seguir como resolver isso por meio de um conjunto de filtros personalizado:

Amostra de modelo de workentry:

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

Conjunto de visualização do modelo básico:

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 personalizado (substitui filter_fields por meio de filter_class no conjunto de visualização do modelo básico)

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 chamável conforme documentado aqui: 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

dê uma olhada neste código:

Isso não corrige o problema de vazamento de dados ao qual você está se referindo? Meus campos de pesquisa estão presentes na API navegável, mas os resultados ainda estão limitados ao queryset filtrado pelo proprietário.

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 Não estou tentando filtrar os hosts, estou tentando filtrar instâncias de um campo relacionado (por exemplo, os proprietários de um host)

Estou olhando para este problema específico que gostaria de resolver em meu REST ... normalmente os exemplos são baseados em request.user . Eu gostaria de lidar com um caso um pouco mais complexo.

Digamos que eu tenha um Company que tenha Employees e que Company tenha um atributo de funcionário do mês:

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

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

Eu gostaria que a interface REST limitasse employee_of_the_month por Employee com o mesmo company.id que Company .

Isso é o que eu descobri até agora,

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)

... este método é algo que pode ser abstraído? É baseado um pouco sobre @nailgun 's https://github.com/encode/django-rest-framework/issues/1985#issuecomment -61871134

Eu também estou pensando que eu também poderia validate() que employee_of_the_month satisfaça o queryset construído acima tentando fazer um get() contra o queryset com employee_of_the_month.id

Olhando para # 3605, vejo que isso também pode ser feito com um serializador personalizado para o campo - vamos usar o CEO em vez de Funcionário do mês:

 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)           

Isso foi projetado especificamente para retornar nenhum objeto para seleção, a menos que estejamos procurando uma empresa específica. Em outras palavras, uma nova empresa não poderia ter um CEO até que tivesse funcionários, o que você não pode ter até que a empresa seja criada.

Meu único arrependimento com esta abordagem é que parece que isso poderia ser feito mais DRYer / genérico.

Esta página foi útil?
0 / 5 - 0 avaliações