/** * @file Ce fichier regroupe les fonctions simples de recherche et de test utiles, mais trop puissantes pour être exportées directement. * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches. * @author hawkspar */ import LDAP from './basics.js'; // Essentiels pour le fichier de config import path from 'path'; import fs from 'fs'; // Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement var configPath = path.resolve('./','ldap_config.json'); var config = JSON.parse(fs.readFileSync(configPath, 'utf8')); //------------------------------------------------------------------------------------------------------------------------ // Fonctions de recherche //------------------------------------------------------------------------------------------------------------------------ export class SmartSearch { /** * @class Cette classe contient des fonctions de recherche génériques trop puissantes pour être exportées tel quel. * @summary Constructeur vide. * @author hawkspar */ constructor() {} /** * @summary Fonction qui interroge le LDAP et retrouve les groupes (voir LDAP) qui ressemblent * à l'entrée. Etape 0 vers un vrai TOL (Trombino On Line). * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. * Accepte des champs exacts ou incomplets mais pas approximatifs * et ne gère pas l'auto-complete. Cette fonction utilise aussi config.json. MEF Timeout pour * des recherches trop vagues. Renvoit une liste d'uid. * Elle utilise LDAPEscape pour éviter les injections. * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe. * @arg {string[]} return_attributes - Liste d'attributs à renvoyer dans le résultat final * @return {Promise(string[])} Liste des uid de groupes dont le nom ressemble à l'input * @static @async */ static async groups(input, return_attributes) { // Construction du filtre custom let filter= "(|("+config.key_id+"="+ input+")" + // On cherche la valeur exacte "(|("+config.key_id+"=*"+input+")" + // La valeur finale avec des trucs avant ; wildcard * "(|("+config.key_id+"=*"+input+"*)"+ // La valeur du milieu avec des trucs avant et après "("+ config.key_id+"="+ input+"*))))"; // La valeur du début avec des trucs après // Appel rechercheLDAP avec filtre de l'espace try { return LDAP.search(config.dn_groups, return_attributes, filter); } catch(err) { throw "Erreur lors de la recherche intelligente d'un groupe."; } } /** * @summary Fonction qui renvoit les attributs demandés des paxs validant les critères de recherche. Première étape vers vrai TOL (Trombino On Line). * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs * mais pas approximatifs et ne gère pas l'auto-complete. MEF Timeout pour des recherches trop vagues. Elle utilise LDAPEscape pour éviter les injections. * Utiliser trouverGroupesParTypes pour chaque champ relié à groups. * @arg {Object} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exempl pour chercher un membre * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined. * @arg {string} data[givenName] - Prénom * @arg {string} data[lastName] - Nom * @arg {string} data[nickname] - Surnom * @arg {string} data[nationality] - Nationalité (non implémentée pour l'instant, pas de format spécifique) * @arg {string} data[promotion] - String de l'année de promo * @arg {string} data[phone] - String du numéro de portable * @arg {string} data[mail] - Adresse mail * @arg {string} data[ips] - Une ou des adresses ip * @arg {string} data[school] - Ecole d'appartenance (pour l'instant instable). Doit être exact. * @arg {string} data[groups] - Un ou plusieurs groupes (pas de différence entre membre simple et admin). Doit être exact. * @arg {string} data[studies] - PA ou autre. Doit être exact. * @arg {string} data[sport] - Section sportive ou autre Doit être exact. * @arg {string[]} return_attributes - Liste d'attributs à renvoyer dans le résultat final * @return {Promise(Object[])} Liste de dictionnaires de profils en cohérence avec l'input avec pour clés les attributs des profils. * @static @async */ static async users(data, return_attributes) { let filter=""; // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans config encore for (var key in data) { if ((data[key]!= undefined) & (data[key] != '')) { // Si il y a qque chose à chercher pour ce filtre if (!Array.isArray(data[key])) { data[key]=[data[key]]; } // Gestion d'une liste de valeurs à rechercher // Iteration pour chaque valeur fournie par l'utilisateur data[key].forEach(val => { // Traduction en language LDAP let attribute = config.user[key]; // Creation incrémentale du filtre filter="(&"+filter+ "(|("+attribute+"="+ val+")"+ // On cherche la valeur exacte "(|("+attribute+"=*"+val+")"+ // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs) "(|("+attribute+"=*"+val+"*)"+ // La valeur du milieu avec des trucs avant et après "("+ attribute+"="+ val+"*)))))"; // La valeur du début avec des trucs après }); } } // Appel rechercheLDAP avec filtre de l'espace try { return LDAP.search(config.dn_users, return_attributes, filter); } catch(err) { throw "Erreur lors de la recherche intelligente d'un utilisateur."; } } } //------------------------------------------------------------------------------------------------------------------------ // Fonctions intermédiaires TBT //------------------------------------------------------------------------------------------------------------------------ export class Tests { /** * @summary Cette fonction teste une valeur d'un attribut (typiquement un identifiant) et le fait évoluer jusqu'à ce qu'il soit unique. * @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.search} pour vérifier * qu'il n'y a pas d'autres occurences de cette valeur pour cette attribut * dans le dn fourni. * @param {string} value - Valeur de l'attribut (le plus souvent un identifiant) à tester à cette itération * @param {string} attribute - Attribut à tester * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique * @param {function} changeValue - Fonction qui prend uniquement en argument l'id courant et * le nombre d'itérations et qui renvoit la prochaine valeur de l'attribut * @param {int} n [0] - Nombre d'itérations (à initialiser à 0) * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static @async */ static async ensureUnique(value, attribute, dn, changeValue, n=0) { // Recherche d'autres occurences de l'id try { return LDAP.search(dn, config.key_id, "("+attribute+"="+value+")").then(matches => { if (!matches) { throw ""; } // On renvoit la valeur si elle est bien unique else if (matches.length==0) { return value; } // Sinon, on tente de nouveau notre chance avec la valeur suivante else { return this.ensureUnique(changeValue(value, n+1), dn, changeValue, n+1); } }); } catch(err) { throw "Erreur lors de la recherche d'une valeur pour assurer son unicité."; } } /** * @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique. * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation). * @param {string} givenName - Prénom * @param {string} lastName - Nom * @param {string} promotion - Année de promotion * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static @async */ static async generateUid(givenName, lastName, promotion) { try { // normalize et lowerCase standardisent le format return this.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), config.key_id, config.dn_users, (id,n) => { if (n==1) { id+='.'+promotion; } // Si prénom.nom existe déjà, on rajoute la promo else if (n==2) { id+='.'+n-1; } // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1 else if (n>2) { id+=n; } // Ensuite on continue .123, .1234, etc... return id; }); } catch(err) { throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid)."; } } /** * @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique. * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation). * @param {string} name - Nom * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static @async */ static async generateReadableId(name) { try { // normalize et lowerCase standardisent le format return this.ensureUnique(name.toLowerCase().normalize('UFD'), config.key_id, config.dn_groups, (id,n) => { if (n==1) { id+='.'+n; } // Si nom existe déjà, on essaie nom.1 else if (n>1) { id+=n; } // Ensuite on continue .12, .123, etc... return id; }); } catch(err) { throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid)."; } } /** * @summary Cette fonction teste une valeur dummy (0) pour un identifiant numérique puis le fait évoluer aléatoirement (entre 1 et 100 000) jusqu'à ce qu'il soit unique. * @param {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}. * @param {string} attribut - Intitulé exact de l'id concerné * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static @async */ static async generateId(attribut, dn) { try { return this.ensureUnique("0", attribut, dn, (id,n) => { return Math.floor((Math.random() * 100000) + 1).toString(); }); } catch(err) { throw "Erreur lors de l'assurance de l'unicité d'un unique identifier numérique."; } } }