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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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:

1
2
3
4
# 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 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.pyy 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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:

1
2
# 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:

1
2
3
4
# 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:

1
2
3
4
5
6
class MaadixUser(AbstractUser):
...

@property
def last_login(self):
    pass

Con lo que creamos el backend definitivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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