#Identidad y permiso responden preguntas diferentes
Autenticación significa comprobar quién es la persona que hace una petición. Autorización significa decidir qué puede hacer sobre un recurso concreto. Una sesión válida de Ana no le permite leer automáticamente los proyectos de Bruno. Esta diferencia es la base del aislamiento por organización.
Django incluye gestión de usuarios, contraseñas, sesiones y permisos por tipo de modelo. No necesitas diseñar tu propio algoritmo de contraseñas ni guardar un user_id enviado por el navegador como prueba de identidad. Sí necesitas expresar las reglas de tu producto: qué organización, qué rol, qué objeto y qué acción.
#Cómo continúa la identidad entre peticiones
Tras validar credenciales, el servidor crea una sesión y el navegador conserva una cookie identificadora. En otra petición, el middleware lee la cookie, recupera la sesión y asocia request.user. La cookie no es una contraseña en cada petición, pero quien la roba puede actuar como esa sesión hasta que deje de ser válida; por eso importa HTTPS y la política de cookies.
La configuración generada ya incluye aplicaciones y middleware necesarios. Su orden importa: las sesiones se resuelven antes de que autenticación las utilice. No retires middleware al azar para que desaparezca un error. Los procesos comparten las sesiones mediante la base configurada, no mediante una variable global.
#Contraseñas gestionadas por Django
from django.contrib.auth import get_user_model
User = get_user_model()
usuario = User.objects.create_user(username="editora", password="solo-un-ejemplo-local")
print(usuario.check_password("solo-un-ejemplo-local"))
Este fragmento de shell crea un usuario de laboratorio y devuelve True al comprobar la clave; no uses esa contraseña publicada en una aplicación real. create_user y set_password gestionan el hash. Asignar usuario.password = "texto" no lo hace correctamente. get_user_model() obtiene el modelo de usuario configurado; para relaciones en modelos utiliza settings.AUTH_USER_MODEL.
Un hash de contraseña permite comprobar una candidata sin guardar la contraseña original. No es cifrado reversible. Django incluye algoritmos y mecanismos para actualizar hashes cuando procede; utiliza sus validadores y un flujo de recuperación en vez de enviar contraseñas por correo. El login incorporado no añade por sí mismo rate limiting frente a intentos masivos: coloca límites medidos en la capa apropiada sin bloquear indiscriminadamente usuarios que comparten IP.
Si un proyecto necesita un usuario propio, decide el modelo sustituto antes de las primeras migraciones; cambiarlo con tablas y relaciones en producción requiere una migración considerable. No necesitas un usuario personalizado solo para guardar preferencias: una relación separada puede bastar.
#Conectar login y logout sin escribir criptografía
En config/urls.py, añade estos imports y rutas conservando las de artículos y administrador:
from django.contrib.auth import views as auth_views
urlpatterns += [
path("cuentas/login/", auth_views.LoginView.as_view(), name="login"),
path("cuentas/logout/", auth_views.LogoutView.as_view(), name="logout"),
]
En settings.py, sustituye el LOGIN_URL provisional y añade:
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "articulos:listado"
LOGOUT_REDIRECT_URL = "articulos:listado"
Crea articulos/templates/registration/login.html:
{% extends 'base.html' %}
{% block contenido %}
<h1>Entrar</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="hidden" name="next" value="{{ next }}">
<button type="submit">Entrar</button>
</form>
{% endblock %}
next permite volver a la página solicitada después de entrar; LoginView valida el destino en lugar de aceptar una redirección externa arbitraria. Para salir, usa un formulario con POST y CSRF:
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">Salir</button>
</form>
Django 5.2 no implementa logout mediante un simple enlace GET. Salir cambia estado y debe protegerse como tal. Los mensajes de login tampoco deben revelar innecesariamente si una cuenta concreta existe.
#Permisos por modelo y grupos
Django crea permisos add, view, change y delete para cada modelo al migrar. Una cuenta is_staff puede entrar al administrador si además reúne las condiciones necesarias; is_superuser concede privilegios globales. Ninguno de esos indicadores significa «gestor de una organización concreta».
En la shell puedes preparar un grupo editorial:
from django.contrib.auth.models import Group, Permission
editores, _ = Group.objects.get_or_create(name="Editores")
permisos = Permission.objects.filter(
content_type__app_label="articulos",
codename__in=["add_articulo", "change_articulo", "view_articulo"],
)
editores.permissions.set(permisos)
usuario.groups.add(editores)
Este bloque supone la variable usuario creada antes; si has abierto otra shell, recupérala con get_user_model().objects.get(username="editora"). Un grupo reúne permisos y simplifica su administración. has_perm("articulos.change_articulo") responde a un permiso global del tipo de modelo; no deduce propiedad ni pertenencia de cada fila. El backend incorporado no convierte automáticamente has_perm(..., objeto) en permisos por objeto.
En una prueba, si cambias permisos y ya los habías consultado en esa misma instancia de usuario, puede existir una caché de permisos: recupera una instancia nueva antes de comprobar el cambio. No confundas ese detalle de objetos en memoria con una autorización obsoleta almacenada permanentemente en la base.
#Aislamiento por organización, sin confiar en la URL
El proyecto final introducirá Organizacion, Membresia(usuario, organizacion, activa, gestiona) y Sala(organizacion, nombre). La organización de una URL expresa a dónde quiere ir el usuario; una consulta de membresía decide si puede hacerlo.
# Patrón sobre los modelos del proyecto final.
miembro = get_object_or_404(
Membresia.objects.select_related("organizacion"),
usuario=request.user, organizacion_id=organizacion_id, activa=True,
)
sala = get_object_or_404(
Sala.objects.filter(organizacion=miembro.organizacion), pk=sala_id,
)
El segundo get_object_or_404 parte del conjunto permitido. No hace una búsqueda global que luego «recuerda» comprobar a veces. Si cambias sala_id por el de otra organización, no coincide. Si cambias organizacion_id, falla la primera consulta. Un UUID difícil de adivinar puede reducir enumeración, pero no sustituye estas condiciones.
Aplicar el filtro solo al listado deja expuestos detalle, descarga, edición, borrado o exportación. Los selectores de formulario también deben limitar las relaciones elegibles: una reserva no puede escoger una sala ajena aunque su página pertenezca a tu organización. Y la función de servicio vuelve a comprobarlo para entradas por otras vías.
#Qué pasa si el permiso cambia mientras se ejecuta la acción
Si una persona deja de pertenecer a la organización entre el formulario y el POST, se debe comprobar de nuevo. Si necesitas ordenar una revocación que compite exactamente con una escritura, define la semántica: una operación que ya tomó el bloqueo puede confirmarse antes de la revocación; las siguientes deben rechazarse. El proyecto final bloquea la membresía dentro de la transacción para dar ese orden explícito.
No mantengas un permiso derivado indefinidamente en una caché sin definir invalidación y vencimiento. Tampoco reutilices una respuesta HTML privada entre usuarios porque comparta URL: una caché mal particionada puede saltarse una autorización correcta en la view.
#Hacer la autorización observable en pruebas
Prepara dos usuarios y dos organizaciones. Comprueba lectura y escritura, no solo un camino feliz:
| Intento | Resultado esperado |
|---|---|
| Usuario sin sesión abre panel privado | Redirección al login de la web |
| Miembro abre su organización | 200 con sus datos |
| Miembro cambia la URL a otra organización | 404 según la política de no revelar existencia |
| Miembro envía una sala ajena en POST | Error de campo y ninguna escritura |
| Miembro intenta cancelar una reserva ajena de su organización | 403 salvo rol gestor |
| Miembro revocado repite la operación | Rechazo, aunque conserve una sesión válida |
Un 403 significa que el servidor entendió la petición pero no permite la acción. Elegir 404 para recursos fuera del conjunto visible evita confirmar su existencia; aplica la política de forma consistente. Registra intentos relevantes sin volcar cookies, contraseñas, cuerpos completos ni datos privados en logs.
Comprobación: entra como un usuario sin permisos editoriales y prueba /articulos/nuevo/; después concédele el grupo y vuelve a intentarlo. Explica por qué eso resuelve el permiso global de artículos pero no sería suficiente para un producto con varias organizaciones.