Café y Código

7. Autenticación, Usuario Personalizado y Seguridad 🛡️

1. Extensión del Modelo de Usuario en Django

El modelo User por defecto de Django incluye únicamente username, email, first_name, last_name y password. En aplicaciones profesionales casi siempre necesitamos datos adicionales: número de documento (RUT / DNI), teléfono, dirección, rol en el sistema o estado de cuenta.

⚠️ Regla de Oro en Django (Official Recommendation):Configura un modelo de usuario personalizado al inicio del proyecto antes de ejecutar la primera migración. Cambiar AUTH_USER_MODEL a mitad de un proyecto en producción requiere migraciones manuales complejas para actualizar las claves foráneas existentes.

1.1 Estrategias de Extensión: AbstractUser vs OneToOneField

Opción A: AbstractUser (Recomendada)

Hereda de django.contrib.auth.models.AbstractUser. Mantiene toda la lógica nativa (hash de contraseñas, reseteo de claves, grupos y permisos) y añade nuevas columnas directamente a la tabla del usuario.

✔ Sin JOINs extra en base de datos.
Opción B: Perfil OneToOneField

Crea un modelo satélite (ej: PerfilUsuario) enlazado mediante OneToOneField(User). Útil cuando no puedes alterar la tabla de usuarios existente en un sistema legado.

Requiere JOIN extra para cada consulta.

1.2 Implementación del Modelo Usuario en usuarios/models.py

Creamos una aplicación dedicada usuarios (python manage.py startapp usuarios) y definimos nuestro modelo extendido:

usuarios/models.py
PYTHON
1# usuarios/models.py
2from django.contrib.auth.models import AbstractUser
3from django.core.validators import RegexValidator
4from django.db import models
5
6
7class Usuario(AbstractUser):
8 ROLES = [
9 ('LECTOR', 'Lector / Socio'),
10 ('BIBLIOTECARIO', 'Bibliotecario (Personal)'),
11 ('ADMINISTRADOR', 'Administrador General'),
12 ]
13
14 ESTADOS_CUENTA = [
15 ('ACTIVA', 'Cuenta Activa'),
16 ('SUSPENDIDA', 'Suspendida por Incumplimiento'),
17 ('EN_MORA', 'En Mora / Multas Pendientes'),
18 ]
19
20 # Validador de formato de documento de identidad
21 validador_rut = RegexValidator(
22 regex=r'^[0-9]{1,2}\.[0-9]{3}\.[0-9]{3}-[0-9kK]{1}$',
23 message="Formato de RUT/DNI inválido. Debe tener la estructura XX.XXX.XXX-X."
24 )
25
26 rut_dni = models.CharField(
27 max_length=20,
28 unique=True,
29 validators=[validador_rut],
30 verbose_name="RUT / Documento de Identidad"
31 )
32 telefono = models.CharField(max_length=20, blank=True, verbose_name="Teléfono Móvil")
33 direccion = models.CharField(max_length=255, blank=True, verbose_name="Dirección de Residencia")
34 fecha_nacimiento = models.DateField(null=True, blank=True, verbose_name="Fecha de Nacimiento")
35 rol = models.CharField(max_length=20, choices=ROLES, default='LECTOR', db_index=True)
36 estado_cuenta = models.CharField(max_length=20, choices=ESTADOS_CUENTA, default='ACTIVA', db_index=True)
37 multas_pendientes = models.DecimalField(max_digits=8, decimal_places=2, default=0.00)
38 avatar = models.ImageField(upload_to='avatars/%Y/%m/', null=True, blank=True)
39
40 class Meta:
41 verbose_name = "Usuario de Biblioteca"
42 verbose_name_plural = "Usuarios de Biblioteca"
43 ordering = ['username']
44
45 @property
46 def puede_solicitar_prestamos(self):
47 """Regla de negocio: el usuario debe estar activo, sin multas y sin suspensión."""
48 return self.is_active and self.estado_cuenta == 'ACTIVA' and self.multas_pendientes == 0
49
50 def __str__(self):
51 return f"{self.get_full_name() or self.username} ({self.get_rol_display()})"

Luego, registramos el modelo en config/settings.py:

config/settings.py
PYTHON
1# config/settings.py
2AUTH_USER_MODEL = 'usuarios.Usuario'
3
4INSTALLED_APPS = [
5 'django.contrib.admin',
6 'django.contrib.auth',
7 'django.contrib.contenttypes',
8 'django.contrib.sessions',
9 'django.contrib.messages',
10 'django.contrib.staticfiles',
11 # Nuestras Apps del proyecto:
12 'usuarios.apps.UsuariosConfig',
13 'biblioteca.apps.BibliotecaConfig',
14]

2. Manipulación Avanzada del Django Admin para Usuarios (UserAdmin)

No registres un modelo de usuario con un simple admin.ModelAdmin común, pues perderías los formularios especiales de Django que hashean contraseñas con algoritmos criptográficos (PBKDF2/Argon2). Debes heredar de django.contrib.auth.admin.UserAdmin:

usuarios/admin.py
PYTHON
1# usuarios/admin.py
2from django.contrib import admin
3from django.contrib.auth.admin import UserAdmin
4from django.utils.html import format_html
5from .models import Usuario
6
7
8class SolicitudPrestamoInline(admin.TabularInline):
9 """Permite auditar el historial de préstamos del usuario directamente en su ficha."""
10 from biblioteca.models import SolicitudPrestamo
11 model = SolicitudPrestamo
12 extra = 0
13 readonly_fields = ['fecha_solicitud', 'libro', 'estado', 'multa_acumulada']
14 can_delete = False
15
16
17@admin.register(Usuario)
18class CustomUserAdmin(UserAdmin):
19 # Columnas en el listado
20 list_display = [
21 'username', 'email', 'nombre_completo_col', 'rut_dni',
22 'rol_badge', 'estado_badge', 'multas_pendientes', 'is_staff'
23 ]
24 list_filter = ['rol', 'estado_cuenta', 'is_staff', 'is_active']
25 search_fields = ['username', 'email', 'rut_dni', 'first_name', 'last_name']
26 inlines = [SolicitudPrestamoInline]
27
28 # FIELDSETS: Organización de los campos al EDITAR un usuario
29 fieldsets = (
30 ('Credenciales de Acceso', {
31 'fields': ('username', 'password')
32 }),
33 ('Información Personal Extendida', {
34 'fields': ('first_name', 'last_name', 'email', 'rut_dni', 'telefono', 'direccion', 'fecha_nacimiento', 'avatar')
35 }),
36 ('Roles y Permisos de Sistema', {
37 'fields': ('rol', 'is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
38 'classes': ('collapse',)
39 }),
40 ('Estado Operativo y Finanzas', {
41 'fields': ('estado_cuenta', 'multas_pendientes')
42 }),
43 ('Fechas de Auditoría', {
44 'fields': ('last_login', 'date_joined'),
45 'classes': ('collapse',)
46 }),
47 )
48
49 # ADD_FIELDSETS: Campos solicitados al CREAR un nuevo usuario en /admin/usuarios/usuario/add/
50 add_fieldsets = UserAdmin.add_fieldsets + (
51 ('Datos Adicionales Obligatorios', {
52 'fields': ('email', 'rut_dni', 'rol')
53 }),
54 )
55
56 def nombre_completo_col(self, obj):
57 return obj.get_full_name() or "—"
58 nombre_completo_col.short_description = "Nombre Completo"
59
60 def rol_badge(self, obj):
61 colores = {'ADMINISTRADOR': '#7C3AED', 'BIBLIOTECARIO': '#0284C7', 'LECTOR': '#475569'}
62 return format_html(
63 '<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 9999px; font-weight: bold; font-size: 10px;">{}</span>',
64 colores.get(obj.rol, '#475569'),
65 obj.rol
66 )
67 rol_badge.short_description = "Rol"
68
69 def estado_badge(self, obj):
70 if obj.estado_cuenta == 'ACTIVA':
71 return format_html('<span style="color: #10B981; font-weight: bold;">● Activa</span>')
72 elif obj.estado_cuenta == 'EN_MORA':
73 return format_html('<span style="color: #F59E0B; font-weight: bold;">▲ En Mora</span>')
74 return format_html('<span style="color: #EF4444; font-weight: bold;">✖ Suspendida</span>')
75 estado_badge.short_description = "Estado Cuenta"

3. Validaciones Críticas en el Administrador y Formularios

Un error común en sistemas administrativos es permitir que un superusuario se desactive a sí mismo o se quite permisos involuntariamente, bloqueando el acceso al sistema. Podemos proteger esto sobreescribiendo el formulario del admin:

usuarios/forms.py (Validaciones de Seguridad)
PYTHON
1# usuarios/forms.py
2from django import forms
3from django.contrib.auth.forms import UserChangeForm
4from .models import Usuario
5
6
7class CustomUserChangeForm(UserChangeForm):
8 class Meta:
9 model = Usuario
10 fields = '__all__'
11
12 def __init__(self, *args, **kwargs):
13 self.request_user = kwargs.pop('current_user', None)
14 super().__init__(*args, **kwargs)
15
16 def clean(self):
17 cleaned_data = super().clean()
18 is_active = cleaned_data.get('is_active')
19 is_superuser = cleaned_data.get('is_superuser')
20
21 # REGLA DE AUTO-PROTECCIÓN:
22 # Si el usuario que se está editando es el mismo que está logueado en la sesión
23 if self.instance and self.request_user and self.instance.pk == self.request_user.pk:
24 if not is_active:
25 raise forms.ValidationError(
26 "Seguridad: No puedes desactivar tu propia cuenta mientras estás administrando el sistema."
27 )
28 if not is_superuser:
29 raise forms.ValidationError(
30 "Seguridad: No puedes revocar tus propios privilegios de superusuario."
31 )
32
33 # Validación cruzada de estado vs multas
34 multas = cleaned_data.get('multas_pendientes', 0)
35 estado = cleaned_data.get('estado_cuenta')
36 if multas > 0 and estado == 'ACTIVA':
37 # Auto-corregir o advertir
38 cleaned_data['estado_cuenta'] = 'EN_MORA'
39
40 return cleaned_data

4. Acciones Masivas de Administración (@admin.action)

Las acciones permiten al personal de biblioteca procesar decenas o cientos de registros simultáneamente sin tener que abrirlos uno por uno:

usuarios/actions.py
PYTHON
1from django.contrib import admin, messages
2
3@admin.action(description="Suspender cuentas seleccionadas con multas pendientes")
4def suspender_por_mora(modeladmin, request, queryset):
5 # Solo aplicamos a quienes registren deuda
6 morosos = queryset.filter(multas_pendientes__gt=0)
7 actualizados = morosos.update(estado_cuenta='SUSPENDIDA', is_active=False)
8
9 if actualizados > 0:
10 modeladmin.message_user(
11 request,
12 f"Se han suspendido {actualizados} cuenta(s) por registrar multas pendientes.",
13 messages.WARNING
14 )
15 else:
16 modeladmin.message_user(
17 request,
18 "Ninguno de los usuarios seleccionados registraba multas pendientes para suspender.",
19 messages.INFO
20 )
21
22
23@admin.action(description="Promover usuarios a rol de BIBLIOTECARIO (Staff)")
24def promover_a_bibliotecario(modeladmin, request, queryset):
25 actualizados = queryset.update(rol='BIBLIOTECARIO', is_staff=True)
26 modeladmin.message_user(
27 request,
28 f"Se promovieron {actualizados} usuario(s) al rol de Bibliotecario con acceso al panel.",
29 messages.SUCCESS
30 )
31
32
33@admin.action(description="Reactivar cuentas seleccionadas")
34def reactivar_cuentas(modeladmin, request, queryset):
35 actualizados = queryset.update(is_active=True, estado_cuenta='ACTIVA')
36 modeladmin.message_user(
37 request,
38 f"Se han reactivado {actualizados} cuenta(s) exitosamente.",
39 messages.SUCCESS
40 )
41
42# Se registran en CustomUserAdmin:
43CustomUserAdmin.actions = [suspender_por_mora, promover_a_bibliotecario, reactivar_cuentas]

⚡ Laboratorio Interactivo: Simulador de Autenticación y UserAdmin

Prueba en vivo la inspección del objeto request.user, cómo reaccionan las vistas protegidas ante diferentes roles y multas, y experimenta las validaciones de auto-protección y acciones masivas del administrador.

🛡️

Django Auth & UserAdmin Simulator

Prueba usuarios extendidos (AbstractUser), roles, permisos y acciones del UserAdmin

Usuario Activo:admin_centralADMINISTRADOR
Objeto `request.user` (AbstractUser)auth_user
username: "admin_central"
email: "admin@biblioteca.local"
nombre_completo: "Beatriz González"
rut_dni: "15.420.912-3"
rol: "ADMINISTRADOR"
is_authenticated: True
is_staff: true
is_superuser: true
is_active: true
multas_pendientes: $0.00 USD
Probar Vistas Protegidas con este UsuarioDecoradores

Haz clic para simular que admin_central intenta ingresar a diferentes endpoints:

6. Control de Acceso en Vistas (Decoradores y Mixins)

En Django proteges tus vistas utilizando decoradores (para funciones FBV) y mixins de herencia (para clases CBV):

seguridad_vistas.py
PYTHON
1from django.contrib.auth.decorators import login_required, user_passes_test, permission_required
2from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
3from django.views.generic import TemplateView
4from django.shortcuts import render
5from .models import Usuario
6
7
8# 1. En FBVs: Verificación de estado de cuenta y morosidad
9def es_lector_habilitado(user):
10 return user.is_authenticated and user.puede_solicitar_prestamos
11
12@login_required
13@user_passes_test(es_lector_habilitado, login_url='/cuenta/regularizar-multas/')
14def solicitar_prestamo_libro(request, libro_id):
15 """Solo usuarios autenticados sin multas ni suspensiones pueden pedir préstamos."""
16 return render(request, 'biblioteca/solicitar_prestamo.html', {'libro_id': libro_id})
17
18
19# 2. En CBVs: Vista solo accesible por Bibliotecarios o Administradores
20class PanelGestionBibliotecarioView(LoginRequiredMixin, UserPassesTestMixin, TemplateView):
21 template_name = 'biblioteca/panel_bibliotecario.html'
22
23 def test_func(self):
24 user = self.request.user
25 return user.is_staff or user.rol in ['BIBLIOTECARIO', 'ADMINISTRADOR']

7. Ejercicios Didácticos: Proyecto 1 (Biblioteca)

Pon a prueba la gestión de usuarios y permisos sobre la Biblioteca Central.

Ejercicio 7.1: Mixin de Verificación de Multas Pendientes

Crea un Mixin personalizado reutilizable para CBVs llamado SinMultasRequiredMixin que rechace con PermissionDenied (HTTP 403) a cualquier usuario que tenga multas_pendientes > 0.

Ver Solución Explicada
solucion_mixin_multas.py
PYTHON
1from django.core.exceptions import PermissionDenied
2from django.contrib.auth.mixins import AccessMixin
3
4class SinMultasRequiredMixin(AccessMixin):
5 """Mixin que verifica que el usuario autenticado no tenga sanciones económicas activas."""
6 def dispatch(self, request, *args, **kwargs):
7 if not request.user.is_authenticated:
8 return self.handle_no_permission()
9 if request.user.multas_pendientes > 0:
10 raise PermissionDenied("Tu cuenta registra multas pendientes. No puedes realizar préstamos.")
11 return super().dispatch(request, *args, **kwargs)

Ejercicio 7.2: Acción de Admin para Condonar y Reactivar

Escribe una acción para CustomUserAdmin que ponga las multas a $0.00 y restaure el estado de cuenta a 'ACTIVA'.

Ver Solución Explicada
solucion_condonar_action.py
PYTHON
1@admin.action(description="Condonar multas y reactivar cuentas seleccionadas")
2def condonar_y_reactivar(modeladmin, request, queryset):
3 actualizados = queryset.update(
4 multas_pendientes=0.00,
5 estado_cuenta='ACTIVA',
6 is_active=True
7 )
8 modeladmin.message_user(
9 request,
10 f"Se han regularizado {actualizados} cuenta(s) de usuarios.",
11 messages.SUCCESS
12 )

8. Desafío Práctico: Segundo Proyecto — Tienda Online (`tienda`) 🛒

Aplica los conceptos de usuarios y permisos en el segundo proyecto (C:\proyectos\django\tienda), donde los clientes tienen límites de crédito y categorización VIP.

Reto 8.1: Extensión de Cliente con Límite de Crédito

En pedidos/models.py, amplía el modelo de Cliente con un método puede_comprar_a_credito(monto) que valide si el total del pedido no excede su crédito asignado y su cuenta no está bloqueada por fraude.

Ver Solución Explicada
solucion_cliente_tienda.py
PYTHON
1# pedidos/models.py
2from django.db import models
3from django.conf import settings
4
5class PerfilCliente(models.Model):
6 user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='perfil_tienda')
7 es_vip = models.BooleanField(default=False)
8 limite_credito = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
9 bloqueado_por_fraude = models.BooleanField(default=False)
10
11 def puede_comprar_a_credito(self, monto):
12 if self.bloqueado_por_fraude:
13 return False
14 return self.limite_credito >= monto

Reto 8.2: Acción de Admin para Ascender a Cliente VIP

Crea una acción en el panel administrativo de la tienda que convierta a los clientes seleccionados en es_vip = True y les otorgue automáticamente un límite de crédito de $2,000 USD.

Ver Solución Explicada
solucion_vip_action.py
PYTHON
1# pedidos/admin.py
2from django.contrib import admin, messages
3
4@admin.action(description="Promover a CLIENTE VIP (Crédito $2,000 USD)")
5def promover_cliente_vip(modeladmin, request, queryset):
6 actualizados = queryset.update(es_vip=True, limite_credito=2000.00)
7 modeladmin.message_user(
8 request,
9 f"Se han promovido {actualizados} cliente(s) a categoría VIP con crédito habilitado.",
10 messages.SUCCESS
11 )

Ponte a prueba

Verifica que comprendes el modelo de usuario personalizado, la configuración de UserAdmin y el control de seguridad.

Ko-fi
Donaciones
Apoyá cafeycodigo con un café en Ko-fi. Colaboradores: insignia, muro y zona exclusiva.