/** * @file Ce fichier regroupe les fonctions fondamentales aux interactions avec le LDAP. * C'est ici que tout le filtrage est opéré, au plus bas niveau. * Toutes les fonctions écrites ici sont asynchrones et renvoient des Promises ce qui nécessite de les appeler avec la synthaxe * un peu particulière `f(args).then(res => ...)` pour exploiter leur résultat. * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches. * @author hawkspar */ import ldap from 'ldapjs'; // Toutes les entrées utilisateur sont escapées par sécurité import ldapEscape from 'ldap-escape'; // Fichier de ldapConfig du ldap import {ldapConfig, credentialsLdapConfig} from './config'; // Important ; permet de vérifier que l'utilisateur reste connecté. //var ensureLoggedin = require('connect-ensure-login').ensureLoggedIn; //hawkspar->manifold : est-ce encore utile ? je ne crois pas // Connection au serveur LDAP avec des temps de timeout arbitraires var client = ldap.createClient({ url: ldapConfig.server}); interface dic { [Key: string]: string; } //------------------------------------------------------------------------------------------------------------------------ // Fonctions de base agissant sur le LDAP //------------------------------------------------------------------------------------------------------------------------ export class LDAP { /** * @class Cette classe est la brique de base du fichier tout entier puisqu'elle contient les functions qui agissent directement sur le LDAP. * @summary Constructeur vide. */ constructor() {} /** * @summary Fonction qui sert à s'identifier sur le LDAP. * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès. Méthode ldapjs * (voir [`Client API`](http://ldapjs.org/client.html) méthode bind). * @arg {string} dn - Nom de domaine ; identifiant de l'utilisateur cherchant à se connecter * @arg {string} password - Mot de passe de l'utilisateur cherchant à se connecter * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon. * @static * @async */ static async bind(dn: string, password: string) : Promise<boolean> { // Escape DN as everywhere in this file, but password is taken as is client.bind(dn, password, res => { // Gestion erreur try { res; } catch(err) { throw "Erreur lors de la connection au LDAP."; } }); // End with a boolean return true; } /** * @summary Fonction qui sert à s'identifier sur le LDAP avec plein pouvoirs. * @desc Appelle {@link bind} avec un utilisateur tout puissant. * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon. * @static * @async */ static async adminBind() : Promise<boolean> { return LDAP.bind(credentialsLdapConfig.dn, credentialsLdapConfig.password); } /** * @summary Fonction qui sert à se déconnecter du LDAP. * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès. * Fait appel à {@link LDAP.bind} avec deux champs vides. * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon. * @static * @async */ static async unbind() : Promise<boolean> { return LDAP.bind("", ""); } /** * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit une liste de valeurs trouvées. * @desc Cette fonction utilise ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode search). Cette fonction fait une demande au LDAP * qu'elle filtre selon un schéma prédéfini dans `filter` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end). * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur) * @arg {string} attribute - Attribut unique à renvoyer * @arg {string} id [null] - Identifiant facultatif pour une recherche triviale en o(1) * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape * @return {Promise(string[])} Résultats de la recherche ; soit une liste de valeurs d'attributs, * soit une liste de dictionnaires si on veut plus d'un attribut (les clés du dictionnaire sont celles du LDAP) * @static * @async */ static async searchSingle(domain: 'gr'|'us', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> { LDAP.adminBind(); let dn =""; if (id != null) { dn+=ldapConfig.key_id+'='+id+','; } if (domain == "gr") { dn+=ldapConfig.dn_groups; } else { dn+=ldapConfig.dn_users; } let vals=[]; // Interrogation LDAP selon filter client.search(ldapEscape.dn("${txt}", { txt: dn}), { "scope": "sub", "filter": ldapEscape.filter("${txt}", { txt: filter}), "attributes": [attribute] }, (err, res) => { // Gestion erreur ; pb car pas simple true / autre en sortie if (err) { throw "Erreur lors de la recherche sur le LDAP."; } else { // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse res.on('searchEntry', entry => { // Cas un seul attribut où le résultat est une liste directement vals.push(entry.object[attribute]); }); // Si la recherche renvoie une erreur, on renvoit res.on('error', resErr => { throw resErr; }); // Si la recherche est finie on se déconnecte res.on('end', _ => { LDAP.unbind(); }); } }); // On renvoit le résultat return vals; } /** * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit les valeurs trouvées. * @desc Cette fonction utilise ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode search). Cette fonction fait une demande au LDAP * qu'elle filtre selon un schéma prédéfini dans `filter` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end). * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur) * @arg {string[]} attributes - Liste des attributs qui figureront dans le résultat final ; peut aussi être un seul élément * @arg {string} id [null] - Identifiant facultatif pour une recherche triviale en o(1) * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape * @return {Promise(Array<dic>)} Résultats de la recherche ; soit une liste de valeurs d'attributs, * soit une liste de dictionnaires si on veut plus d'un attribut (les clés du dictionnaire sont celles du LDAP) * @static * @async */ static async searchMultiple(domain: 'gr'|'us', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> { LDAP.adminBind(); let dn =""; if (id != null) { dn+=ldapConfig.key_id+'='+id+','; } if (domain == "gr") { dn+=ldapConfig.dn_groups; } else { dn+=ldapConfig.dn_users; } let vals=[]; // Interrogation LDAP selon filter client.search(ldapEscape.dn("${txt}", { txt: dn}), { "scope": "sub", "filter": ldapEscape.filter("${txt}", { txt: filter}), "attributes": attributes }, (err, res) => { // Gestion erreur ; pb car pas simple true / autre en sortie if (err) { throw "Erreur lors de la recherche sur le LDAP."; } else { // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse res.on('searchEntry', entry => { // Cas plusieurs attributs donc résultat dictionnaire vals.push({}); attributes.forEach(attribute => { vals.slice(-1)[0][attribute]=entry.object[attribute]; }); }); // Si la recherche renvoie une erreur, on renvoit res.on('error', resErr => { throw resErr; }); // Si la recherche est finie on se déconnecte res.on('end', _ => { LDAP.unbind(); }); } }); // On renvoit le résultat return vals; } //TBT /** * @summary Fonction qui permet de modifier un élément sur le LDAP. Gestion intelligente de l'appartenance à un binet. * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode modify). * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur) * @arg {string} id - Identifiant unique de la feuille à modifier * @arg {"add"|"del"|"replace"} op - Operation à réaliser sur le LDAP. Trois opération sont possibles ; "add", qui rajoute des attributs et qui peut créer des doublons, * "del" qui en supprime, et "replace" qui remplace du contenu par un autre. * @arg {dic} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs. * @arg {string} mod[key] - Nouvelle valeur de l'attribut key. Une nouvelle valeur vide ("") est équivalent à la suppression de cet attribut. * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, `false` sinon. * @static * @async */ static async change(domain: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> { LDAP.adminBind(); let dn = ldapConfig.key_id+'='+id+',' if (domain == "gr") { dn+=ldapConfig.dn_groups } else { dn+=ldapConfig.dn_users } // Modification LDAP selon dn fourni en argument (pourrait prendre une liste de Changes) client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({ operation: op, modification: mod, // Gestion erreur }), err => { throw "Erreur lors d'une opération de modification sur le LDAP."; }); LDAP.unbind(); return true; } //TBT /** * @summary Fonction qui permet de rajouter un élément sur le LDAP. * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode add). * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur) * @arg {Object.<string, string>} vals - Dictionnaire contenant les valeurs à créer (contient un champ en ldapConfig.key_id) * @arg {Object} vals[key] - Nouvelle valeur pour le champ key * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon. * @static * @async */ static async add(domain: 'gr'|'us', vals) : Promise<boolean> { LDAP.adminBind(); let dn = ldapConfig.key_id+"="+vals[ldapConfig.key_id]; if (domain == "gr") { dn+=ldapConfig.dn_groups; } else { dn+=ldapConfig.dn_users; } // Ajout LDAP selon la ldapConfiguration en argument client.add(ldapEscape.dn("${txt}", { txt: dn}), vals, err => { throw "Erreur lors d'une opération d'ajout sur le LDAP."; }); LDAP.unbind(); return true; } //TBT /** * @summary Fonction qui permet de supprimer une feuille du LDAP. * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode del). * Elle est différente de modify avec "del" car elle affecte directement une feuille et pas un attribut. * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur) * @arg {string} id - Identifiant unique de la cible * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @static * @async */ static async clear(domain: 'gr'|'us', id: string) : Promise<boolean> { LDAP.adminBind(); let dn = ldapConfig.key_id+'='+id+',' if (domain == "gr") { dn+=ldapConfig.dn_groups; } else { dn+=ldapConfig.dn_users; } // Suppression LDAP client.del(ldapEscape.dn("${txt}", {txt: dn}), err => { throw "Erreur lors d'une opération de suppression sur le LDAP."; }); LDAP.unbind(); return true; } }