Reutiliza auth_ldap en vez de hardcodear LDAP en Moodle

Reutiliza auth_ldap en vez de hardcodear LDAP en Moodle
ContenidoContents

Tengo un plugin propio de Moodle, mod_pledge. Su función es sencilla: antes de poder hacer los exámenes online de la universidad, el estudiante tiene que aceptar un código de honor. Si no lo acepta, no ve los cuestionarios ni las actividades del examen — sin compromiso, no hay examen.

Sobre esa base añadí algo más: cuando el alumno acepta el código de honor, disparo una tarea que genera su justificante de asistencia al examen y se lo envía por email. Y justo esa tarea es la que un día empezó a fallar en silencio: cada ejecución terminaba con “No se proporcionó ningún documento” y entraba en un bucle de reintentos (el faildelay de Moodle subiendo y subiendo). Ningún error de conexión, ninguna excepción ruidosa. Solo justificantes que no salían.

La causa final no fue lo que parecía. Y la lección es de esas que se aplican a casi cualquier integración.

Por qué el justificante necesita LDAP

Para rellenar el justificante hace falta el número de documento (DNI/NIE) del alumno. Ese dato no está en Moodle: vive en el directorio corporativo. Así que, al generar el justificante, la tarea lo buscaba por LDAP.

Hasta ahí, normal. El problema estaba en cómo lo buscaba.

El diagnóstico

Mi primera reacción fue pensar que fallaban todos. Pero al revisar el historial de ejecuciones de la tarea me di cuenta de que algunos justificantes sí salían: no era un fallo total, era selectivo. Y el patrón, una vez visto, cantaba: los que fallaban eran los alumnos nuevos.

¿Por qué? El código original consultaba directamente un Oracle Internet Directory (OID) antiguo, con todo hardcodeado: la IP del host, el base_dn, el atributo, el filtro y un bind anónimo. Algo así:

 1private static function obtener_numdocumento_ldap(string $uid) {
 2    $host      = "ldap://10.x.x.x:389";
 3    $base_dn   = "cn=users,dc=ejemplo,dc=es";
 4    $filtro    = "(uid=$uid)";
 5    $atributos = ["numdocumento"];
 6
 7    $ldapconn = ldap_connect($host);
 8    ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
 9    ldap_bind($ldapconn); // bind anónimo
10
11    $resultado = ldap_search($ldapconn, $base_dn, $filtro, $atributos);
12    $entradas  = ldap_get_entries($ldapconn, $resultado);
13    // ...
14}

¿Qué había pasado? Que la organización había migrado a Active Directory y había dejado de actualizar el OID: a los usuarios nuevos ya solo se les daba de alta en el AD, no en el viejo directorio. Lo traicionero es que el OID seguía respondiendo —por eso no saltaba ningún error de conexión—, así que mi tarea encontraba a los alumnos de siempre pero a los recién llegados no. Sin documento, no había justificante. De ahí que unos justificantes salieran y otros no.

El bug de superficie era “el directorio cambió”. Pero el bug de fondo era otro: el host, el base_dn, el atributo y el bind estaban duplicados a mano dentro del plugin… cuando Moodle ya sabía hablar con el nuevo AD. Lo hace cada vez que alguien inicia sesión, a través del plugin auth_ldap.

Tenía la configuración correcta delante. Solo que la había copiado a un sitio donde no tenía por qué estar.

La solución: dejar de reinventar la conexión

Y aquí entró un condicionante muy de la vida real: era fin de semana. La gente de sistemas no me iba a pasar los datos de conexión y autenticación del nuevo AD (host, cuenta de servicio, contraseña) hasta el lunes. Podía esperar… o caer en lo evidente: Moodle ya tenía esa conexión configurada y funcionando, la que usa auth_ldap para autenticar a todo el mundo contra ese mismo AD. ¿Para qué pedir unas credenciales que el propio Moodle ya tenía guardadas?

Así que en lugar de mantener mi propia conexión LDAP, reutilizo la de auth_ldap con get_auth_plugin('ldap'):

 1private static function obtener_numdocumento_ldap(string $username) {
 2    global $CFG;
 3    require_once($CFG->libdir . '/authlib.php');
 4
 5    // Reutilizamos la config de auth_ldap (host, bind, versión, TLS…).
 6    $authplugin = get_auth_plugin('ldap');
 7
 8    try {
 9        $ldapconn = $authplugin->ldap_connect();
10    } catch (\Exception $e) {
11        debugging("No se pudo conectar/bind con el AD: " . $e->getMessage(), DEBUG_DEVELOPER);
12        return null;
13    }
14
15    $userdn = $authplugin->ldap_find_userdn($ldapconn, $username);
16    if (!$userdn) {
17        $authplugin->ldap_close();
18        return null;
19    }
20
21    $atributo  = 'miatributo_documento';
22    $resultado = ldap_read($ldapconn, $userdn, '(objectClass=*)', [$atributo]);
23    if (!$resultado) {
24        $authplugin->ldap_close();
25        return null;
26    }
27
28    $entradas = ldap_get_entries($ldapconn, $resultado);
29    $authplugin->ldap_close();
30
31    if (!empty($entradas[0][$atributo][0])) {
32        return $entradas[0][$atributo][0];
33    }
34    return null;
35}

Cuatro detalles que importan:

  1. get_auth_plugin('ldap') devuelve una instancia ya configurada: host, cuenta de bind, contraseña, versión de protocolo y TLS salen de la administración de Moodle, no del código.
  2. ldap_find_userdn() localiza el DN del usuario usando el mismo user_attribute con el que Moodle hace login. Si el alumno puede entrar a Moodle, su DN se encuentra. Punto.
  3. ldap_read() sobre el DN concreto en vez de ldap_search() sobre una base entera: es más barato, lee un único objeto y pides solo el atributo que necesitas.
  4. ldap_close() SIEMPRE, también en cada rama de error, para no dejar conexiones colgando.

La lección

Si Moodle —o cualquier framework— ya tiene configurada una integración, reutiliza esa configuración en lugar de duplicarla.

El día que cambie el host del directorio, la cuenta de servicio o la contraseña, no hay que tocar el código del plugin ni desplegar nada: se cambia en un único sitio (la administración de auth_ldap) y todo lo demás sigue funcionando. De hecho, si lo hubiera hecho así desde el principio, la migración de OID a AD no habría roto nada: mi tarea habría seguido al nuevo directorio sola.

El bug no fue culpa de una IP que cambió, sino de haberla escrito a mano en un sitio donde no tenía por qué estar.

CompartirShare