Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 33 de 45

Permisos y decoradores

Cerrar las páginas con una línea, el decorador que manda a identificarse a quien ya está identificado, y la diferencia entre permisos por modelo y por fila.

⏱ 16 min de lectura

Ya hay usuarios que entran y salen. Ahora falta que eso sirva para algo, porque hasta este momento todas las vistas del módulo son públicas: cualquiera puede listar, editar y borrar los contactos de cualquiera.

Y aquí es donde se paga la deuda que dejaron la 23 y la 24. Las dos dejaron escrito un patrón a medias: filtrar por propietario en get_queryset y rellenar formulario.instance.propietario en form_valid, los dos esperando a que existiera el usuario. Ya existe.

peticion.user#

Siempre está, haya sesión o no. Sin sesión es un AnonymousUser, que responde a lo mismo diciendo que no:

Salida en pantalla
                  is_authenticated   username   is_staff
AnonymousUser     False              ''         False
basico            True               'basico'   False
admin             True               'admin'    True

Eso está bien pensado: no hace falta comprobar si hay usuario antes de preguntarle nada. peticion.user.is_authenticated funciona siempre.

Y viene de un componente concreto, el AuthenticationMiddleware que está en MIDDLEWARE desde la lección 4. Si alguna vez peticion.user no existe, es que se ha quitado de ahí.

Decoradores#

Tres, para tres casos:

contactos/views.py
from django.contrib.auth.decorators import login_required, permission_required, user_passes_test


@login_required
def privada(peticion):
        return HttpResponse(f'hola {peticion.user.username}')


@permission_required('contactos.add_contacto', raise_exception=True)
def nuevo(peticion):
        ...


@user_passes_test(lambda u: u.is_staff)
def solo_equipo(peticion):
        ...

La cadena del segundo es la de la lección 30: aplicacion.accion_modelo. Los cuatro permisos por modelo los crea Django al migrar.

Medidos con tres visitantes distintos:

Salida en pantalla
                        anónimo                       identificado sin permiso   superusuario
@login_required         302 /cuentas/login/?next=...  200                        200
@permission_required    302 /cuentas/login/?next=...  302 /cuentas/login/?next=  200
... raise_exception     403                           403                        200
@user_passes_test       302 /cuentas/login/?next=...  302 /cuentas/login/?next=  200

El @login_required hace lo esperado: manda a identificarse con el next de la lección anterior ya puesto.

Mixins#

Lo mismo para las vistas de clase de la lección 22:

contactos/views_clase.py
from django.contrib.auth.mixins import (
    LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin)


class ListaPrivada(LoginRequiredMixin, ListView):
        model = Contacto


class ListaConPermiso(PermissionRequiredMixin, ListView):
        model = Contacto
    permission_required = 'contactos.add_contacto'
    raise_exception = True


class ListaSoloEquipo(UserPassesTestMixin, ListView):
        model = Contacto

    def test_func(self):
                return self.request.user.is_staff
Salida en pantalla
            privada   permiso   equipo
anónimo     302       403       302
basico      200       403       403
admin       200       200       200

permission_required admite una lista si hacen falta varios, y test_func es un método, así que puede mirar la petición entera y no solo el usuario, que es lo que lo hace más útil que la función suelta.

Permisos por fila#

Los permisos de Django son por modelo: "puede cambiar contactos", no "puede cambiar sus contactos". Y lo segundo es lo que hace falta casi siempre.

Con el modelo preparado:

contactos/models.py
propietario = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
                                related_name='contactos')

Fíjate en settings.AUTH_USER_MODEL en lugar de importar User. Es lo que permite cambiar de modelo de usuario en la lección siguiente sin reescribir las relaciones, y hacerlo desde el principio sale gratis.

Y entonces, los dos patrones que quedaron pendientes.

Filtrar lo que se ve, que era la deuda de la lección 23:

python
class MisContactos(LoginRequiredMixin, ListView):
        model = Contacto

    def get_queryset(self):
                return super().get_queryset().filter(propietario=self.request.user)

En un DetailView, un UpdateView o un DeleteView, ese mismo get_queryset hace que pedir la ficha ajena dé 404. Y eso es mejor que un 403, porque un 403 confirma que ese objeto existe, que es la misma lógica del mensaje vago de la lección anterior.

Rellenar lo que no se pregunta, que era la deuda de la lección 24:

python
class CrearContacto(LoginRequiredMixin, CreateView):
        model = Contacto
    fields = ['nombre', 'correo', 'telefono']

    def form_valid(self, formulario):
                formulario.instance.propietario = self.request.user
        return super().form_valid(formulario)

El propietario no está en fields a propósito. Si estuviera, saldría como desplegable y cualquiera podría crear contactos a nombre de otro.

En las plantillas#

Dos variables están disponibles en todas partes, sin que ninguna vista las pase:

html
{% if user.is_authenticated %}
  Hola, {{ user.username }}
  {% if perms.contactos.add_contacto %}
    <a href="{% url 'contactos:nuevo' %}">Añadir</a>
  {% endif %}
{% else %}
  <a href="{% url 'login' %}?next={{ request.path|urlencode }}">Entrar</a>
{% endif %}
Salida en pantalla
anónimo  ->  fuera          | perm:False | staff:False
basico   ->  dentro:basico  | perm:False | staff:False
admin    ->  dentro:admin   | perm:True  | staff:True

Vienen de los procesadores de contexto que se mencionaron en la lección 19, y que están en TEMPLATES desde el principio. Por eso {{ user }} funciona en cualquier plantilla.

perms se consulta con el mismo nombre de tres partes, y si te equivocas al escribirlo da False en silencio por lo de la lección 17. Un permiso mal escrito esconde un botón para siempre y nadie se entera. Para las comprobaciones que importan, una prueba.