Django-rest-framework: Documente cómo manejar los permisos y el filtrado de campos relacionados.

Creado en 23 oct. 2014  ·  26Comentarios  ·  Fuente: encode/django-rest-framework

Actualmente, las relaciones no aplican automáticamente el mismo conjunto de permisos y filtros que se aplican a las vistas. Si necesita permisos o filtrar las relaciones, debe tratarlo explícitamente.

Personalmente, no veo ninguna buena forma de lidiar con esto automáticamente, pero obviamente es algo que al menos podríamos hacer para documentar mejor.

En este momento, mi opinión es que deberíamos tratar de presentar un caso de ejemplo simple y documentar cómo lidiarías con eso de manera explícita. Cualquier código automático para lidiar con esto debe dejarse para que lo manejen los autores de paquetes de terceros. Esto permite a otros colaboradores explorar el problema y ver si pueden encontrar buenas soluciones que podrían incluirse en el núcleo.

En el futuro, este problema podría ascender de 'Documentación' a 'Mejora', pero a menos que haya propuestas concretas respaldadas por un paquete de una tercera parte, permanecerá en este estado.

Documentation

Comentario más útil

He usado esta simple combinación de serializador para filtrar conjuntos de consultas en 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)

El uso también es simple:

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)

¿Cómo es? ¿Ves algún problema con eso?

Todos 26 comentarios

Intenté buscar en el código, pero no pude encontrar una manera fácil de hacerlo. Idealmente, debería poder llamar a los permisos '"has_object_permission" para cada objeto relacionado. En este momento, el serializador no tiene acceso al objeto de permiso.

En este momento, el serializador no tiene acceso al objeto de permiso.

Excepto que esto no es tan simple como eso.

_¿Qué_ objeto de permiso? Estas son relaciones con _otros_ objetos, por lo que las clases de permisos y filtros en la vista actual no serán necesariamente las mismas reglas que desearía aplicar a las relaciones de objetos.

Para las relaciones con hipervínculos, en teoría, podría determinar la vista a la que apuntaron (+) y determinar el filtrado / permisos en función de eso, pero ciertamente terminaría como un diseño horrible y estrechamente acoplado. Para las relaciones sin hipervínculos, ni siquiera puede hacer eso. No hay garantía de que cada modelo esté expuesto una vez en una sola vista canónica, por lo que no puede intentar determinar automáticamente los permisos que desea usar para las relaciones sin hipervínculos.

(+) En realidad, probablemente no sea posible hacerlo de ninguna manera _sensible_, pero pretendamos por el momento.

Tal vez tenga un "has__permission "? Cada objeto de permiso podría decir qué objetos relacionados son visibles o no.

¿Cómo usa la gente el filtrado? ¿Lo están usando solo para ocultar objetos para los que el usuario no tiene permisos? Porque si ese es el caso de uso, entonces tal vez no se necesiten filtros.

Uno de los problemas a los que se hace referencia # 1646 trata de limitar las opciones que se muestran en las páginas navegables de la API para los campos relacionados.

Me encanta la API navegable y creo que es una gran herramienta no solo para mí como desarrollador backend, sino también para los desarrolladores / usuarios frontales de la API REST. Me encantaría enviar el producto con la API navegable activada (es decir, se ejecuta incluso cuando el sitio ya no está en modo DEBUG). Para que yo pueda hacer eso, no puedo permitir que se produzca una fuga de información a través de las páginas de API navegables. (Esto, por supuesto, además del requisito de que esas páginas generalmente estén listas para producción y sean seguras).

Lo que eso significa es que no debería obtenerse más información sobre la existencia de campos relacionados a través de las páginas HTML de la que se obtendría mediante POST.

Terminé creando una clase mixin para mis serializadores que usa la Vista del campo relacionado para proporcionar el filtrado.

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

Resolví esto usando un mixin de View: # 1935 en lugar de mezclar Serializadores y Vistas. En lugar de necesitar un diccionario, utilicé una lista de secured_fields en la Vista.

He usado esta simple combinación de serializador para filtrar conjuntos de consultas en 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)

El uso también es simple:

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)

¿Cómo es? ¿Ves algún problema con eso?

Para mí, me gusta mantener la lógica sobre los usuarios y las solicitudes fuera del serializador y dejar eso en la Vista.

El problema está en los campos relacionados. Un usuario puede tener acceso a la vista pero
no a todos los objetos relacionados.

El miércoles 5 de noviembre de 2014 a las 6:16 p. M., Alex Rothberg [email protected]
escribió:

Para mí, me gusta mantener la lógica sobre los usuarios y las solicitudes fuera del
Serializador y déjelo en la Vista.

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

Por favor, lea esto, parece que es un tema relacionado. ¿Cómo podemos separar el filtrado de objetos de campos relacionados de Meta (ModelSerializer) para el método OPTIONS y un método POST o PUT?

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

Si configuramos model = serializers.PrimaryKeyRelatedField (queryset = Model.objects.none ()), entonces no podemos guardar el objeto actual con la instancia de modelo relacionada, porque sealizer PrimaryKeyRelatedField "queryset usado para búsquedas de instancias de modelos al validar la entrada de campo".

Si model = serializers.PrimaryKeyRelatedField (queryset = Model.objects.all ()) (por defecto para ModelSerializer podemos comentar esto), entonces todos los objetos "relacionados" (no están relacionados realmente, porque las OPCIONES muestran acciones (POST, PUT) propiedades para la clase de modelo principal, no la instancia con objetos relacionados) que se muestran en las opciones del campo "modelo" (método OPCIONES).

actualizar. @ cancan101 +1. Pero no solo "usuario". Creo que esta es una mala idea mezclar lógica y serializadores, como veo queryset en serializadores: "serializers.PrimaryKeyRelatedField (queryset =".

por supuesto, es bueno:

clase ModelSerializer:
clase Meta:
modelo = Modelo

porque Serializer debe saber cómo y qué campos se crean automáticamente desde el modelo.

Sin embargo, podría estar equivocado.

Esto 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 Eso lo convierte en un campo de solo lectura ... pero funciona.

Me encontré con un problema que parece estar relacionado con este hilo. Cuando un backend de filtrado (Django Filter en mi caso) está habilitado, la API navegable agrega un botón Filters a la interfaz y, por lo que puedo decir, el menú desplegable no respeta el conjunto de consultas establecido en el campo. Me parece que debería hacerlo.

Ejemplo:

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)

El ejemplo anterior limita el menú desplegable del proyecto del formulario Agregar / editar elemento a los proyectos correctos, pero todo todavía se muestra en el menú desplegable Filters .

El enfoque de la pistola de clavos me ha funcionado bastante bien, pero solo para las relaciones de uno a varios. Ahora, tengo un modelo en el que mi relación es ManyToManyField. En este caso, el enfoque Mixin no funciona. ¿Alguna idea de cómo solucionarlo por estos?

@fibbs cambia el enfoque de la pistola de clavos agregando lo siguiente:

            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, alguien contribuyó con una solución limpia a esto y ahora es posible sin piratear los métodos de inicio: https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e

Me pregunto por qué este hilo no se ha actualizado / cerrado.

ese alguien siendo yo;)
Buen punto, voy a cerrar esto porque # 3605 ya agrega algo sobre eso en la documentación.
Seguiremos considerando mejorar esa parte si alguien puede traer algo.

Es genial que el método get_queryset() ahora exista para RelatedFields. ¡Sin embargo, sería genial tenerlo también para serializadores anidados!

Probablemente. ¿Quieres seguir adelante con eso? :)

Wow ... increíble, si esto está documentado, ¿podría indicarme la dirección correcta? ¡Me tomó mucho tiempo darme cuenta de esto!

Aquí hay un resumen de mi problema para la edificación:

Para este ejemplo, los nombres de mis modelos son "deployedEnvs" y "Host".
deployedEnvs contiene una clave externa para el modelo de host (es decir, muchos deployedEnvs pueden apuntar al mismo host). Necesitaba que el serializador mostrara el campo fqdn de HOST en lugar del PK para el host (usé el campo relacionado con slug para eso que era bastante simple). También necesitaba al crear una entrada deployedEnv (POST), poder especificar el host buscando el valor FK para el campo HOST relevante por FQDN. Ejemplo: cree deployedEnv con el campo host (configurado para que coincida con el fqdn del objeto host relevante) buscando el PK del objeto Host con el campo coincidente host.fqdn.

Desafortunadamente, no pude limitar los resultados devueltos en la barra desplegable a solo opciones para objetos de host que son propiedad del usuario actual.

Aquí está mi código de corrección 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')

Estoy cerca de 5 libros en Django (puedo enumerarlos todos si lo desea), y ninguno de los textos de referencia muestra cómo trabajar con esta área / funcionalidad particular del Framework. Al principio pensé que estaba haciendo algo mal, ¿verdad? ¿Existe una mejor manera de hacer lo que estoy tratando de hacer? No dude en ponerse en contacto conmigo OOB para que no termine alterando los comentarios de este problema. Gracias a todos por tomarse el tiempo de leer mi comentario (como novato en django, esto fue realmente difícil de entender).

@Lcstyle

Cada Field en DRF (incluidos Serializers ellos mismos) tiene 2 métodos principales para serializar datos de entrada y salida (es decir, entre los tipos JSON y Python):

  1. to_representation - datos que "salen"
  2. to_internal_value - datos que ingresan

Saliendo del esquema general de los modelos que ha proporcionado, a continuación se muestra un esquema de cómo funcionan RelatedFields, siendo SlugRelatedField una versión 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)

En realidad, normalmente desea poner un montón de cheques en los métodos get_queryset o to_internal_value , para cosas como seguridad (si usa algo como django-guardian o rules ) y también para asegurarse de que exista el objeto ORM real.

Un ejemplo más completo podría verse así

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 cuanto a lo que @ cancan101 escribió hace algún tiempo:

Me encontré con un problema que parece estar relacionado con este hilo. Cuando un backend de filtrado (Django Filter en mi caso) está habilitado, la API navegable agrega un botón de Filtros a la interfaz y, por lo que puedo decir, el menú desplegable no respeta el conjunto de consultas establecido en el campo. Me parece que debería hacerlo.

Esto sigue siendo cierto por lo que puedo ver. Esto se puede remediar a través de un campo Filterset para el campo de clave extranjera que está filtrando datos, pero @tomchristie todavía creo que esto debería resolverse 'automáticamente' y la elección del modelo de filtro debería respetar el método get_queryset de la declaración de campo personalizado en el serializador.

En cualquier caso necesitaría documentación adicional.

A continuación, estoy documentando cómo resolver esto a través de un conjunto de filtros personalizados:

Modelo de entrada de trabajo de muestra:

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

Conjunto de vistas del modelo 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 (anula filter_fields través de filter_class en el conjunto de vistas del modelo 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',),
        }

queryset invocable como se documenta aquí: 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

echa un vistazo a este código:

¿No soluciona esto el problema de fuga de datos al que se refiere? Mis campos de búsqueda están presentes en la API navegable, pero los resultados aún están limitados al conjunto de consultas filtrado por propietario.

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 No estoy tratando de filtrar los hosts, estoy tratando de filtrar instancias de un campo relacionado (por ejemplo, los propietarios de un host)

Estoy viendo este problema en particular que me gustaría resolver en mi REST ... generalmente los ejemplos se basan en request.user . Me gustaría manejar un caso un poco más complejo.

Digamos que tengo un Company que tiene Employees y el Company tiene un atributo de empleado del mes:

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

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

Me gustaría que la interfaz REST limitara employee_of_the_month por Employee con el mismo company.id que Company .

Esto es lo que se me ocurrió hasta ahora

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)

... ¿es este método algo que podría abstraerse? Se basa un poco en @nailgun 's https://github.com/encode/django-rest-framework/issues/1985#issuecomment -61871134

También estoy pensando que también podría validate() que employee_of_the_month satisfaga el conjunto de consultas creado anteriormente al intentar hacer un get() contra el conjunto de consultas con employee_of_the_month.id

Mirando el n. ° 3605, veo que esto también se puede hacer con un serializador personalizado para el campo; usemos CEO en lugar de Empleado del mes:

 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)           

Esto está diseñado específicamente para no devolver ningún objeto para su selección a menos que estemos mirando a una empresa específica. En otras palabras, una nueva empresa no puede tener un director ejecutivo hasta que tenga empleados, lo que no se puede tener hasta que se crea la empresa.

Lo único que lamento con este enfoque es que parece que podría hacerse más SECO / genérico.

¿Fue útil esta página
0 / 5 - 0 calificaciones