From d9b2d988714a648bc2521c0b95637dd02ae582c5 Mon Sep 17 00:00:00 2001 From: Quentin CHEVALIER <quentin.chevalier@polytechnique.edu> Date: Wed, 4 Jul 2018 23:58:19 +0200 Subject: [PATCH] General clean-up of ldap_data TBT --- src/ldap/admins.js | 341 ++++++++++++++++++++++++++++++++++++++++++ src/ldap/basics.js | 258 +++++++++++++++++--------------- src/ldap/users.js | 304 +++++++++++++++++++++++++++++++++++++ src/ldap/utilities.js | 201 +++++++++---------------- 4 files changed, 858 insertions(+), 246 deletions(-) create mode 100644 src/ldap/admins.js create mode 100644 src/ldap/users.js diff --git a/src/ldap/admins.js b/src/ldap/admins.js new file mode 100644 index 0000000..1c4fe93 --- /dev/null +++ b/src/ldap/admins.js @@ -0,0 +1,341 @@ +/** + * @file Ce fichier regroupe les différentes classes avec différents admins. Ces classes sont dédiées à être exportées directement pour être utilisées par le solver. + * @author hawkspar + */ + +import LDAP from './basics'; +import {SmartSearch, Tests} from './utilities'; +import User from './users'; +// Essentiels pour le fichier de config +import path from 'path'; +import fs from 'fs'; + +// Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement +var configPath = path.resolve('./','ldap_config.json'); +var config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + +class Admin extends User { + /** + * @class Cette classe est la classe de l'administrateur d'un groupe qui lui permet de rajouter des membres, en supprimer, idem pour des admins, + * ou éditer, voir supprimer le groupe. + * @summary Ce constructeur appelle simplement le constructeur de sa classe mère. + * @arg {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}. + * @author hawkspar + */ + constructor(user) { super(user); } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions de relation TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui permet de rajouter un membre déjà créé à un groupe. + * @desc Cette fonction fait essentiellement appel à {@link LDAP.modifier} et {@link listerGroupes}. Elle n'autorise pas les doublons et opère dans les deux dns users + * et groups. + * @arg {string} uid - Identifiant du futur membre + * @arg {string} gid - Identifiant du groupe + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + static addGroupMember(uid, gid) { + // Vérifie que l'utilisateur est pas déjà membre pour groupes + return this.getMembers(gid).then(lm => { + if (!lm.includes(uid)) { + let vals = {}; + vals[config.groups.member] = uid; + LDAP.change(config.key_id+gid+config.dn_groups, "add", vals).then(res => { + // Erreur si pb lors de la modification + if (!res) { throw Error; } + }); + } + // Vérifie que l'utilisateur est pas déjà membre pour users + }).then(res => this.getGroups(uid)).then( lg => { + if (!lg.includes(gid)) { + let vals2 = {}; + vals2[config.users.groups] = gid; + LDAP.change(config.key_id+uid+config.dn_users, "add", vals2).then(res => { + // Erreur si pb lors de la modification + if (!res) { throw Error; } else { return true; } + }); + } + }); + } + + /** + * @summary Fonction qui permet de supprimer un membre existant d'un groupe. + * @desc Cette fonction fait essentiellement appel à {@link LDAP.search}, {@link LDAP.change}, {@link Open.getGroups} et {@link Open.getMembers}. + * @arg {string} uid - Identifiant de l'ex-membre + * @arg {string} gid - Identifiant du groupe + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + static delGroupMember(uid, gid) { + // Vérifie que l'utilisateur est pas déjà viré pour groupes + return this.getMembers(gid).then(lm => { + if (lm.includes(uid)) { + // Supprime tous les utilisateurs + LDAP.change(config.key_id+gid+config.dn_groups, "del", config.group.member).then(res => { + // Les rajoute un par un, sauf pour le supprimé + lm.forEach(id => { + if (id!=uid) { this.addGroupMember(id, gid); } + }); + }); + } + }).then(res => this.getGroups(uid)).then(lg => { + // Vérifie que l'utilisateur est pas déjà viré pour users + if (lg.includes(gid)) { + // Supprime tous les groupes + LDAP.change(config.key_id+uid+config.dn_users, "del", config.member.groups).then(res => { + // Les rajoute un par un, sauf pour le supprimé + lg.forEach(id => { + if (id!=gid) { this.addGroupMember(uid, id).then(res => { if (res) { return true; } else { throw Error; }}); } + }); + }); + } + }); + } + + /** + * @summary Fonction qui permet de promouvoir membre au stade d'administrateur d'un groupe. + * @desc Cette fonction fait essentiellement appel à {@link Admin.addGroupMember} {@link LDAP.change} et {@link Open.getAdmins}. Elle n'autorise pas + * les doublons et opère dans les deux dns users et groups. + * @arg {string} uid - Identifiant du futur membre + * @arg {string} gid - Identifiant du groupe + * @return {boolean} `true` si la modification s'est bien déroulée, false sinon + */ + static addGroupAdmin(uid, gid){ + // Ajoute le membre au groupe avant d'en faire un admin + return this.addGroupMember(uid,gid).then(res => { + this.getAdmins(gid).then(la => { + if (!la.includes(uid)) { + // Finalement modification, uniquement dans groups + let vals = {}; + vals[config.groups.admin] = uid; + LDAP.change(config.key_id+gid+config.dn_groups, "add", vals).then(res => { + // Gestion d'erreur + if (!res) { throw Error; } else { return true; } + }); + } + }); + }); + } + + /** + * @summary Fonction qui permet de rétrograder un membre du stade d'administrateur d'un groupe au stade d'utilisateur. + * @desc Cette fonction fait essentiellement appel à {@link LDAP.change}. + * @arg {string} uid - Identifiant du futur membre + * @arg {string} gid - Identifiant du groupe + * @return {boolean} `true` si la modification s'est bien déroulée, false sinon + */ + delGroupAdmin(uid, gid) { + // Peut paraître absurde mais permet de s'assurer que le membre est bien présent et que ses champs sont comme il faut + return this.delGroupMember(uid, gid).then( res => { this.addGroupMember(uid,gid).then( res => + // Vérifie que l'utilisateur est bien admin (comme dans delGroupMember) + this.getAdmins(gid).then(la => { + if (la.includes(uid)) { + // Supprime tous les administrateurs + LDAP.change(config.key_id+gid+config.dn_groups, "del", config.group.admin).then(res => { + // Les rajoute un par un, sauf pour le supprimé + la.forEach(id => { + if (id!=uid) { this.addGroupAdmin(id, gid); } + }); + }); + } + }).then(res => { return true; })); + }); + } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonction d'édition TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui édite un groupe existant dans le LDAP. Très similaire à {@link User.addGroup} + * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link Admin.addGroupMember} et {@link Admin.addGroupAdmin} en godmode pour gérer les groupes du nouvel utilisateur. + * @arg {string} gid - Identifiant du groupe à modifier + * @arg {Object} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels. + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + editGroup(gid, data) { + // Récupération des anciennes données + return this.peekGroup(gid).then(profil => { + // Reecriture de profil avec les bons champs + Object.keys(profil).forEach(keyLDAP => { + Object.keys(config.group).forEach(keyAlias => { + config.group[keyAlias]=keyLDAP; + profil[keyAlias]=profil[keyLDAP]; + }); + }); + // Surcharge des champs à modifier selon data + Object.keys(data).forEach(key => { + profil[key]=data[key]; + }); + // Modification propre + this.delGroup(gid).then(r => { + this.addGroup(profil).then(res => { if (!res) { throw Error; } else { return true; }}); + }); + }); + } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions de suppression TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui supprime un groupe du LDAP. + * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement. + * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant. + * @arg {string} gid - gid de la victime + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + delGroup(gid) { + // Gestion des membres et administrateurs d'abord + return this.constructor.peekGroup(gid).then(profil => { + // Ordre important + profil[config.group['admin']].forEach( id => { this.delGroupAdmin( id, gid); }); + profil[config.group['member']].forEach(id => { this.delGroupMember(id, gid); }); + // Elimination + }).then(res => { + LDAP.clear(config.key_id+"="+gid+","+config.dn_groups); + }); + } +} + +class SuperAdmin extends Admin { + /** + * @class Cette classe est la classe du super administrateur qui créé et supprime des membres. + * @summary Bête appel au constructeur de la classe mère. + * @arg {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}. + * @author hawkspar + */ + constructor(user) { super(user); } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions de création TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui créé un nouvel utilisateur dans le LDAP. + * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link User.addGroupMember} et {@link Admin.addGroupAdmin} pour gérer les groupes du nouvel utilisateur. + * @arg {Object} data - Dictionnaire des informations utilisateurs. Des erreurs peuvent apparaître si tous les champs ne sont pas remplis. + * @arg {string} data[givenName] - Prénom + * @arg {string} data[lastName] - Nom + * @arg {string} data[nickname] [] - Surnom + * @arg {string} data[birthdate] - Date d'anniversaire au format annee-mois-jour + * @arg {string} data[password] - Mot de passe + * @arg {string} data[promotion] - Simple année d'entrée école (pas d'X17) + * @arg {string} data[mail] - Courriel supposé valide + * @arg {string} data[phone] - String du numéro de portable + * @arg {string} data[photo] - Photo jpeg directement en bytestring + * @arg {string} data[room] - Pour l'instant juste le numéro de casert ou rien, à terme l'adresse complète + * @arg {string} data[readPerm] [] - Permissions spéciales BR sous la forme "*truc," + * @arg {string} data[writePerm] [] - Permissions spéciales BR sous la forme "*truc," + * @arg {string[]} data[forlifes] [] - Liste d'alias spéciaux BR (attention le filtre .fkz n'est plus fonctionnel) + * @arg {string[]} data[ips] - Liste des ips connus de la personne + * @arg {string[]} data[groups] [] - Liste des gid dont le pax est membre (doit contenir "on_platal" si il y a lieu) + * @arg {string[]} data[groupsIsAdmin] [] - Liste des gid dont le pax est admin ; supposé sous-liste du précédent + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + addUser(data) { + // Calcul d'un dictionnaire d'ajout + let vals = {}; + + // uid de base généré à partir de nom et prénom, plus potentiellement promo et un offset + // MEF mélange de Promise et de fonction standard + Tests.generateUid(data['givenName'],data['lastName'],data['promotion']).then(id => { vals[config.key_id]=id; } ); + + // Ecriture de toutes les valeurs directement inscrites dans le LDAP (in pour input) + // Génère une erreur si un champ n'est pas rempli + config.user.direct_input.forEach(key_att => vals[config.user[key_att]]=data[key_att]); + + // Appel à la fonction de base + return LDAP.add(config.key_id+"="+vals[config.key_id]+","+config.dn_users, vals).then( res => { + // Modifications multiples pour avoir plusieurs champs de même type ; boucle sur les attributs multiples (d'où mul) + config.user.muliple_input.forEach(key_att => { + // On rajoute chaque valeur en entrée + data[key_att].forEach(val => { + let vals2 = {}; + vals2[config.user[key_att]]=val; + LDAP.change(config.key_id+"="+vals[config.key_id]+","+config.dn_users, "add", vals2).then(res => { + if (!res) { throw Error; } + }); + }); + }); + + // Certains champs nécessitent de petits calculs + let vals3={}; + + // Création d'un nom complet lisible + vals3[config.user['fullName']]=data['givenName']+' '+data['lastName'].toUpperCase(); + // ?! + vals3[config.user['password']] = '{CRYPT}' + data['password']; + // Ecriture d'un surnom s'il y a lieu + if ((data['nickname']!=undefined)&(data['nickname']!='')) { + vals3[config.user['nickname']]=data['nickname']; + } + // Génération id aléatoire unique + Tests.generateId(config.user['id'], config.dn_users).then(id => { vals3[config.user['id']]=id; }); + + // Stockage machine ; dépend du prénom + vals3[config.user['directory']] = '/hosting/users/' + data['givenName'][0]; + + // Code root + vals3[config.user['cleanFullName']]=data['fullName'].replace(':', ';').toLowerCase().normalize('UFD'); + + // Adressage root + if (data['groups'].includes("on_platal")) { vals3[config.user['login']] = "/bin/bash"; } + else { vals3[config.user['login']] = "/sbin/nologin"; } + + // Permissions BR + vals3[config.user['readPerm']] = 'br.*,public.*'; + if (data['readPerm'].length>0) { vals3[config.user['readPerm']] += ',' + data['readPerm']; } + vals3[config.user['writePerm']] = 'br.*,!br.blague-du-jour,public.*,!br.campagnekes'; + if (data['writePerm'].length>0) { vals3[config.user['readPerm']] += ',' + data['writePerm']; } + + // Valeur nécessaire mais inutile + vals3[config.user['idNum']] ='5000'; + + // Inscription des valeurs calculées + LDAP.change(config.key_id+"="+vals[config.user['hruid']]+","+config.dn_users, "add", vals3).then(res => { + if (!res) { throw Error; } + }); + + ["posixAccount", "shadowAccount", "inetOrgPerson", "brAccount"].forEach(cst => { + let val3={}; + vals3[config.user['class']]=cst; + LDAP.change(config.key_id+"="+vals[config.user['hruid']]+","+config.dn_users, "add", vals3).then(res => { + if (!res) { throw Error; } + }); + }); + + // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble + data['groupsIsMember'].forEach(gid => { this.addGroupMember(vals[config.key_id], gid); }); + data['groupsIsAdmin'].forEach(gid => { this.addGroupAdmin( vals[config.key_id], gid); }); + + return true; + }); + } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions de suppression TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui supprime un utilisateur du LDAP. + * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement. + * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant. + * @arg {string} uid - uid de la victime + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + delUser(uid) { + // Gestion des groupes d'abord + return super.peekUser(uid).then(profil => { + profil[config.user['groups']].forEach(gid => { + this.isGroupAdmin(gid).then(res => { + this.delGroupAdmin(uid, gid); + }).then(res => { this.delGroupMember(uid, gid); }); + }); + // Elimination + }).then(res => { this.LDAP.clear(config.key_id+"="+uid+","+config.dn_users); }); + } +} + +export {Admin, SuperAdmin}; \ No newline at end of file diff --git a/src/ldap/basics.js b/src/ldap/basics.js index 8384bd8..ae880b0 100644 --- a/src/ldap/basics.js +++ b/src/ldap/basics.js @@ -7,9 +7,11 @@ */ import ldap from 'ldapjs'; -import fs from 'fs'; +// Toutes les entrées utilisateur sont escapées par sécurité import ldapEscape from 'ldap-escape'; +// Essentiels pour le fichier de config import path from 'path'; +import fs from 'fs'; // Important ; permet de vérifier que l'utilisateur reste connecté. var ensureLoggedin = require('connect-ensure-login').ensureLoggedIn; @@ -25,129 +27,151 @@ var client = ldap.createClient({ url: config.ldap.server}); // Fonctions de base agissant sur le LDAP //------------------------------------------------------------------------------------------------------------------------ -/** - * @summary Fonction qui sert à s'identifier sur le LDAP. Ne renvoie rien. - * @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 {Object} user - Utilisateur de la forme suivante : - * @arg {string} user[uid] - User identifier - * @arg {string} user[password] - Mot de passe - */ -function bind(user) { client.bind(ldapEscape.dn("${txt}", { txt: user["dn"]}), ldapEscape.filter("${txt}", { txt: user["password"]}), (err, res) => {}); } // TBM utiliser user +class LDAP { + /** + * @class Cette classe est la brique de base du fichier tout entier puisqu'elle contient les functions qui agisse directement sur le LDAP. + * @summary Constructeur vide. + * @author hawkspar + */ + constructor() {} -/** - * @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). Il faut l'appeler suivant un schéma - * `rechercher(...).then((res) => { truc avec res });`. Cette fonction fait une demande au LDAP qu'elle filtre selon un schéma prédéfini dans `dic` - * et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end). - * @arg {string} dn - DN de l'emplacement de la requête - * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape - * @arg {string} filter_dic[key] - Vraie valeur pertinente de la recherche - * @arg {string[]} attributes - Liste des attributs qui figureront dans le résultat final ; peut aussi être un seul élément - * @return {Promise(Object[])} 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) - */ -function search(dn, attributes, filter="(objectClass=*)") { - return new Promise((resolve, reject) => { - let vals=[]; - // Interrogation LDAP selon configuration fournie en argument - client.search(ldapEscape.dn("${txt}", { txt: dn}), { - "scope": "sub", - "filter": ldapEscape.filter("${txt}", { txt: filter}), - "attributes": attributes - }, function(err, res) { - // Gestion erreur - if (err) { - reject(err); - } else { - // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse - res.on('searchEntry', function(entry) { - // Cas un seul attribut où le résultat est une liste directement - if (!Array.isArray(attributes)) { vals.push(entry.object[attributes]); } - else if (attributes.length == 1) { vals.push(entry.object[attributes[0]]); } - // Cas plusieurs attributs donc résultat dictionnaire - else { - vals.push({}); - attributes.forEach((attribute) => { - vals.slice(-1)[0][attribute]=entry.object[attribute]; - }); - } - }); - // Si la recherche est finie on se déconnecte et on renvoit la liste - res.on('end', function(res) { resolve(vals); }); - } + /** + * @summary Fonction qui sert à s'identifier sur le LDAP. Ne renvoie rien. + * @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 {Object} user - Utilisateur de la forme suivante : + * @arg {string} user[uid] - User identifier + * @arg {string} user[password] - Mot de passe + * @static + */ + static bind(user) { client.bind(ldapEscape.dn("${txt}", { txt: user["dn"]}), ldapEscape.filter("${txt}", { txt: user["password"]}), (err, res) => {}); } // TBM utiliser user + + /** + * @summary Fonction qui sert à se déconnecter du LDAP. Ne renvoie rien. + * @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). + * @static + */ + static unbind() { client.bind("", "", (err, res) => {}); } + + /** + * @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). Il faut l'appeler suivant un schéma + * `rechercher(...).then((res) => { truc avec res });`. Cette fonction fait une demande au LDAP qu'elle filtre selon un schéma prédéfini dans `dic` + * et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end). + * @arg {string} dn - DN de l'emplacement de la requête + * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape + * @arg {string} filter_dic[key] - Vraie valeur pertinente de la recherche + * @arg {string[]} attributes - Liste des attributs qui figureront dans le résultat final ; peut aussi être un seul élément + * @return {Promise(Object[])} 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 + */ + static search(dn, attributes, filter="(objectClass=*)") { + return new Promise((resolve, reject) => { + let vals=[]; + // Interrogation LDAP selon configuration fournie en argument + client.search(ldapEscape.dn("${txt}", { txt: dn}), { + "scope": "sub", + "filter": ldapEscape.filter("${txt}", { txt: filter}), + "attributes": attributes + }, function(err, res) { + // Gestion erreur + if (err) { + reject(err); + } else { + // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse + res.on('searchEntry', function(entry) { + // Cas un seul attribut où le résultat est une liste directement + if (!Array.isArray(attributes)) { vals.push(entry.object[attributes]); } + else if (attributes.length == 1) { vals.push(entry.object[attributes[0]]); } + // Cas plusieurs attributs donc résultat dictionnaire + else { + vals.push({}); + attributes.forEach((attribute) => { + vals.slice(-1)[0][attribute]=entry.object[attribute]; + }); + } + }); + // Si la recherche est finie on se déconnecte et on renvoit la liste + res.on('end', function(res) { resolve(vals); }); + } + }); }); - }); -} + } -//TBT -/** - * @summary Fonction qui permet de modifier un élément sur le LDAP. Gestion intelligente de l'appartenance à un binet. - * @desc Cette fonction utilise une Promise pour être asynchrone ; elle renvoit la promesse d'une réponse puis traite la demande avec ldapjs - * (voir [`Client API`](http://ldapjs.org/client.html) méthode modify). Il faut l'appeler suivant un schéma `LDAP.modifier(...).then((res) => { truc avec res });`. - * @arg {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.connecter} - * @arg {string} dn - DN de l'endroit à modifier - * @arg {string} 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 {Object} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs. 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. - */ -function change(dn, op, mod) { - return new Promise((resolve, reject) => { - // Modification LDAP selon configuration en argument (pourrait prendre une liste de Changes) - client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({ - operation: op, - modification: mod, - }), err => { - reject(err); + //TBT + /** + * @summary Fonction qui permet de modifier un élément sur le LDAP. Gestion intelligente de l'appartenance à un binet. + * @desc Cette fonction utilise une Promise pour être asynchrone ; elle renvoit la promesse d'une réponse puis traite la demande avec ldapjs + * (voir [`Client API`](http://ldapjs.org/client.html) méthode modify). Il faut l'appeler suivant un schéma `LDAP.modifier(...).then((res) => { truc avec res });`. + * @arg {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.connecter} + * @arg {string} dn - DN de l'endroit à modifier + * @arg {string} 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 {Object} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs. 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(dn, op, mod) { + return new Promise((resolve, reject) => { + // Modification LDAP selon configuration en argument (pourrait prendre une liste de Changes) + client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({ + operation: ldapEscape.dn("${txt}", {txt: op}), + modification: mod, + }), err => { + reject(err); + }); + resolve(true); }); - resolve(true); - }); -} + } -//TBT -/** - * @summary Fonction qui permet de rajouter un élément sur le LDAP. - * @desc Cette fonction appelle {@link LDAP.connecter} pour authentifier l'utilisateur puis rompts la connexion. Cette fonction utilise une Promise pour être asynchrone ; - * elle renvoit la promesse d'une réponse puis traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode add). - * Il faut l'appeler suivant un schéma `LDAP.ajouter(...).then((res) => { truc avec res });`. - * @arg {Object} user - Utilisateur de la forme nécessaire au {@linkoperationsLDAP. connecterLDAP} - * @arg {string} dn - Adresse du parent - * @arg {Object} vals - Dictionnaire contenant les valeurs à créer - * @arg {string} vals[key] - Nouvelle valeur pour le champ key - * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon. - */ -function add(dn, vals) { - return new Promise((resolve, reject) => { - // Ajout LDAP selon la configuration en argument - client.add(ldapEscape.dn(config.key_id+"="+vals[config.key_id]+",${txt}", { txt: dn}), vals, function(err) { - reject(err); + //TBT + /** + * @summary Fonction qui permet de rajouter un élément sur le LDAP. + * @desc Cette fonction appelle {@link LDAP.connecter} pour authentifier l'utilisateur puis rompts la connexion. Cette fonction utilise une Promise pour être asynchrone ; + * elle renvoit la promesse d'une réponse puis traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode add). + * Il faut l'appeler suivant un schéma `LDAP.ajouter(...).then((res) => { truc avec res });`. + * @arg {Object} user - Utilisateur de la forme nécessaire au {@linkoperationsLDAP. connecterLDAP} + * @arg {string} dn - Adresse du parent + * @arg {Object} vals - Dictionnaire contenant les valeurs à créer + * @arg {string} 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(dn, vals) { + return new Promise((resolve, reject) => { + // Ajout LDAP selon la configuration en argument + client.add(ldapEscape.dn(config.key_id+"="+vals[config.key_id]+",${txt}", { txt: dn}), vals, function(err) { + reject(err); + }); + resolve(true); }); - resolve(true); - }); -} + } -//TBT -/** - * @summary Fonction qui permet de supprimer une feuille du LDAP. - * @desc Cette fonction appelle {@link LDAP.connecter} pour authentifier l'utilisateur puis rompts la connexion. Cette fonction utilise une Promise pour être asynchrone ; - * elle renvoit la promesse d'une réponse puis traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode del). - * Il faut l'appeler suivant un schéma `LDAP.supprimer(...).then((res) => { truc avec res });`. Cette fonction fait une demande au LDAP qu'elle filtre selon un - * schéma prédéfini dans `dic` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end). - * Elle est différente de modify avec "del" car elle affecte directement une feuille et pas un attribut. - * @arg {Object} user - Utilisateur de la forme nécessaire au {@link LDAP.connecter} - * @arg {string} dn - Adresse de la cible - * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon - */ -function clear(dn) { - return new Promise((resolve, reject) => { - // Suppression LDAP - client.del(ldapEscape.dn("${txt}", {txt: dn}), function(err) { - reject(err); + //TBT + /** + * @summary Fonction qui permet de supprimer une feuille du LDAP. + * @desc Cette fonction appelle {@link LDAP.connecter} pour authentifier l'utilisateur puis rompts la connexion. Cette fonction utilise une Promise pour être asynchrone ; + * elle renvoit la promesse d'une réponse puis traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode del). + * Il faut l'appeler suivant un schéma `LDAP.supprimer(...).then((res) => { truc avec res });`. Cette fonction fait une demande au LDAP qu'elle filtre selon un + * schéma prédéfini dans `dic` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end). + * Elle est différente de modify avec "del" car elle affecte directement une feuille et pas un attribut. + * @arg {Object} user - Utilisateur de la forme nécessaire au {@link LDAP.connecter} + * @arg {string} dn - Adresse de la cible + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + * @static + */ + static clear(dn) { + return new Promise((resolve, reject) => { + // Suppression LDAP + client.del(ldapEscape.dn("${txt}", {txt: dn}), function(err) { + reject(err); + }); + resolve(true); }); - resolve(true); - }); + } } -export {bind, search, change, add, clear}; \ No newline at end of file +export {LDAP}; \ No newline at end of file diff --git a/src/ldap/users.js b/src/ldap/users.js new file mode 100644 index 0000000..6e0e1b4 --- /dev/null +++ b/src/ldap/users.js @@ -0,0 +1,304 @@ +/** + * @file Ce fichier regroupe les différentes classes avec différents utilisateurs. Ces classes sont dédiées à être exportées directement pour être utilisées par le solver. + * @author hawkspar + */ + +import LDAP from './basics'; +import {SmartSearch, Tests} from './utilities'; +import {Admin, SuperAdmin} from './admins'; +// Essentiels pour le fichier de config +import path from 'path'; +import fs from 'fs'; + +// Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement +var configPath = path.resolve('./','ldap_config.json'); +var config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + +//------------------------------------------------------------------------------------------------------------------------ +// Classes à exporter TBT +//------------------------------------------------------------------------------------------------------------------------ + +class Open { + /** + * @class Cette classe est la classe exportable de base permettant à un utilisateur non connecté de faire des petites recherches simples. + * @summary Constructeur vide. + * @author hawkspar + */ + constructor(config) {} + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions de lecture + //------------------------------------------------------------------------------------------------------------------------ + /** + * @summary Fonction qui retrouve les groupes dont un individu est membre. + * @desc Cette fonction utilise {@link LDAP.search} va directement à la feuille de l'utilisateur. + * @arg {string} uid - Identifiant de l'individu à interroger (le plus souvent prenom.nom, parfois l'année, supposé valide) + * @return {Promise(string[])} Liste des uid de groupes (noms flat des groupes) où l'id fourni est membre + * @static + */ + static getGroups(uid) { + return LDAP.search(config.key_id+uid+config.dn_users, config.user.groups)[0]; + } + + /** + * @summary Fonction qui retrouve la liste des membres d'un groupe. + * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans config.json. + * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule) + * @return {Promise(String[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes) + * @static + */ + static getMembers(gid) { + return LDAP.search(config.key_id+gid+config.dn_users, config.group.member)[0]; + } + + /** + * @summary Fonction qui retrouve la liste des admins d'un groupe. + * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans config.json. + * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule) + * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes) + * @static + */ + static getAdmins(gid) { + return LDAP.search(config.key_id+gid+config.dn_users, config.group.admin)[0]; + } + + /** + * @summary Cette fonction teste si un utilisateur est membre d'un groupe. + * @desc Utilise les méthodes statiques {@link open.getGroups} et {@link open.getMembers} + * @param {string} uid - Identifiant de l'utilisateur à tester + * @param {string} gid - Identification du groupe à tester + * @returns {Promise(boolean)} True si l'utilisateur est membre + * @static + */ + static isGroupMember(uid, gid) { + return this.getGroups(uid).then(lg => { + this.getMembers(gid).then(lm => { + if (lg.includes(gid) && lm.includes(uid)) { + return true; + } + else { + return false; + } + }); + }); + } + + /** + * @summary Cette fonction teste si un utilisateur est admin d'un groupe. + * @desc Utilise la méthode statique {@link Open.getAdmins} + * @param {string} uid - Identifiant de l'utilisateur à tester + * @param {string} gid - Identification du groupe à tester + * @returns {Promise(boolean)} True si l'utilisateur est administrateur + * @static + */ + static isGroupAdmin(uid, gid) { + return this.getAdmins(gid).then(la => { + if (la.includes(uid)) { + return true; + } + else { + return false; + } + }); + } + + /** + * @summary Fonction qui renvoit toutes les infos relatives à un utilisateur particulier. + * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis. + * @arg {string} uid - Identifiant de l'utilisateur + * @return {Promise(Object[])} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet de l'utilisateur ; + * voir `ldap_config.json`(..\..\ldap_config.json) pour les clés exactes. + * @static + */ + static peekUser(uid) { + return LDAP.search(config.key_id+uid+config.dn_users, config.user.profil); + } + + /** + * @summary Fonction qui renvoit toutes les infos relatives à un groupe particulier. + * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis. + * @arg {string} gid - Identifiant du groupe + * @return {Promise(Object[])} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ; + * voir `ldap_config.json`(..\..\ldap_config.json) pour les clés exactes. + * @static + */ + static peekGroup(gid) { + return LDAP.search(config.key_id+gid+config.dn_groups, config.group.profil); + } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions de recherche + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui retrouve le groupe qui ressemblent à l'input et qui correspond au type fourni. Etape 0 vers un vrai TOL (Trombino On Line). + * @desc Cette fonction utilise {@link SmartSearch.groups}. + * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe. + * @arg {string} type - String aux valeurs prédéfinies dans ldap_config. + * @return {Promise(string[])} Liste des gid dont le nom ressemble à l'input. + * @static + */ + static findGroups(input, type) { + // Trucs intelligents faits dans ./utilities + return SmartSearch.groups(input, [config.key_id, config.group.type]).then(gList => { + let gidtyList = []; + gList.forEach(g => { + // Si le groupe est du bon type on rajoute son gid + if (g[config.group.type]==type) { gidtyList.push(g[config.key_id]); } + }); + return gidtyList; + }); + } + + /** + * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Autre étape vers vrai TOL (Trombino On Line). Doit être préféré à repliquerTOL + * car moins gourmande envers le LDAP (utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos). + * @desc Cette fonction utilise {@link SmartSearch.users}. + * @arg {Object} data - Dictionnaire contenant les données nécessaires à {@link SmartSearch.groups} + * @return {Promise(string[])} gids des profils qui "match" les critères proposés. + * @static + */ + static findUsers(data) { + return SmartSearch.users(data, config.key_id); + } +} + +class User extends Open { + /** + * @class Cette classe est la classe de l'utilisateur connecté qui peut déjà créer un groupe et changer son profil. + * @summary Ce constructeur appelle simplement {@link LDAP.bind}. + * @arg {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}. + * @author hawkspar + */ + constructor(user) { + super(); + LDAP.bind(user); + } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonction de création TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui créé un nouveau groupe dans le LDAP. + * @desc Cette fonction fait une utilisation massive d'eval pour anonymiser son code ; c'est mal et cela suppose que beaucoup de soins ont été pris lors de + * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Admin.addMemberGroup} et {@link Admin.addAdminGroup} + * pour gérer les groupes du nouvel utilisateur. Attention une manip FOIREUSE est cachée dedans. + * @arg {Object} data - Dictionnaire des informations utilisateurs (voir détail des champs dans config.json) + * @arg {string} data[name] - Nom du groupe + * @arg {string} data[ns] - Statut du groupe ; 'binet' ou 'free', cà d ouvert à tous + * @arg {string[]} data[members] - Liste des membres du groupe + * @arg {string[]} data[admins] - Liste des admins du groupe ; supposée être une sous-liste de la précédente + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + addGroup(data) { + // Calcul d'un dictionnaire d'ajout + let vals = {}; + + // uid de base généré à partir du nom standardisé + Tests.generateReadableId(data['name']).then(id => { vals[config.group['name']]=id; }); + + // Ecriture de toutes les valeurs directement inscrites dans le LDAP (in pour input) + config.group.direct_input.forEach(key_att => vals[config.group[key_att]]=data[key_att]); + + // Appel à la fonction de base + return LDAP.add(config.key_id+"="+vals[config.group['name']]+","+config.dn_groups, vals).then( res => { + // Certains champs nécessitent de petits calculs + let vals2={}; + + // Sauvegarde du nom (pour le cas où gid != data['name']) + vals2[config.group["name"]]=data['name']; + + // ?! + vals2[config.user['password']] = ''; + + // Génération id aléatoire et test contre le LDAP + Tests.generateId(config.groups["idNumber"], config.dn_groups).then(id => { vals2[config.group['idNumber']]=id; }); + // FOIREUX : Hypothèse sur la structure du reste des données mais évite un test.assurerUnicite à deux variables + vals2[config.group['idNumber2']]=vals2[config.group['idNumber']]; + + // Stockage machine ; dépend du prénom + vals2[config.group['directory']] = '/hosting/groups/'+vals[config.key_id]; + + // Code root + vals2[config.group['cleanFullName']]=data['name'].replace(':', ';').toLowerCase().normalize('UFD'); + + // Adressage root + vals2[config.group['login']] = "/sbin/nologin"; + + // Permissions BR + vals2[config.group['readPerm']] = '!*'; + vals2[config.group['writePerm']] = '!*'; + + // Inscription des valeurs calculées + LDAP.change(config.key_id+"="+vals[config.key_id]+","+config.dn_groups, "add", vals2).then(res => { + if (!res) { throw Error; } + }); + + ["posixAccount", "posixGroup", "brAccount"].forEach(cst => { + let vals3={}; + vals3[config.group['class']]=cst; + LDAP.change(config.key_id+"="+vals[config.key_id]+","+config.dn_groups, "add", vals3).then(res => { + if (!res) { throw Error; } + }); + }); + + // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble + data['members'].forEach(uid => { Admin.addGroupMember(uid, vals[config.key_att]); }); + data['admins'].forEach(uid => { Admin.addGroupAdmin( uid, vals[config.key_att]); }); + + return true; + }); + } + + //------------------------------------------------------------------------------------------------------------------------ + // Fonctions d'édition TBT + //------------------------------------------------------------------------------------------------------------------------ + + /** + * @summary Fonction qui édite un utilisateur existant dans le LDAP. Très similaire à {@link creerUtilisateur} + * @desc Appelle simplement {@link creerUtilisateur} et {@link supprimerUtilisateur} en godmode, plus {@link renseignerSurUtilisateur} pour les champs non fournis. + * Ce choix a pour conséquence que l'ordre du dictionnaire de correspondance dans ldap_config est important. A modifier car donne trop de pouvoir à l'utilisateur. + * @arg {string} uid - Utilisateur à modifier (le plus souvent le même, mais root possible) + * @arg {Object} data - Dictionnaire des informations utilisateurs au même format que pour {@link creerUtilisateur} avec tous les champs optionnels ; + * MEF ces valeurs vont écraser les précédentes. + * attention toutes les clés de cette entrée seront modifiées dans le LDAP + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + */ + editUser(uid, data) { + // Récupération des anciennes données + return this.peekUser(uid).then(profil => { + // Reecriture de profil avec les bons champs + Object.keys(profil).forEach(keyLDAP => { + Object.keys(config.user).forEach(keyAlias => { + config.user[keyAlias]=keyLDAP; + profil[keyAlias]=profil[keyLDAP]; + }); + }); + // Régénération du champ manquant dans profil + this.getGroups(uid).then(lg => { + profil['groupsIsAdmin']=[]; + lg.forEach(gid =>{ + this.isGroupAdmin(uid, gid).then(res =>{ + if (res) { profil['groupsIsAdmin'].push(gid); } + }).then(res => { + // Surcharge des champs à modifier selon data + Object.keys(data).forEach(key => { + profil[key]=data[key]; + }); + // Passage en godmode + var god = SuperAdmin({"uid":"", "password":""}); + // Modification propre + god.delUser(uid).then(r => { + god.createUser(profil).then(res => { if (!res) { throw Error; } else { return true; }}); + }); + }); + }); + }); + }); + } + + destr() { LDAP.unbind(); } +} + +export {Open, User}; \ No newline at end of file diff --git a/src/ldap/utilities.js b/src/ldap/utilities.js index 9dd45e4..f2f8a79 100644 --- a/src/ldap/utilities.js +++ b/src/ldap/utilities.js @@ -1,19 +1,22 @@ /** - * @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. + * @file Ce fichier regroupe les fonctions simples de recherche et de test utiles, mais trop puissantes pour être exportées directement. * @author hawkspar */ -import * as basics from './basics'; +import LDAP from './basics'; +// Essentiels pour le fichier de config +import path from 'path'; import fs from 'fs'; +// Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement +var configPath = path.resolve('./','ldap_config.json'); +var config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + //------------------------------------------------------------------------------------------------------------------------ // Fonctions de recherche //------------------------------------------------------------------------------------------------------------------------ -class rechercheModulable { +class SmartSearch { /** * @class Cette classe contient des fonctions de recherche génériques trop puissantes pour être exportées tel quel. * @summary Constructeur vide. @@ -24,7 +27,7 @@ class rechercheModulable { /** * @summary Fonction qui interroge le LDAP et retrouve les groupes (voir LDAP) qui ressemblent * à l'entrée. Etape 0 vers un vrai TOL (Trombino On Line). - * @desc Cette fonction utilise {@link LDAP.rechercher} mais avec un filtre généré à la volée. + * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. * Accepte des champs exacts ou incomplets mais pas approximatifs * et ne gère pas l'auto-complete. Cette fonction utilise aussi config.json. MEF Timeout pour * des recherches trop vagues. Renvoit une liste d'uid. @@ -34,25 +37,20 @@ class rechercheModulable { * @return {Promise(string[])} Liste des uid de groupes dont le nom ressemble à l'input * @static */ - static trouverGroupes(input, return_attributes) { - return new Promise((resolve, reject) => { - // Escape de l'input utilisateur par sécurité - let str=ldapEscape.filter("${txt}", { txt: input}); - - // Construction du filtre custom - let filter= "(|("+config.key_id+"="+str+")"+ // On cherche la valeur exacte - "(|("+config.key_id+"=*"+str+")"+ // La valeur finale avec des trucs avant ; wildcard * - "(|("+config.key_id+"=*"+str+"*)"+ // La valeur du milieu avec des trucs avant et après - "("+config.key_id+"="+str+"*))))"; // La valeur du début avec des trucs après + static groups(input, return_attributes) { + // Construction du filtre custom + let filter= "(|("+config.key_id+"="+input+")"+ // On cherche la valeur exacte + "(|("+config.key_id+"=*"+input+")"+ // La valeur finale avec des trucs avant ; wildcard * + "(|("+config.key_id+"=*"+input+"*)"+ // La valeur du milieu avec des trucs avant et après + "("+config.key_id+"="+input+"*))))"; // La valeur du début avec des trucs après - // Appel rechercheLDAP avec filtre de l'espace - LDAP.rechercher(config.dn_groups, return_attributes, filter).then(res => resolve(res)); - }); + // Appel rechercheLDAP avec filtre de l'espace + return LDAP.search(config.dn_groups, return_attributes, filter); } /** * @summary Fonction qui renvoit les attributs demandés des paxs validant les critères de recherche. Première étape vers vrai TOL (Trombino On Line). - * @desc Cette fonction utilise {@link LDAP.rechercher} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs + * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs * mais pas approximatifs et ne gère pas l'auto-complete. MEF Timeout pour des recherches trop vagues. Elle utilise LDAPEscape pour éviter les injections. * Utiliser trouverGroupesParTypes pour chaque champ relié à groups. * @arg {Object} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse @@ -74,30 +72,26 @@ class rechercheModulable { * @return {Promise(Object[])} Liste de dictionnaires de profils en cohérence avec l'input avec pour clés les attributs des profils. * @static */ - static trouverUtilisateurs(data, return_attributes) { - return new Promise((resolve, reject) => { - let filter=""; - // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans config encore - for (var key in data) { - if ((data[key]!= undefined) & (data[key] != '')) { // Si il y a qque chose à chercher pour ce filtre - if (!Array.isArray(data[key])) { data[key]=[data[key]]; } // Gestion d'une liste de valeurs à rechercher - // Iteration pour chaque valeur fournie par l'utilisateur - data[key].forEach(val => { - // Escape de l'input utilisateur - let str=ldapEscape.filter("${input}", { input: val}); - // Traduction en language LDAP - let attribute = config.user[key]; - // Creation du filtre étape par étape - filter="(&"+filter+ "(|("+attribute+"="+str+")"+ // On cherche la valeur exacte - "(|("+attribute+"=*"+str+")"+ // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs) - "(|("+attribute+"=*"+str+"*)"+ // La valeur du milieu avec des trucs avant et après - "("+attribute+"="+str+"*)))))"; // La valeur du début avec des trucs après - }); - } + static users(data, return_attributes) { + let filter=""; + // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans config encore + for (var key in data) { + if ((data[key]!= undefined) & (data[key] != '')) { // Si il y a qque chose à chercher pour ce filtre + if (!Array.isArray(data[key])) { data[key]=[data[key]]; } // Gestion d'une liste de valeurs à rechercher + // Iteration pour chaque valeur fournie par l'utilisateur + data[key].forEach(val => { + // Traduction en language LDAP + let attribute = config.user[key]; + // Creation du filtre étape par étape + filter="(&"+filter+ "(|("+attribute+"="+val+")"+ // On cherche la valeur exacte + "(|("+attribute+"=*"+val+")"+ // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs) + "(|("+attribute+"=*"+val+"*)"+ // La valeur du milieu avec des trucs avant et après + "("+attribute+"="+val+"*)))))"; // La valeur du début avec des trucs après + }); } - // Appel rechercheLDAP avec filtre de l'espace - LDAP.rechercher(config.dn_users, return_attributes, filter).then(res => resolve(res)); - }); + } + // Appel rechercheLDAP avec filtre de l'espace + return LDAP.search(config.dn_users, return_attributes, filter); } } @@ -105,128 +99,77 @@ class rechercheModulable { // Fonctions intermédiaires TBT //------------------------------------------------------------------------------------------------------------------------ -class test { - /** - * @class Cette classe contient des fonctions intermédiaires de calcul utiles mais non exportables. La plupart sont des tests contre le LDAP - * @summary Constructeur vide. - * @author hawkspar - */ - constructor() {} - - /** - * @summary Cette fonction teste si un utilisateur est membre d'un groupe. - * @desc Utilise les méthodes statiques listerGroupes et listerMembres de {@link utilisateurAnonyme} mais aussi ajouterMembreGroupe de {@link administrateurConnecte} - * @param {string} uid - Identifiant de l'utilisateur à tester - * @param {string} gid - Identification du groupe à tester - * @returns {Promise(boolean)} True si l'utilisateur est membre - * @static - */ - static etreMembreGroupe(uid, gid) { - return new Promise((resolve, reject) => { - utilisateurAnonyme.listerGroupes(uid).then(lg => { - utilisateurAnonyme.listerMembres(gid).then(lm => { - if (lg.includes(gid) | lm.includes(uid)) { - superAdministrateurConnecte.ajouterMembreGroupe(uid, gid); - resolve(true); - } - }); - }); - }); - } - - /** - * @summary Cette fonction teste si un utilisateur est admin d'un groupe. - * @desc Utilise les méthodes statiques listerAdministrateurs de {@link utilisateurAnonyme} mais aussi ajouterAdministrateurGroupe de {@link administrateurConnecte} - * @param {string} uid - Identifiant de l'utilisateur à tester - * @param {string} gid - Identification du groupe à tester - * @returns {Promise(boolean)} True si l'utilisateur est administrateur - * @static - */ - static etreAdministrateurGroupe(uid, gid) { - return new Promise((resolve, reject) => { - utilisateurAnonyme.listerAdministrateurs(gid).then(la => { - if (la.includes(uid)) { - superAdministrateurConnecte.ajouterAdministrateurGroupe(uid, gid); - resolve(true); - } - }); - }); - } - +class Tests { /** * @summary Cette fonction teste une valeur d'un attribut (typiquement un identifiant) et le fait évoluer jusqu'à ce qu'il soit unique. - * @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.rechercher} pour vérifier + * @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.search} pour vérifier * qu'il n'y a pas d'autres occurences de cette valeur pour cette attribut * dans le dn fourni. - * @param {string} valeur - Valeur de l'attribut (le plus souvent un identifiant) à tester à cette itération - * @param {string} attribut - Attribut à tester + * @param {string} value - Valeur de l'attribut (le plus souvent un identifiant) à tester à cette itération + * @param {string} attribute - Attribut à tester * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique - * @param {function} evoluerValeur - Fonction qui prend uniquement en argument l'id courant et + * @param {function} changeValue - Fonction qui prend uniquement en argument l'id courant et * le nombre d'itérations et qui renvoit la prochaine valeur de l'attribut * @param {int} n [0] - Nombre d'itérations (à initialiser à 0) * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static */ - static assurerUnicite(valeur, attribut, dn, evoluerValeur, n=0) { - return new Promise((resolve, reject) => { - // Recherche d'autres occurences de l'id - LDAP.rechercher(dn, config.key_id, "("+attribut+"="+valeur+")").then(matches => { - // On renvoit la valeur si elle est bien unique - if (matches.length==0) { resolve(valeur); } - // Sinon, on tente de nouveau notre chance avec la valeur suivante - else { resolve(test.assurerUnicite(evoluerValeur(valeur, n+1), dn, evoluerValeur, n+1)); } - }); + static ensureUnique(value, attribute, dn, changeValue, n=0) { + // Recherche d'autres occurences de l'id + LDAP.search(dn, config.key_id, "("+attribute+"="+value+")").then(matches => { + // On renvoit la valeur si elle est bien unique + if (matches.length==0) { return value; } + // Sinon, on tente de nouveau notre chance avec la valeur suivante + else { return this.ensureUnique(changeValue(value, n+1), dn, changeValue, n+1); } }); } /** * @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique. - * @desc Limité à un appel à {@link test.assurerUnicite} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation). + * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation). * @param {string} givenName - Prénom * @param {string} lastName - Nom * @param {string} promotion - Année de promotion * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static */ - static genererUid(givenName, lastName, promotion) { - // Le filtrage évite l'injection de code dans le LDAP, le normalize et lowerCase standardisent le format - return new Promise((resolve, reject) => { - test.assurerUnicite(ldapEscape.filter("${uid}",{uid: givenName+'.'+lastName}).toLowerCase().normalize('UFD'), config.key_id, config.dn_users, (id,n) => { - if (n==1) { id+='.'+ldapEscape.filter("${pr}",{pr: promotion}); } // Si prénom.nom existe déjà , on rajoute la promo - else if (n==2) { id+='.'+n-1; } // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1 - else if (n>2) { id+=+n; } // Ensuite on continue .23, .234, etc... - return id; - }).then(id => resolve(id)); + static generateUid(givenName, lastName, promotion) { + // normalize et lowerCase standardisent le format + return this.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), config.key_id, config.dn_users, (id,n) => { + if (n==1) { id+='.'+promotion; } // Si prénom.nom existe déjà , on rajoute la promo + else if (n==2) { id+='.'+n-1; } // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1 + else if (n>2) { id+=n; } // Ensuite on continue .23, .234, etc... + return id; }); } /** - * @summary Cette fonction génère un gid standard, puis le fait évoluer jusqu'à ce qu'il soit unique. - * @desc Limité à un appel à {@link test.assurerUnicite} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation). + * @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique. + * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation). * @param {string} name - Nom * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static */ - static genererGid(name) { - // Le filtrage évite l'injection de code dans le LDAP, le normalize et lowerCase standardisent le format - return new Promise((resolve, reject) => { - test.assurerUnicite(ldapEscape.filter("${id}",{id: name}).toLowerCase().normalize('UFD'), config.key_id, config.dn_groups, (id,n) => { - if (n==1) { id+='.'+n; } // Si nom existe déjà , on essaie nom.1 - else if (n>1) { id+=+n; } // Ensuite on continue .12, .123, etc... - return id; - }).then(id => resolve(id)); + static generateReadableId(name) { + // normalize et lowerCase standardisent le format + return this.ensureUnique(name.toLowerCase().normalize('UFD'), config.key_id, config.dn_groups, (id,n) => { + if (n==1) { id+='.'+n; } // Si nom existe déjà , on essaie nom.1 + else if (n>1) { id+=+n; } // Ensuite on continue .12, .123, etc... + return id; }); } /** * @summary Cette fonction teste une valeur dummy (0) pour un identifiant numérique puis le fait évoluer aléatoirement (entre 1 et 100 000) jusqu'à ce qu'il soit unique. - * @param {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.connecter}. + * @param {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}. * @param {string} attribut - Intitulé exact de l'id concerné * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static */ - static genererIdNum(attribut, dn) { - return new Promise((resolve,reject) => { test.assurerUnicite("0", attribut, dn, (id,n) => { Math.floor((Math.random() * 100000) + 1).toString(); }).then(id => resolve(id)); }); + static generateId(attribut, dn) { + return this.ensureUnique("0", attribut, dn, (id,n) => { Math.floor((Math.random() * 100000) + 1).toString(); }); } -} \ No newline at end of file +} + +export {SmartSearch, Tests}; \ No newline at end of file -- GitLab