diff --git a/src/ldap/basics.ts b/src/ldap/basics.ts index 1ce31791fd8ac0d6b0cc1e9fe89337007d6225bc..8135f1aa4b2bec05a2a5d981987336bd29db257f 100644 --- a/src/ldap/basics.ts +++ b/src/ldap/basics.ts @@ -19,6 +19,10 @@ import {ldapConfig, credentialsLdapConfig} from './config'; // Connection au serveur LDAP avec des temps de timeout arbitraires var client = ldap.createClient({ url: ldapConfig.server}); +interface dic { + [Key: string]: string; +} + //------------------------------------------------------------------------------------------------------------------------ // Fonctions de base agissant sur le LDAP //------------------------------------------------------------------------------------------------------------------------ @@ -71,7 +75,52 @@ export class LDAP { * @async */ static async unbind() : Promise<boolean> { return LDAP.bind("", ""); } - + + /** + * @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 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} 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: 'gr'|'us', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> { + LDAP.adminBind(); + let dn =""; + if (id != null) { dn+=ldapConfig.key_id+'='+id+','; } + if (domain == "gr") { dn+=ldapConfig.dn_groups; } + else { dn+=ldapConfig.dn_users; } + let vals=[]; + // Interrogation LDAP selon filter + client.search(ldapEscape.dn("${txt}", { txt: dn}), { + "scope": "sub", + "filter": ldapEscape.filter("${txt}", { txt: filter}), + "attributes": [attribute] + }, (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 => { + // Cas un seul attribut où le résultat est une liste directement + vals.push(entry.object[attribute]); + }); + // Si la recherche renvoie une erreur, on renvoit + res.on('error', resErr => { throw resErr; }); + // Si la recherche est finie on se déconnecte + res.on('end', _ => { LDAP.unbind(); }); + } + }); + // On renvoit le résultat + return vals; + } + /** * @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 @@ -80,18 +129,19 @@ export class LDAP { * @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.<Object>)|Promise(Array.Object.<string, Object>))} Résultats de la recherche ; soit une liste de valeurs d'attributs, + * @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 search(domain: 'gr'|'us', attributes: string[], id=null, filter="(objectClass=*)") : Promise<Array<any>> { + static async searchMultiple(domain: 'gr'|'us', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> { LDAP.adminBind(); - if (domain == "gr") { var dn = ldapConfig.dn_groups; } - else { var dn = ldapConfig.dn_users; } - if (id != null) { dn=ldapConfig.key_id+'='+id+','+dn; } + let dn =""; + if (id != null) { dn+=ldapConfig.key_id+'='+id+','; } + if (domain == "gr") { dn+=ldapConfig.dn_groups; } + else { dn+=ldapConfig.dn_users; } let vals=[]; - // Interrogation LDAP selon ldapConfiguration fournie en argument + // Interrogation LDAP selon filter client.search(ldapEscape.dn("${txt}", { txt: dn}), { "scope": "sub", "filter": ldapEscape.filter("${txt}", { txt: filter}), @@ -103,16 +153,11 @@ export class LDAP { } else { // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse res.on('searchEntry', 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]; - }); - } + vals.push({}); + attributes.forEach(attribute => { + vals.slice(-1)[0][attribute]=entry.object[attribute]; + }); }); // Si la recherche renvoie une erreur, on renvoit res.on('error', resErr => { throw resErr; }); @@ -132,18 +177,18 @@ export class LDAP { * @arg {string} id - Identifiant unique de la feuille à modifier * @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 {Object.<string, string>} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs. - * @arg {Object} mod[key] - Nouvelle valeur de l'attribut key. Une nouvelle valeur vide ("") est équivalent à la suppression de cet attribut. + * @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: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod) : Promise<boolean> { + static async change(domain: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> { LDAP.adminBind(); let dn = ldapConfig.key_id+'='+id+',' if (domain == "gr") { dn+=ldapConfig.dn_groups } else { dn+=ldapConfig.dn_users } - // Modification LDAP selon ldapConfiguration en argument (pourrait prendre une liste de Changes) + // 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, @@ -168,10 +213,11 @@ export class LDAP { */ static async add(domain: 'gr'|'us', vals) : Promise<boolean> { LDAP.adminBind(); - if (domain == "gr") { var dn = ldapConfig.dn_groups } - else { var dn = ldapConfig.dn_users } + let dn = ldapConfig.key_id+"="+vals[ldapConfig.key_id]; + if (domain == "gr") { dn+=ldapConfig.dn_groups; } + else { dn+=ldapConfig.dn_users; } // Ajout LDAP selon la ldapConfiguration en argument - client.add(ldapEscape.dn(ldapConfig.key_id+"="+vals[ldapConfig.key_id]+",${txt}", { txt: dn}), vals, err => { + client.add(ldapEscape.dn("${txt}", { txt: dn}), vals, err => { throw "Erreur lors d'une opération d'ajout sur le LDAP."; }); LDAP.unbind(); @@ -192,8 +238,8 @@ export class LDAP { static async clear(domain: 'gr'|'us', id: string) : Promise<boolean> { LDAP.adminBind(); let dn = ldapConfig.key_id+'='+id+',' - if (domain == "gr") { dn+=ldapConfig.dn_groups } - else { dn+=ldapConfig.dn_users } + if (domain == "gr") { dn+=ldapConfig.dn_groups; } + else { dn+=ldapConfig.dn_users; } // Suppression LDAP client.del(ldapEscape.dn("${txt}", {txt: dn}), err => { throw "Erreur lors d'une opération de suppression sur le LDAP."; diff --git a/src/ldap/config.ts b/src/ldap/config.ts index a05c02d720f3d5bb5188d273d76fd53df4a34034..d1922ecca2eea3d5f4c8c3e4b92d4921908528dd 100644 --- a/src/ldap/config.ts +++ b/src/ldap/config.ts @@ -7,11 +7,11 @@ import fs from 'fs'; import path from 'path'; import colors from 'colors'; // Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement -let path_config = path.resolve('ldap_config.json'); -let path_credentials = path.resolve('ldap_credentials.json'); +let path_config = path.resolve(__dirname,'..','ldap_config.json'); console.log(colors.cyan("Loading LDAP config file from "+path_config)); -console.log(colors.cyan("Loading LDAP credentials from "+path_credentials)); export const ldapConfig = JSON.parse(fs.readFileSync(path_config).toString()); +let path_credentials = path.resolve(__dirname,'..','ldap_credentials.json') +console.log(colors.cyan("Loading LDAP credentials from "+path_credentials)); export const credentialsLdapConfig = JSON.parse(fs.readFileSync(path_credentials).toString()); // Override config server from environment if (process.env.LDAP_URI != null) { diff --git a/src/ldap/group.ts b/src/ldap/group.ts index 91dbaf6324cdca1a40f42888061a2e501a86a272..39d64ef1f3fcea144f373b04f0d52489499d4b7a 100644 --- a/src/ldap/group.ts +++ b/src/ldap/group.ts @@ -10,7 +10,7 @@ import {Tools} from './utilities'; /** * @interface groupData * @var {string} gid - Identifiant du groupe - * @var {string} name - Nom du groupe + * @var {string} name - Nom du groupe (souvent son nom mais pas nécessairement) * @var {string} type - Statut du groupe ; binet, section sportive... (actuellement juste 'binet' ou 'free') * @var {string[]} members - Liste des membres du groupe * @var {string[]} admins - Liste des admins du groupe ; supposée être une sous-liste de la précédente @@ -38,16 +38,15 @@ export class Group { /** * @summary Fonction qui renvoit toutes les infos relatives à un groupe particulier. - * @desc Cette fonction utilise {@link Tools.genericPeek} avec l'interface {@link groupData}. + * @desc Cette fonction utilise {@link Tools.peek} avec l'interface {@link groupData}. * @arg {string} gid - Identifiant du groupe - * @return {Promise(groupData)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ; - * voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes. + * @return {Promise(groupData)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe au format {@link groupData}. * @static * @async */ static async peek(gid: string) : Promise<groupData> { try { - return Tools.genericPeek<groupData>("gr", gid); + return Tools.peek<groupData>("gr", gid); } catch(err) { throw "Erreur lors d'une recherche d'informations sur un groupe."; @@ -55,31 +54,31 @@ export class Group { } /** - * @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 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 ldapConfig.json. MEF Timeout pour - * des recherches trop vagues. Renvoit une liste d'uid. - * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe. + * @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) pour les groupes. + * @desc Cette fonction utilise {@link Tools.search}. + * @arg {groupData} input - String entré par l'utilisateur qui ressemble au nom du groupe. * @return {Promise(string[])} Liste des gid dont le nom ressemble à l'input. * @static * @async */ - static async search(input: string) : Promise<string[]> { + static async search(data: groupData) : Promise<string[]> { try { - return Tools.genericSearch("gr", { - "gid": input, - "name": "", - "type": "", - "members": [], - "admins": [] - }); + return Tools.search("gr", data); } catch(err) { throw "Erreur lors de la recherche approximative d'un groupe."; } } + /** + * @summary Fonction qui permet d'ajouter un utilisateur à un groupe. + * @desc Cette fonction fait essentiellement appel à {@link Tools.getMembers}, {@link Tools.getGroups} et {@link LDAP.change}. + * @arg {string} uid - Identifiant de l'utilisateur à ajouter + * @arg {string} gid - Identifiant du groupe + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + * @async + * @static + */ static async addMember(uid: string, gid: string) : Promise<boolean> { try { // Vérifie que l'utilisateur est pas déjà membre pour groupes @@ -116,7 +115,7 @@ export class Group { /** * @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}. + * @desc Cette fonction fait essentiellement appel à {@link Tools.getMembers}, {@link Tools.getGroups} et {@link LDAP.change}. * @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 @@ -135,8 +134,8 @@ export class Group { // Les rajoute un par un, sauf pour le supprimé lm.forEach(id => { if (id!=uid) { - this.addMember(id, gid).then(res => { - if (!res) { throw "Erreur lors du ré-ajout des autres membres"; } + Group.addMember(id, gid).then(res => { + if (!res) { throw "Erreur lors du ré-ajout d'un autre membre"; } }); } }); @@ -156,7 +155,7 @@ export class Group { // Les rajoute un par un, sauf pour le supprimé lg.forEach(id => { if (id!=gid) { - this.addMember(uid, id).then(res => { + Group.addMember(uid, id).then(res => { if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; } }); } @@ -170,10 +169,10 @@ export class Group { } /** - * @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 + * @summary Fonction qui permet de promouvoir un membre au stade d'administrateur d'un groupe. + * @desc Cette fonction fait essentiellement appel à {@link Group.addMember} {@link Tools.getAdmins} et {@link LDAP.change}. 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} uid - Identifiant du membre futur admin * @arg {string} gid - Identifiant du groupe * @return {boolean} `true` si la modification s'est bien déroulée, false sinon * @async @@ -201,8 +200,9 @@ export class Group { /** * @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 + * @desc Cette fonction fait essentiellement appel à {@link Group.remMember}, {@link Group.addMember} {@link LDAP.change}. + * Rajoute l'utilisateur au groupe par effet de bord si l'utilisateur n'est pas administrateur. + * @arg {string} uid - Identifiant de l'admin à dégrader, supposé membre * @arg {string} gid - Identifiant du groupe * @return {boolean} `true` si la modification s'est bien déroulée, false sinon * @async @@ -210,13 +210,15 @@ export class Group { */ static async remAdmin(uid: string, gid: string): Promise<boolean> { // Peut paraître absurde mais permet de s'assurer que le membre est bien présent et que ses champs sont comme il faut - if (!(await Group.remMember(uid, gid) && Group.addMember(uid,gid))) { throw "Erreur dans l'éjection/réadmission du futur admin."; } + if (!(await Group.remMember(uid, gid) && Group.addMember(uid,gid))) { throw "Erreur dans l'éjection/réadmission de l'ex-admin."; } try { // Vérifie que l'utilisateur est bien admin (comme dans delGroupMember) let la = await Tools.getAdmins(gid); if (la.includes(uid)) { // Supprime tous les administrateurs - if (!await LDAP.change("gr", gid, "del", ldapConfig.group.admins)) { throw "Erreur dans la suppression de tous les admins pour en supprimer un."; } + if (!await LDAP.change("gr", gid, "del", ldapConfig.group.admins)) { + throw "Erreur dans la suppression de tous les admins pour en supprimer un."; + } // Les rajoute un par un, sauf pour le supprimé la.forEach(id => { if (id!=uid) { Group.addAdmin(id, gid).then(res => { @@ -233,7 +235,7 @@ export class Group { /** * @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} + * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Group.addMember} et {@link Group.addAdmin} * pour gérer les groupes du nouvel utilisateur. Attention une manip FOIREUSE est cachée dedans. * @arg {groupData} data - Dictionnaire des informations utilisateurs (voir détail des champs dans ldapConfig.json) * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon @@ -255,7 +257,7 @@ export class Group { throw "Erreur lors de la génération d'un hruid pour créer un nouveau groupe."; } - let gid : string = vals[ldapConfig.key_id]; + let gid: string = vals[ldapConfig.key_id]; // Ecriture de toutes les valeurs directement inscrites dans le LDAP for (let key_att in data) { vals[ldapConfig.group[key_att]]=data[key_att] }; @@ -320,29 +322,41 @@ export class Group { if (!res) { throw "Erreur de l'ajout d'un admin au groupe."; } }); }); - return true; } /** * @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. + * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement. A modifier une fois que le LDAP incluerait les groupes administres par une utilisateur. + * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Group.remMember} et {@link Group.remAdmin} 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 * @async * @static */ - static async delete(gid): Promise<boolean> { + static async delete(gid: string): Promise<boolean> { try { - // Gestion des membres et administrateurs d'abord + // Gestion des administrateur et membres d'abord let profil = await Group.peek(gid); - // Ordre important - profil[ldapConfig.group['admin']].forEach( id => { - Group.remAdmin( id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un admin d'un groupe en cours de suppression."; } }); - }); - profil[ldapConfig.group['member']].forEach(id => { - Group.remMember(id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un membre."; } }); + // Modification du profil de chaque utilisateur + profil[ldapConfig.group['member']].forEach(async function quickPartRemUser(uid: string) { + // Modification des profils de tous les utilisateurs + let lg = await Tools.getGroups(uid); + // Vérifie que l'utilisateur est pas déjà viré pour users + if (lg.includes(gid)) { + // Supprime tous les groupes + if (!await LDAP.change("us", uid, "del", ldapConfig.user.groups)) { + throw "Erreur lors de la suppression de tous les groupes du membre."; + } + // Les rajoute un par un, sauf pour le supprimé + lg.forEach(id => { + if (id!=gid) { + Group.addMember(uid, id).then(res => { + if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; } + }); + } + }); + } }); // Elimination if (!await LDAP.clear("gr",gid)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; } @@ -354,8 +368,8 @@ export class Group { } /** - * @summary Fonction qui édite un groupe existant dans le LDAP. - * @desc Appelle {@link genericEdit} bien sûr, mais aussi {@link addMember} et {@link addAdmin}. + * @summary Fonction qui édite un groupe existant dans le LDAP. Sans influence sur ses membres ou admins. + * @desc Appelle {@link Tools.edit}. * @arg {groupData} data - Dictionnaire des informations du groupe. * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async @@ -363,16 +377,7 @@ export class Group { */ static async edit(data: groupData) : Promise<boolean> { try { - let gid = data["gid"]; - // Remove old members and admins - let profil = Group.peek(gid); - profil["members"].forEach(uid => { Group.remMember(uid, gid); }); - profil["admins"].forEach(uid => { Group.remAdmin(uid, gid); }); - // Add new members and admins - data["members"].forEach(uid => { Group.addMember(uid, gid); }); - data["admins"].forEach(uid => { Group.addAdmin(uid, gid); }); - // Edit all other fields - return Tools.genericEdit("gr",data); + return Tools.edit("gr",data); } catch(err) { throw "Erreur lors de la modification d'un groupe."; diff --git a/src/ldap/user.ts b/src/ldap/user.ts index 35fadd5bdc3d6d3cd18dde759ea7ad83a5a1fdc4..f2f9356801208824251f0d25cb1e7addc6e53995 100644 --- a/src/ldap/user.ts +++ b/src/ldap/user.ts @@ -71,15 +71,15 @@ export class User { /** * @summary Fonction qui renvoit les infos de base relatives à un utilisateur particulier. - * @desc Cette fonction utilise {@link Tools.genericPeek} avec l'interface {@link userData}. - * @arg {string} uid - Identifiant de l'utilisateur - * @return {Promise(userData)} Informations recueillies. + * @desc Cette fonction utilise {@link Tools.peek} avec l'interface {@link userData}. + * @arg {string} uid - Identifiant de l'utilisateur, supposé valide. + * @return {Promise(userData)} Informations recueillies au format {@link userData}. * @static * @async */ static async peek(uid: string) : Promise<userData> { try { - return Tools.genericPeek<userData>("us", uid); + return Tools.peek<userData>("us", uid); } catch(err) { throw "Error while peeking a user."; @@ -87,12 +87,10 @@ export class User { } /** - * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Utiliser {@link peek} au cas par cas après pour obtenir les vraies infos. - * @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. Va crasher si un champ n'est pas dans ldapConfig. - * Utiliser trouverGroupesParTypes pour chaque champ relié à groups. + * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Utiliser {@link User.peek} au cas par cas après pour obtenir les vraies infos. + * @desc Cette fonction utilise {@link Tools.search}. * @arg {userData} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse - * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exempl pour chercher un membre + * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exemple pour chercher un membre * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined. * @return {Promise(string[])} gids des profils qui "match" les critères proposés. * @static @@ -100,7 +98,7 @@ export class User { */ static async search(data: userData) : Promise<string[]> { try { - return Tools.genericSearch("us", data); + return Tools.search("us", data); } catch(err) { throw "Erreur lors de la recherche approximative d'un utilisateur."; @@ -109,8 +107,9 @@ export class User { /** * @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. + * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link Group.addMember} et {@link Group.addAdmin} pour gérer les groupes du nouvel utilisateur. * @arg {fullUserData} data - Dictionnaire des informations utilisateurs. Des erreurs peuvent apparaître si tous les champs ne sont pas remplis. + * Cette application ne permet pas de rejoindre des groupes. * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async * @static @@ -206,19 +205,6 @@ export class User { if (!res) { throw "Erreur lors de l'ajout d'une valeur constante à la feuille du nouvel utilisateur."; } }); }); - - // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble - data['groupsIsMember'].forEach(gid => { - Group.addMember(uid, gid).then(res => { - if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe."; } - }); - }); - data['groupsIsAdmin'].forEach(gid => { - Group.addAdmin(uid, gid).then(res => { - if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe en tant qu'admin."; } - }); - }); - return true; } @@ -229,7 +215,7 @@ export class User { /** * @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. + * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Group.remMember} et {@link Group.remAdmin} 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 * @async @@ -239,12 +225,25 @@ export class User { try { // Gestion des groupes d'abord let profil = await User.peek(uid); - profil[ldapConfig.user['groups']].forEach(gid => { - // Opérations effectuées par effet de bord - if (Tools.isGroupAdmin(uid,gid)) { - if (!Group.remAdmin(uid, gid)) { throw "Erreur lors de la suppression des droits d'admin de l'utilisateur."; } + profil[ldapConfig.user['groups']].forEach(async function (gid: string) { + // Si l'utilisateur était admin, l'enlever + Group.remAdmin(uid, gid); + // Enlever de la liste des membres + let lm = await Tools.getMembers(gid); + if (lm.includes(uid)) { + // Supprime tous les membres + if (!await LDAP.change("gr", gid, "del", ldapConfig.group.members)) { + throw "Erreur lors de la suppression de tous les membres du groupe."; + } + // Les rajoute un par un, sauf pour le supprimé + lm.forEach(id => { + if (id!=uid) { + Group.addMember(id, gid).then(res => { + if (!res) { throw "Erreur lors du ré-ajout d'un autre membre"; } + }); + } + }); } - if (!Group.remMember(uid, gid)) { throw "Erreur lors de la suppression de l'appartenance à un groupe de l'utilisateur."; } }); } catch(err) { @@ -256,8 +255,8 @@ export class User { } /** - * @summary Fonction qui édite un utilisateur existant dans le LDAP. Très similaire à {@link creerUtilisateur} - * @desc Appelle simplement {@link genericEdit}. + * @summary Fonction qui édite un utilisateur existant dans le LDAP. + * @desc Appelle simplement {@link Tools.edit}. Sans effet sur les groupes de l'utilisateur concerné. * @arg {userData} data - Dictionnaire des informations utilisateurs * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async @@ -265,16 +264,7 @@ export class User { */ static async edit(data : userData) : Promise<boolean> { try { - let uid = data["uid"]; - // Leave old groups - let profil = User.peek(uid); - profil["groups"].forEach(gid => { Group.remMember(uid, gid); }); - profil["groupsIsAdmin"].forEach(gid => { Group.remAdmin(uid, gid); }); - // Join new groups - data["groups"].forEach(gid => { Group.addMember(uid, gid); }); - data["groupsIsAdmin"].forEach(gid => { Group.addAdmin(uid, gid); }); - // Edit all other fields - return Tools.genericEdit("us",data); + return Tools.edit("us",data); } catch(err) { throw "Erreur lors de la modification d'un utilisateur."; diff --git a/src/ldap/utilities.ts b/src/ldap/utilities.ts index dc47a122097126bc9301dc10a96f4ef2e4349acf..28f4ce7278ba1bde6dc10c2c909952abaa145c84 100644 --- a/src/ldap/utilities.ts +++ b/src/ldap/utilities.ts @@ -6,8 +6,8 @@ import {ldapConfig} from './config'; import {LDAP} from './basics'; -import { userData } from './user'; -import { groupData } from './group'; +import {userData} from './user'; +import {groupData} from './group'; //------------------------------------------------------------------------------------------------------------------------ // Fonctions intermédiaires TBT @@ -31,7 +31,7 @@ export class Tools { * @static * @async */ - static async genericPeek<T>(domain: 'us'|'gr', id: string) : Promise<T> { + static async peek<T>(domain: 'us'|'gr', id: string) : Promise<T> { if (domain='gr') { var dirtyKeys = ldapConfig.group; } @@ -39,7 +39,7 @@ export class Tools { var dirtyKeys = ldapConfig.user; } let cleanData : T; - let dirtyData = await LDAP.search(domain, dirtyKeys.values(), id); + let dirtyData = await LDAP.searchMultiple(domain, dirtyKeys.values(), id)[0]; // Rename output for (let uncleanKey in dirtyData) { for (let cleanKey in cleanData) { @@ -56,7 +56,7 @@ export class Tools { * @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. Va crasher si un champ n'est pas dans ldapConfig. * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData}) - * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe) + * @arg {"us"|"gr"} domain - Domaine de la recherche (utilisateur ou groupe) * @arg {userData | groupData} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exemple pour chercher un membre * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined. @@ -64,7 +64,7 @@ export class Tools { * @static * @async */ - static async genericSearch(domain : "us"|"gr", data : userData|groupData) : Promise<string[]> { + static async search(domain : "us"|"gr", data : userData|groupData) : Promise<string[]> { let filter=""; // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans ldapConfig encore for (var key in data) { @@ -85,19 +85,19 @@ export class Tools { } } // Appel avec filtre de l'espace - return LDAP.search(domain, [ldapConfig.key_id], null, filter); + return LDAP.searchSingle(domain, ldapConfig.key_id, null, filter); } /** - * @summary Fonction qui édite un groupe existant dans le LDAP. + * @summary Fonction qui édite un groupe ou utilisateur existant dans le LDAP. N'agit pas sur l'apprtenance à un groupe. * @desc Appelle {@link LDAP.change}. - * @arg {userData | groupData} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels... - * Sauf 'gid', qui permet de savoir quel groupe modifier et qui est donc inchangeable. On peut modifier nickname par contre. - * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon + * @arg {"us"|"gr"} domain - Domaine de l'opération' (utilisateur ou groupe). + * @arg {userData | groupData} data - Dictionnaire avec les nouvelles valeurs de la feuille. + * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon. * @async * @static */ - static async genericEdit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> { + static async edit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> { if (domain = "us") { var id=data['uid']; var dirtyKeys=ldapConfig.user; @@ -106,9 +106,10 @@ export class Tools { var id=data['gid']; var dirtyKeys=ldapConfig.group; } - // Renommage LDAP-friendly + // Rename in an LDAP-friendly way let dirtyData = {}; Object.keys(data).forEach(function(key: string) { + // Some values edit can't change if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) { dirtyData[dirtyKeys.key]=data[key]; } @@ -132,15 +133,15 @@ export class Tools { * @param {"gr"|"us"} domain - Domaine dans lequel l'attribut doit être unique * @param {changeValueCallback} 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) + * @param {number} n [0] - Nombre d'itérations (à initialiser à 0) * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié * @static * @async */ - static async ensureUnique(value: string, attribute: string, domain: 'gr'|'us', changeValue: (string, number) => string, n=0) : Promise<string> { + static async ensureUnique(value: string, attribute: string, domain: 'gr'|'us', changeValue: (string, number) => string, n: number=0) : Promise<string> { // Recherche d'autres occurences de l'id try { - return LDAP.search(domain, [ldapConfig.key_id], null, "("+attribute+"="+value+")").then(function (matches: string[]) { + return LDAP.searchSingle(domain, ldapConfig.key_id, null, "("+attribute+"="+value+")").then(function (matches: string[]) { if (!matches) { throw ""; } // On renvoit la valeur si elle est bien unique else if (matches.length=0) { return value; } @@ -227,7 +228,7 @@ export class Tools { */ static async getGroups(uid: string) : Promise<string[]> { try { - return LDAP.search("us", [ldapConfig.user.groups], uid)[0]; + return LDAP.searchSingle("us", ldapConfig.user.groups, uid); } catch(err) { throw "Erreur lors de la recherche des groupes d'un individu."; @@ -244,7 +245,7 @@ export class Tools { */ static async getMembers(gid: string) : Promise<string[]> { try { - return LDAP.search("gr", [ldapConfig.group.members], gid)[0]; + return LDAP.searchSingle("gr", ldapConfig.group.members, gid); } catch(err) { throw "Erreur lors de la recherche des membres d'un groupe."; @@ -261,7 +262,7 @@ export class Tools { */ static async getAdmins(gid: string) : Promise<string[]> { try { - return LDAP.search("gr", [ldapConfig.group.admins], gid)[0]; + return LDAP.searchSingle("gr", ldapConfig.group.admins, gid); } catch(err) { throw "Erreur lors de la recherche des admins d'un groupe.";