/** * @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 {ldapConfig} from './config'; import {LDAP} from './basics'; /** * @interface searchUserFields * @desc Interface permettant la recherche d'un utilisateur avec des champs incomplets. Plusieurs valeurs sont possibles pour le même champ. * Aucun de ces champs n'est obligatoire, mais certains de ces champs doivent être exacts pour obtenir un bon résultat. * @var {string|string[]} givenName - Prénom(s) * @var {string|string[]} lastName - Nom(s) * @var {string|string[]} nickname - Surnom(s) * @var {string|string[]} nationality - Nationalité(s) (à implémenter) * @var {string|string[]} promotion - Année(s) de promo * @var {string|string[]} phone - Numéro(s) de téléphone * @var {string|string[]} mail - Adresse(s) courriel * @var {string|string[]} ip - Adresse(s) ip * @var {string|string[]} adress - Adresse(s) * @var {string} school - Ecole d'appartenance (instable, doit être exact) * @var {string|string[]} groups - Un ou plusieurs groupes dont l'utilisateur est membre (doit être exact). * @var {string} course - PA ou autre. Doit être exact. */ export interface searchUserFields { givenName: string, lastName: string, nickname: string, nationality: string, promotion: string, phone: string, mail: string, ip: string, adress: string, school: string, groups: string[], studies: string, sport: string } //------------------------------------------------------------------------------------------------------------------------ // 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 ldapConfig.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: string, return_attributes: string[]) : Promise<string[]> { // Construction du filtre custom let filter= "(|("+ldapConfig.key_id+"="+ input+")" + // On cherche la valeur exacte "(|("+ldapConfig.key_id+"=*"+input+")" + // La valeur finale avec des trucs avant ; wildcard * "(|("+ldapConfig.key_id+"=*"+input+"*)"+ // La valeur du milieu avec des trucs avant et après "("+ ldapConfig.key_id+"="+ input+"*))))"; // La valeur du début avec des trucs après // Appel rechercheLDAP avec filtre de l'espace try { return LDAP.search(ldapConfig.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 {searchUserFields} 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[]} 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: searchUserFields, return_attributes: string[]) : Promise<string[]> { let filter=""; // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans ldapConfig 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 = ldapConfig.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 avec filtre de l'espace try { return LDAP.search(ldapConfig.dn_users, return_attributes, filter); } catch(err) { throw "Erreur lors de la recherche intelligente d'un utilisateur."; } } } //------------------------------------------------------------------------------------------------------------------------ // Fonctions intermédiaires TBT //------------------------------------------------------------------------------------------------------------------------ export class Tests { /** * @class Cette classe contient des fonctions de test d'unicité trop puissantes pour être exportées tel quel. * @summary Constructeur vide. * @author hawkspar */ constructor() {} /** * @callback changeValueCallback * @param {string} id - Id à modifier * @param {number} n - Nombre d'itérations * @return {string} Nouveau id */ /** * @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 {changeValueCallback} 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: string, attribute: string, dn: string, changeValue: (string, number) => string, n=0) : Promise<string> { // Recherche d'autres occurences de l'id try { return LDAP.search(dn, ldapConfig.key_id, "("+attribute+"="+value+")").then(function (matches: string[]) { 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 Tests.ensureUnique(changeValue(value, n+1), attribute, 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: string, lastName: string, promotion: string) : Promise<string> { try { // normalize et lowerCase standardisent le format return this.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, ldapConfig.dn_users, (id: string, n: number) => { if (n==1) { id+='.'+promotion; } // Si prénom.nom existe déjà, on rajoute la promo else if (n==2) { id+='.'+(n-1).toString(); } // 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: string) : Promise<string> { try { // normalize et lowerCase standardisent le format return this.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, ldapConfig.dn_groups, (id: string, n: number) => { if (n==1) { id+='.'+n.toString(); } // Si nom existe déjà, on essaie nom.1 else if (n>1) { id+=n.toString(); } // 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 {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: string, dn: string) : Promise<string> { 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."; } } }