/** * @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. * @author hawkspar * @memberof LDAP */ // Import moche à cause mauvais typage ldapjs var ldap : any = require('ldapjs'); // Toutes les entrées utilisateur sont escapées par sécurité import ldapEscape from 'ldap-escape'; // Fichier de ldapConfig du ldap import { ldapConfig } from './config'; // Connection au serveur LDAP avec des temps de timeout arbitraires var client = ldap.createClient({ url: ldapConfig.server, tlsOptions: ldapConfig.tlsOptions }); // Interface pratique pour que Typescript comprenne ce qu'est un dictionnaire simple interface dic { [Key: string]: string | string[]; } //------------------------------------------------------------------------------------------------------------------------ // Fonctions de base agissant sur le LDAP //------------------------------------------------------------------------------------------------------------------------ export class Basics { /** * @memberof LDAP * @class Basics * @classdesc 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() {} /** * @memberof LDAP * @summary Fonction qui sert à se déconnecter du LDAP, puis s'identifier sur le LDAP avec pleins pouvoirs. * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès. * Fait appel à une méthode ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode bind). * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon. * @static */ static bind() : Promise<boolean> { return new Promise<boolean>((resolve, reject) => { // Se déconnecter dans le doute client.unbind(); // Escape DN as everywhere in this file, but password is taken as is client.bind(process.env.LDAP_DN, process.env.LDAP_PASSWD, err => { // Gestion erreur if (err instanceof ldap.LDAPError) { console.log("Erreur lors de la connection au LDAP : "+err.message); resolve(false); } resolve(true); }); }); } /** * @memberof LDAP * @callback entryHandler * @arg entry {*} - Convoluted ldap.js search result object */ /** * @memberof LDAP * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et modifie une liste pour y insérer 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 - Attributs à 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 * @arg {entryHandler} handler - Wrapper pour gérer les requêtes simples ou multiples * @return {void} Utilise handler pour gérer ses résultats au fur et à mesure * @static */ static search(domain: 'group'|'user', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<boolean> { let dn = ""; if (id != null) { if (domain == "group") dn += ldapConfig.group.gid; else dn += ldapConfig.user.uid; dn += '=' + ldapEscape.dn("${txt}", { txt: id }) + ','; } dn += ldapConfig.dn[domain]; console.log("Searching dn= " + dn + ", filter : " + filter); // Interrogation LDAP selon filter return new Promise<boolean>(function(resolve, reject) { client.search(dn, { // Must be escaped in case of a malignious false id "scope": "sub", "filter": filter, // Must be escaped in case of a malignious search arg "attributes": attributes }, (err, res) => { // Gestion erreur ; pb car pas simple true / autre en sortie if (Basics.catch(err)) { console.log("Erreur lors de la recherche sur le LDAP."); resolve(false); } else { // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse res.on('searchEntry', entry => handler(entry)); // Si la recherche renvoie une erreur (client ou TCP seulement), on renvoit res.on('error', err2 => { console.log(err2); resolve(false); }); // Quand la recherche est finie on se déconnecte res.on('end', res2 => { // Si la co avec le LDAP est tombée on relance if (res2.status != 0) Basics.bind(); resolve(true); }); } }); }); } /** * @memberof LDAP * @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 {@link LDAP.search} directement. * @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: 'group'|'user', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> { let vals=[]; await Basics.search(domain, [attribute], id, filter, entry => { // Cas un seul attribut où le résultat est une liste directement console.log("searchSingle found " + entry.object[(domain == 'group' ? ldapConfig['group']['gid'] : ldapConfig['user']['uid'])]); vals.push(entry.object[attribute]); }); return vals; } /** * @memberof LDAP * @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: 'group'|'user', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> { let vals=[]; await Basics.search(domain, attributes, id, filter, entry => { // Cas plusieurs attributs donc résultat dictionnaire vals.push({}); console.log("searchMultiple found " + entry.object[domain == 'group' ? ldapConfig.group['gid'] : ldapConfig.user['uid']]); // Suppose que tous les champs du LDAP sont définis pour les atttributs demandés attributes.forEach(attribute => vals.slice(-1)[0][attribute]=entry.object[attribute]); }); return vals; } /** * @memberof LDAP * @summary Fonction intermédiaire qui factorise la gestion d'erreur pour toute la classe. * @desc Permet de définir un comportement par défaut pour les méthodes de base add, change et clear pour différents types d'erreur. * @arg {ldap.LDAPError} err - Erreur renvoyée * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon. * @static */ static catch(err) : boolean { if (err instanceof ldap.LDAPError) { console.log("L'erreur suivante est survenue : " +err.message); // TBC if (err instanceof ldap.TimeLimitExceededError) { Basics.bind(); } else if (err instanceof ldap.ProtocolError) { Basics.bind(); } else if (err instanceof ldap.SizeLimitExceededError) { Basics.bind(); } else if (err instanceof ldap.InsufficientAccessRightsError) { Basics.bind(); } return true; } return false; } /** * @memberof LDAP * @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 ; passé par ldapEscape dans cette fonction * @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 */ static change(domain: 'group'|'user', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> { return new Promise<boolean>((resolve, reject) => { let dn = ""; if (domain == 'group') dn += ldapConfig.group.gid; else dn += ldapConfig.user.uid; dn += '='+ldapEscape.dn("${txt}", { txt: id })+','+ldapConfig.dn[domain]; // 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 => { if (Basics.catch(err)) { console.log("Erreur lors d'une opération élémentaire de modification dans le LDAP (Basics.change)"); resolve(false); } resolve(true); }); }); } /** * @memberof LDAP * @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). * On notera le rôle particulier de vals[uid/gid] qui sert à identifier la feuille à changer ; passé par ldapEscape dans cette fonction. * @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) * @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 */ static add(domain: 'group'|'user', vals) : Promise<boolean> { return new Promise<boolean>((resolve, reject) => { let dn = ""; if (domain == "group") dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.group.gid] }); else dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.user.uid] }); dn += ldapConfig.dn[domain]; // Ajout LDAP selon la ldapConfiguration en argument client.add(dn, vals, err => { if (Basics.catch(err)) { console.log("Erreur lors d'une opération élémentaire d'ajout dans le LDAP (Basics.add)"); resolve(false); } resolve(true); }); }); } /** * @memberof LDAP * @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, passé par ldapEscape dans cette fonction * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @static */ static clear(domain: 'group'|'user', id: string) : Promise<boolean> { return new Promise<boolean>((resolve, reject) => { let dn = ""; if (domain == "group") dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: id }); else dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: id }); dn += ldapConfig.dn[domain]; // Suppression LDAP client.del(dn, err => { if (Basics.catch(err)) { console.log("Erreur lors d'une opération élémentaire de suppression dans le LDAP (Basics.clear)"); resolve(false); } resolve(true); }); }); } } // Bind Basics.bind(); console.info("Binding with LDAP client completed successfully, looking good !");