10 minutos
Sistema De Permisos en Django
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:
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:
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í:
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:
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:
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:
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).
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:
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
comoUserPassesTestMixin
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 por modelo
En Django
cada modelo tiene cuatro permisos administrativos (es
decir, son acciones que se pueden realizar desde el admin):
- Agregar registro.
- Modificar registro.
- Borrar registro.
- 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.