Lo primero que tenemos que tener claro cuando queremos montar un sistema de Django + LDAP es el funcionamiento de la base de datos: ¿tenemos una base de datos local además de la remota o sólo existe la base de datos en LDAP? En función de esto la solución puede ser completamente distinta. Veremos el caso sin base de datos local, partiendo de la premisa de que queremos algo lo más 'de fábrica' posible:
### Crear un backend de autenticación
La idea sería simplificar al máximo la implementación, usando para la autenticación un backend de django. Lo bueno de los backends es que se conectan directamente a la sesión, por lo que no tendríamos que re-escribir cómo funcionan estas, ni reescribir la vista de login, ya que todo se haría por debajo.
Los backends de autenticación han de implementar dos métodos: `authenticate` y `get_user`. El primero se puede hacer fácilmente, devolviendo un User de django que no se pilla de base de datos, sino que se crea el vuelo:
#!python
# backends.py
class MaadixBackend:
""" A custom LDAP authentication backend """
def authenticate(self, request, username, password):
pass
def get_user(self, pk):
pass
Y en `settings.py`:
#!python
# settings.py
AUTHENTICATION_BACKENDS = (
"simpleldap.backends.MaadixBackend",
)
Ahora tenemos que definir los métodos del backend, para lo que tenemos dos casos, usar un superusuario que comprueba la autenticación o intentar hacer bind directamente con las credenciales del usuario.
### Hacer login con un admin superusuario
La idea sería hacer esto para hacer un search posterior del _username_ comprobando que el hash de contraseña del formulario coincide con el valor obtenido de LDAP. Ésto lo haríamos en el método authenticate:
#!python
# backends.y
def _authenticate(self, request, username, password):
""" Maadix backend """
c = connect_ldap(
settings.LDAP_USER,
settings.AUTH_LDAP_BIND_PASSWORD
)
ldap = c['connection']
if ldap:
ldap.search(
settings.LDAP_TREE_USERS,
"(&(objectClass=person)(cn=%s))" % username,
attributes=[
"cn",
"userPassword"
]
)
authenticated = ... # check password
if authenticated:
return User(
username=username,
password=password
)
return None
De este modo hacemos bind con el superusuario definido en `private_settings.py`y buscamos los datos del usuario en cuestión. Ahora tenemos que comprobar el pass, para lo que tenemos que saber cómo calcula el hash ldap, de manera que hagamos la misma transformación (implementación HMAC y clave secreta si la hay). Comprobamos el pass y si la comprobación pasa creamos un usuario al vuelo, sin guardarlo en base de datos. *Tenemos la cuestión de cómo se almacena el password*.
### Hacer bind con el usuario
El método _authenticate_ cambia, ya que intentamos conectar directamente con las credenciales del login:
#!python
# backends.py
def authenticate(self, request, username, password):
""" Maadix backend """
ldap = connect_ldap(
username,
password
)
if ldap['connection']:
user = User(
username=username,
password=password
)
return user
return None
Con esto resolveríamos el login. El problema ahora es acceder a una vista autenticada donde el decorador va a llamar al método `get_user`. Éste método devuelve un usuario dada una clave primaria. Podemos crear un modelo custom donde la clave primaria sea el username, sobreescribiendo el campo _username_ heredado de `AbstractUser` (la definición del campo está copiada de allí, tras lo que se le ha añadido `primary_key=True`):
#!python
# users/models.py
# django
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
# Create your models here.
class MaadixUser(AbstractUser):
username_validator = UnicodeUsernameValidator()
username = models.CharField(
'username',
max_length=150,
unique=True,
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
validators=[username_validator],
error_messages={
'unique': "A user with that username already exists.",
},
primary_key=True
)
Y especificarlo en `settings.py`:
#!python
# settings.py
AUTH_USER_MODEL = 'users.MaadixUser'
Pero encontramos ahora dos problemas. El primero es que cuando se hace login Django manda una señal y auth tiene un receiver que guarda en base de datos la fecha en que se ha hecho ese login, lo que exige... una base de datos. Si vemos el código de configuración de `auth` (que es donde se encuentran las señales normalmente) vemos:
#!python
# Register the handler only if UserModel.last_login is a field.
if isinstance(last_login_field, DeferredAttribute):
from .models import update_last_login
user_logged_in.connect(update_last_login, dispatch_uid='update_last_login')
Por lo que sólo tenemos que hacer que en nuestro usuario last_login no sea un campo. Para no romper la lógica OO podemos simplemente sobre-escribirlo como propiedad:
#!python
class MaadixUser(AbstractUser):
...
@property
def last_login(self):
pass
Con lo que creamos el backend definitivo:
#!python
class MaadixBackend:
""" A custom LDAP authentication backend """
def authenticate(self, request, username, password):
""" Maadix backend """
ldap = connect_ldap(
username,
password
)
if ldap['connection']:
user = MaadixUser(
username=username,
password=password
)
print(user)
return user
return None
def get_user(self, username):
ldap = connect_ldap(
settings.LDAP_USER,
settings.AUTH_LDAP_BIND_PASSWORD
)
if ldap['connection']:
user = MaadixUser(
username='username',
# password=... # NO PODEMOS ACCEDER AL PASSWORD
)
return user
return None
### Guardar la conexión a LDAP en el request