Forked from an inaccessible project.
-
Olivér FACKLAM authoredOlivér FACKLAM authored
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 !");