/** * @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 */ 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'; // Connection au serveur LDAP avec des temps de timeout arbitraires var client = ldap.createClient({ url: ldapConfig.server}); // 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 { /** * @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. * @memberof LDAP * @summary Constructeur vide. */ constructor() {} /** * @memberof LDAP * @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; } /** * @memberof LDAP * @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 Basics.bind(credentialsLdapConfig.dn, credentialsLdapConfig.password); } /** * @memberof LDAP * @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 Basics.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 Basics.bind("", ""); } /** * @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 * @async */ static search(domain: 'group'|'user', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<void> { Basics.adminBind(); 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 let promise = new Promise<void>(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 (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 => handler(entry)); // Si la recherche renvoie une erreur, on renvoit res.on('error', resErr => { throw resErr; }); // Quand la recherche est finie on se déconnecte res.on('end', _ => { Basics.unbind(); resolve(); }); } }); }); return promise; } /** * @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'])]); attributes.forEach(attribute => { vals.slice(-1)[0][attribute]=entry.object[attribute]; }); }); return vals; } /** * @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 * @async */ static async change(domain: 'group'|'user', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> { Basics.adminBind(); 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 => { throw "Erreur lors d'une opération de modification sur le LDAP."; }); Basics.unbind(); return 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 * @async */ static async add(domain: 'group'|'user', vals) : Promise<boolean> { Basics.adminBind(); 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(ldapEscape.dn("${txt}", { txt: dn }), vals, err => { throw "Erreur lors d'une opération d'ajout sur le LDAP."; }); Basics.unbind(); return 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 * @async */ static async clear(domain: 'group'|'user', id: string) : Promise<boolean> { Basics.adminBind(); 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(ldapEscape.dn("${txt}", {txt: dn}), err => { throw "Erreur lors d'une opération de suppression sur le LDAP."; }); Basics.unbind(); return true; } }