Skip to content
Snippets Groups Projects
Forked from an inaccessible project.
basics.ts 15.20 KiB
/**
 * @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 !");