Introducción

Django es un framework para el desarrollo de aplicaciones web usando el lenguaje de programación Python. En su página web leemos:

Django makes it easier to build better Web apps more quickly and with less code.


Django facilita construir mejores Web apps más rápido y con menos código.

En este post veremos como se implementa la autenticación y la autorización en django, y comprobaremos que Django realmente simplifica la creación de Web apps. A modo de ejemplo tomaremos una gasolinera, con varios vendedores (despachadores), dos supervisores (uno por turno) y un gerente.

Con baterías incluidas

Aunque este es el motto de Python también aplica en Django, ya que viene cargado con varios módulos que evitan el reinventar la rueda. Entre estos módulos están su sistema de usuarios y grupos.

Autenticación y Autorización

Es importante aclarar estos dos conceptos, ya que en ocasiones se usan de forma indistinta, cuando realmente significan cosas diferentes, y por lo tanto Django trata estos conceptos por separado.

¿Qué es la Autenticación?

Según la RAE, “Es el proceso de autorizar o legalizar algo”. En Django es el mecanismo con el que se determina si un usuario ha iniciado sesión en el sistema.

¿Qué es la Autorización?

Es el proceso con el que Django valida que un usuario autentificado pueda accesar a un recurso protegido (una página web o un endpoint REST). Esto quiere decir que no basta con que un usuario se encuentre logueado para ver una página, también es necesario que tenga los permisos pertinentes para ver dicha página.

Los grupos de usuario

En el párrafo anterior vimos que un usuario debe tener permisos para accesar a un recurso. En Django estos permisos se corresponden a grupos, y un usuario puede tener (o mejor dicho pertenecer) a uno o más grupos.

Alta de grupos

Cuando entramos al panel administrativo, veremos la sección “AUTENTICACIÓN Y AUTORIZACIÓN” como se muestra en la imagen:

Atentificación y autorización

Para dar de alta a un grupo hay que presionar el primer botón Agregar de la imagen. Esto nos mostrará la pantalla para crear grupos:

Crear un grupo nuevo

En esta parte del post solo introduciremos el nombre del grupo en la primera caja de texto y presionaremos el botón GUARDAR, o GUARDAR y AGREGAR OTRO si queremos capturar más de un grupo (la sección Permisos la veremos más adelante). Hemos creado los grupos VENDEDOR, SUPERVISOR y GERENTE, y ahora nuestra pantalla de grupos luce así:

Grupos creados

El proceso de Autenticación

Hemos creado primero los grupos por que al dar de alta un usuario podemos indicar a que grupos pertenecerá ese usuario, pero podemos crear un usuario sin asignarle algún grupo y más tarde agregarlo a los grupos que necesitemos.

Alta de usuarios

Para que la autenticación funcione, primero tenemos que crear a los usuarios a autentificar. Para esto simplemente hay que presionar el botón Agregar del menú Autenticación y Autorización. Esto nos mostrará la primera pantalla de captura de usuarios:

Agregar usuario

En esta primera pantalla se tiene que capturar el nombre de usuario y contraseña, y presionamos el botón GUARDAR, nos llevará a la pantalla para editar la información complementaria del usuario:

Modificar usuario

En esta captura de pantalla al final tenemos la sección Permisos, y las tres primeras opciones son:

  • Activo. Indica si el usuario está vigente. Si esta casilla no está activada el usuario no podrá iniciar sesión.
  • Es staff. Si está activada, el usuario podrá entrar al panel administrativo.
  • Es superusuario. Si se activa esta casilla, el usuario tiene todos los privilegios administrativos del sistema.

Para asignarle un grupo a el usuario simplemente hay que seleccionar los grupos que nos interesen y presionar el botón flecha derecha para asignarlo:

Asignar grupo

Ahora solo nos resta guardar los cambios presionando el botón GUARDAR como se ve en la imagen siguiente (note que hay una sección llamada Permisos de Usuario. Esto lo veremos más adelante).

Guardar usuario

Ahora ya tenemos creado el usuario y agregado al grupo VENDEDOR. ¿Recuerdan que dije que Django también tiene baterías incluidas? Al visitar la página con el listado de usuarios vemos que por defecto nos genera varios filtros para que podamos buscar por: staff, superusuario, activo, y por último, por grupo:

Usuarios registrados

Creando las vistas de la aplicación

Ahora ya tenemos todo lo necesario para que crear las vistas. En sus inicio Django usaba funciones para las vistas, pero actualmente usa clases para implementarlas. Aunque aun se pueden usar las vistas con funciones yo usaré las Vistas Basadas en Clases ó mejor conocidas como CBV (por Class Based View). En este post no voy a profundizar en el funcionamiento de las CBV, solo hay que tener en cuenta que dependiendo de la clase base que usemos nuestra vista tendrá cierta funcionalidad añadida.

Vista para inicio de la app

Vamos a empezar con la vista más sencilla, esta no requiere autenticación ni autorización, cualquier persona puede verla.

from django.views.generic import TemplateView


class IndexView(TemplateView)
    template_name = 'homepage.html'

Esto es todo lo que necesitamos para implementar la vista. La clase TemplateView se encarga de generar gestionar la petición get, generar la plantilla especificada por template_name y en automático regresa un objeto Response con la plantilla procesada.

Vista para mostrar todas las ventas de un vendedor

Ahora necesitamos una vista que muestre todas las ventas realizadas por todos los vendedores (en la realidad se tiene que limitar el número de registros). Para esto utilizaremos la clase ListView la cual regresa todos los registros de un modelo si no se especifica alguna condición, y también nos aseguraremos de que el usuario esté autentificado y autorizado (grupo GERENTE). En Django la autenticación la implementa el método de la vista get_login_url(), mientras que la autorización se hace en el método test_func(). Para que Django sepa que estos métodos son para autenticación y autorización tenemos que usar los mixins LoginRequiredmixin y UserPassesTestMixin.

Nota: Es importante el orden de los mixins. Tanto LoginRequiredMixin como UserPassesTestMixin tienen que ir antes de la CBV.

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import ListView
from .models import Venta


class VentasView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    model = Venta
    template_name = 'ventas/ventas_sin_filtro.html'
    
    def get_login_url(self):
        if not self.request.user.is_authenticated:
            # el usuario no está logueado, ir a la página de login
            return super(VentasView, self).get_login_url()
        # El usuario está logueado pero no está autorizado
        return '/no_autorizado/'
    
    def test_func(self):
        # obtenemos todos los grupos del usuario logueado
        grupos = self.request.user.groups.all()
        # comparamos que el usuario pertenezca al grupo GERENTE
        if 'GERENTE' in grupos:
            return True
        return False

Como se puede observar la autentificación y autorización es muy sencilla, en la primera se regresa una cadena con el nombre de una URL, y esta URL puede ser la página de inicio de sesión o una página para indicar que no se tienen los permisos necesarios. En cuanto a la autorización la función nos pide regresar un valor booleano, True para indicar que el usuario tiene autorización y False en caso contrario. La lógica que usemos ya depende de nosotros, lo que hay que recordar es regresar True o False. Para dar una idea de lo que se puede hacer dentro de test_func vamos a implementar que el usuario pertenezca al grupo GERENTE y que solo pueda ver la página si el día está entre lunes y viernes:

import datetime
...
    def test_func(self):
        # obtenemos todos los grupos del usuario logueado
        grupos = self.request.user.groups.all()
        hoy = datetime.date.today()
        # comparamos que el usuario pertenezca al grupo GERENTE
        if 'GERENTE' in grupos and hoy.weekday() < 5:
            return True
        return False

Ahora vamos a implementar una vista que la puedan consultar tanto el vendedor y el supervisor, y vamos a filtrar las ventas en base al id del vendedor (si el usuario pertenece al grupo VENDEDOR) o en base a los id's de todos los vendedores asignados a un supervisor. Para esto usaremos nuevamente la clase ListView pero ahora usaremos el método get_queryset() para realizar el filtro por id. Cuando usamos get_queryset() ya no es necesario indicar un modelo como lo hicimos con model = Venta, ya que get_queryset() puede usar cualquier modelo (o ninguno, los datos pueden provenir de un archivo por ejemplo), el único requisito es que regrese un diccionario.

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import ListView
from .models import SupervisorVendedor, Venta


class VentasFiltradasView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    # Ya no es necesario especificar un modelo
    # model = Venta
    template_name = 'ventas/ventas_con_filtro.html'
    
    def get_login_url(self):
        if not self.request.user.is_authenticated:
            # el usuario no está logueado, ir a la página de login
            return super(VentasView, self).get_login_url()
        # El usuario está logueado pero no está autorizado
        return '/no_autorizado/'
    
    def test_func(self):
        # obtenemos todos los grupos del usuario logueado
        grupos = self.request.user.groups.all()
        # comparamos que el usuario pertenezca al grupo VENDEDOR o SUPERVISOR
        for grupo in grupos:
            if grupo in ['VENDEDOR', 'SUPERVISOR']:
                return True
        return False
        
    def get_queryset(self):
        grupos = self.request.user.groups.all()
        user_id = self.request.user.id
        if 'VENDEDOR' in grupos:
            ids = user_id
        elif 'SUPERVISOR' in grupos:
            ids = SupervisorVendedor.objects.filter(user_id=user_id).values_list(
                'vendedor_id', flat=True)
        ventas = Venta.objects.filter(vendedor_id__in=ids)
        
        return {'ventas': ventas}

Como podemos observar, get_login_url() no sufrió ningún cambio, pero test_func() sí cambió. Solo tuve que modificar la condición para validar que al menos uno de los grupos del usuario fuera VENDEDOR o SUPERVISOR (aunque también se puede validar que el usuario pertenezca a ambos grupos si así lo deseamos). El método get_queryset() no tiene que ver con la autenticación o autorización, pero lo pongo a modo de ejemplo por la interacción que se tiene con los grupos de usuario.

Herencia en la autenticación y autorización

El lector ávido se habrá percatado que los métodos get_login_url() y test_func() se repiten en las vistas VentasView y VentasFiltradasView, y aunque en este ejemplo en particular sí cambió test_func(), un patrón muy común es solo cambiar la lista de los grupos a validar. Así que en lugar de copiar y pegar estos métodos, los podemos implementar en una clase base para ser reutilizada por todas nuestras clases:

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin


class BaseAuthPerm(LoginRequiredMixin, UserPassesTestMixin):
    _PERMISSIONS = []
    
    def get_login_url(self):
        if not self.request.user.is_authenticated:
            # el usuario no está logueado, ir a la página de login
            return super(BaseAuthPerm, self).get_login_url()
        # El usuario está logueado pero no está autorizado
        return '/no_autorizado/'
    
    def test_func(self):
        # obtenemos todos los grupos del usuario logueado
        grupos = self.request.user.groups.all()
        # comparamos los grupos del usuario con la lista de permisos
        for grupo in self._PERMISSIONS:
            if grupo in ['VENDEDOR', 'SUPERVISOR']:
                return True
        return False

Ahora vamos a reimplementar nuestras clases usando esta clase base, el único requerimiento es agregar la propiedad _PERMISSIONS y asignarle una lista con los grupos que pueden visitar la página:

from my_auth import BaseAuthPerm
from django.views.generic import ListView, TemplateView
from .models import SupervisorVendedor, Venta


class IndexView(TemplateView)
    template_name = 'homepage.html'
    

class VentasView(BaseAuthPerm, ListView):
    _PERMISSIONS = ['GERENTE']
    model = Venta
    template_name = 'ventas/ventas_sin_filtro.html'
    

class VentasFiltradasView(BaseAuthPerm, ListView):
    _PERMISSIONS = ['VENDEDOR', 'SUPERVISOR']
    template_name = 'ventas/ventas_con_filtro.html'

        
    def get_queryset(self):
        grupos = self.request.user.groups.all()
        user_id = self.request.user.id
        if 'VENDEDOR' in grupos:
            ids = user_id
        elif 'SUPERVISOR' in grupos:
            ids = SupervisorVendedor.objects.filter(user_id=user_id).values_list(
                'vendedor_id', flat=True)
        ventas = Venta.objects.filter(vendedor_id__in=ids)
        
        return {'ventas': ventas}

Ahora nuestro código se ve más limpio y nos adherimos al principio DRY. También podemos reutilizar la clase base para las vistas de todas nuestras apps.

Sistema de permisos para el panel administrativo

Cuando vimos la creación de grupos y de usuarios saltamos la sección Permisos de Usuario, ya que esta sección es para los permisos de la sección administrativa de Django y no para las vistas de usuario. La sección relevante en la creación/modificación de usuarios es esta:

Permisos administrativos

Permisos por modelo

En Django cada modelo tiene cuatro permisos administrativos (es decir, son acciones que se pueden realizar desde el admin):

  1. Agregar registro.
  2. Modificar registro.
  3. Borrar registro.
  4. Ver log del registro. Cada acción que se haga directamente en un modelo desde el panel administrativo en automático genera una entrada en la bitácora de modificaciones del modelo.

Con estos cuatro permisos podemos definir de forma muy puntual quién y qué puede hacer dentro de la sección administrativa. Cuando un usuario se loguea en el panel administrativo primero se verifica que tenga el permiso Staff, y después revisa todos los permisos administrativos que tenga asignados para construir un menú con las secciones a las que tiene acceso.

Permisos por grupo

Cuando creamos un grupo o lo modificamos, podemos indicar cualquiera de los cuatros permisos administrativos, ya sea para uno o varios modelos. Esto es muy útil cuando queremos que todos los usuarios de un grupo tengan los mismos privilegios administrativos.

Permisos por usuario

Si necesitamos asignarle permisos a un solo usuario sin importar el grupo al que pertenezca, solo necesitamos editar su información, buscar la sección Permisos y seleccionar las acciones que podrá realizar este usuario.

Conclusión

El sistema de autenticación y autorización en Django es bastante simple, pero muy poderoso, sin magia negra que nos oculte el funcionamiento de estos mecanismos. Los permisos administrativos nos permiten delegar tareas a nuestro staff con la seguridad de que solo podrán realizar las tareas puntuales que les asignemos.