/** * @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'; import { userData } from './user'; import { groupData } from './group'; //------------------------------------------------------------------------------------------------------------------------ // Fonctions intermédiaires TBT //------------------------------------------------------------------------------------------------------------------------ export class Tools { /** * @class Cette classe contient des fonctions intermédiaires qui ne sont pas destinées à être utilisées dans les resolvers. * @summary Constructeur vide. * @author hawkspar */ constructor() {} /** * @summary Fonction qui renvoit toutes les infos relatives à un groupe ou un utilisateur particulier. * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis. * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData}) * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe) * @arg {string} id - Identifiant de la feuille cherchée (uid ou gid) * @return {Promise(T)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe tel que défini par le paramètre T. * @static * @async */ static async genericPeek<T>(domain: 'us'|'gr', id: string) : Promise<T> { if (domain='gr') { var dirtyKeys = ldapConfig.group; } else { var dirtyKeys = ldapConfig.user; } let cleanData : T; let dirtyData = await LDAP.search(domain, dirtyKeys.values(), id); // Rename output for (let uncleanKey in dirtyData) { for (let cleanKey in cleanData) { if (uncleanKey=dirtyKeys[cleanKey]) { cleanData[cleanKey] = dirtyData[uncleanKey]; } } } return cleanData; } /** * @summary Fonction qui retrouve les id des paxs ou groupes validant les critères de recherche. Etape vers vrai TOL (Trombino On Line). * Utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos. * @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. Va crasher si un champ n'est pas dans ldapConfig. * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData}) * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe) * @arg {userData | groupData} 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 exemple pour chercher un membre * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined. * @return {Promise(string[])} ids des profils qui "match" les critères proposés. * @static * @async */ static async genericSearch(domain : "us"|"gr", data : userData|groupData) : 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]]; } // Génération systématique d'une liste de valeurs à rechercher // Iteration pour chaque valeur fournie par l'utilisateur data[key].forEach(val => { // Traduction en language LDAP let attribute = ""; if (domain="us") { attribute = ldapConfig.user[key]; } else { attribute = ldapConfig.group[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 return LDAP.search(domain, [ldapConfig.key_id], null, filter); } /** * @summary Fonction qui édite un groupe existant dans le LDAP. * @desc Appelle {@link LDAP.change}. * @arg {userData | groupData} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels... * Sauf 'gid', qui permet de savoir quel groupe modifier et qui est donc inchangeable. On peut modifier nickname par contre. * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async * @static */ static async genericEdit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> { if (domain = "us") { var id=data['uid']; var dirtyKeys=ldapConfig.user; } else { var id=data['gid']; var dirtyKeys=ldapConfig.group; } // Renommage LDAP-friendly let dirtyData = {}; Object.keys(data).forEach(function(key: string) { if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) { dirtyData[dirtyKeys.key]=data[key]; } }); return LDAP.change(domain,id,"replace",dirtyData); } /** * @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 {"gr"|"us"} domain - Domaine 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, domain: 'gr'|'us', changeValue: (string, number) => string, n=0) : Promise<string> { // Recherche d'autres occurences de l'id try { return LDAP.search(domain, [ldapConfig.key_id], null, "("+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 Tools.ensureUnique(changeValue(value, n+1), attribute, domain, 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 Tools.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 Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "us", (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 Tools.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 Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "gr", (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 {"gr"|"us"} domain - Domaine 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, domain: "gr"|"us") : Promise<string> { try { return Tools.ensureUnique("0", attribut, domain, (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."; } } /** * @summary Fonction qui retrouve les groupes dont un individu est membre. * @desc Cette fonction utilise {@link LDAP.search} va directement à la feuille de l'utilisateur. * @arg {string} uid - Identifiant de l'individu à interroger (le plus souvent prenom.nom, parfois l'année, supposé valide) * @return {Promise(string[])} Liste des uid de groupes (noms flat des groupes) où l'id fourni est membre * @static * @async */ static async getGroups(uid: string) : Promise<string[]> { try { return LDAP.search("us", [ldapConfig.user.groups], uid)[0]; } catch(err) { throw "Erreur lors de la recherche des groupes d'un individu."; } } /** * @summary Fonction qui retrouve la liste des membres d'un groupe. * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json. * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule) * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes) * @static * @async */ static async getMembers(gid: string) : Promise<string[]> { try { return LDAP.search("gr", [ldapConfig.group.members], gid)[0]; } catch(err) { throw "Erreur lors de la recherche des membres d'un groupe."; } } /** * @summary Fonction qui retrouve la liste des admins d'un groupe. * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json. * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule) * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes) * @static * @async */ static async getAdmins(gid: string) : Promise<string[]> { try { return LDAP.search("gr", [ldapConfig.group.admins], gid)[0]; } catch(err) { throw "Erreur lors de la recherche des admins d'un groupe."; } } /** * @summary Cette fonction teste si un utilisateur est membre d'un groupe. * @desc Utilise les méthodes statiques {@link open.getGroups} et {@link open.getMembers} * @param {string} uid - Identifiant de l'utilisateur à tester * @param {string} gid - Identification du groupe à tester * @returns {Promise(boolean)} True si l'utilisateur est membre * @static * @async */ static async isGroupMember(uid: string, gid: string) : Promise<boolean> { try { let lg = await Tools.getGroups(uid); let lm = await Tools.getMembers(gid); if (lg.includes(gid) && lm.includes(uid)) { return true; } else { return false; } } catch(err) { throw "Erreur lors du test d'appartenance à un groupe."; } } /** * @summary Cette fonction teste si un utilisateur est admin d'un groupe. * @desc Utilise la méthode statique {@link Open.getAdmins} * @param {string} uid - Identifiant de l'utilisateur à tester * @param {string} gid - Identification du groupe à tester * @returns {Promise(boolean)} True si l'utilisateur est administrateur * @static * @async */ static async isGroupAdmin(uid: string, gid: string) : Promise<boolean> { try { let lm = await Tools.getMembers(gid); let la = await Tools.getAdmins(gid); if (la.includes(uid) && lm.includes(uid)) { return true; } else { return false; } } catch(err) { throw "Erreur lors du test d'appartenance au bureau d'administration un groupe."; } } }