diff --git a/ldap_config.json b/ldap_config.json index dbb5dc3b66dbf052ac3aaa36fe0d4e5be70e5532..03484ee0cd6facfe2060d30a43f8ced9107d6672 100644 --- a/ldap_config.json +++ b/ldap_config.json @@ -20,7 +20,7 @@ "nationality": "country", "promotion": "brPromo", "phone": "telephoneNumber", - "adresses": "brRoom", + "adress": "brRoom", "id": "uidNumber", "password": "userPassword", "idNum": "gidNumber", @@ -28,7 +28,7 @@ "login": "loginShell", "readPerm": "brNewsReadAccess", "writePerm": "brNewsPostAccess", - "mails": "mail", + "mail": "mail", "ips": "brIP", "forlifes": "brAlias", "groups": "brMemberOf", diff --git a/src/ldap/basics.ts b/src/ldap/basics.ts index e2ab6e0fc050f6fa36890bc76c325aeb5f038da8..1ce31791fd8ac0d6b0cc1e9fe89337007d6225bc 100644 --- a/src/ldap/basics.ts +++ b/src/ldap/basics.ts @@ -117,7 +117,7 @@ export class LDAP { // 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', res => { LDAP.unbind(); }); + res.on('end', _ => { LDAP.unbind(); }); } }); // On renvoit le résultat @@ -130,7 +130,7 @@ export class LDAP { * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode modify). * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur) * @arg {string} id - Identifiant unique de la feuille à modifier - * @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, + * @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. @@ -138,14 +138,14 @@ export class LDAP { * @static * @async */ - static async change(domain: 'gr'|'us', id: string, op: string, mod) : Promise<boolean> { + static async change(domain: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod) : 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) client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({ - operation: ldapEscape.dn("${txt}", {txt: op}), + operation: op, modification: mod, // Gestion erreur }), err => { diff --git a/src/ldap/group.ts b/src/ldap/group.ts index b32643c2acae6924902c4bc4f776ed8facef5fe3..91dbaf6324cdca1a40f42888061a2e501a86a272 100644 --- a/src/ldap/group.ts +++ b/src/ldap/group.ts @@ -67,7 +67,7 @@ export class Group { */ static async search(input: string) : Promise<string[]> { try { - return Tools.genericSearch<groupData>("gr", { + return Tools.genericSearch("gr", { "gid": input, "name": "", "type": "", @@ -354,17 +354,25 @@ export class Group { } /** - * @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 {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. + * @summary Fonction qui édite un groupe existant dans le LDAP. + * @desc Appelle {@link genericEdit} bien sûr, mais aussi {@link addMember} et {@link addAdmin}. + * @arg {groupData} data - Dictionnaire des informations du groupe. * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async * @static */ static async edit(data: groupData) : Promise<boolean> { try { - return Tools.genericEdit<groupData>("gr",data); + 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); } catch(err) { throw "Erreur lors de la modification d'un groupe."; diff --git a/src/ldap/user.ts b/src/ldap/user.ts index 20c9a9047e5d6f8e7dcbfb4b7335b7070befa55c..35fadd5bdc3d6d3cd18dde759ea7ad83a5a1fdc4 100644 --- a/src/ldap/user.ts +++ b/src/ldap/user.ts @@ -35,26 +35,26 @@ import {Group} from './group'; * TBA @var {string[]} likes - Liste des gid dont l'utilisateur est sympathisant */ export interface userData { - "uid"?: string, + "uid": string, + "groups": string[], + "groupsIsAdmin": string[], + "password"?: string, "givenName"?: string, "lastName"?: string, "nickname"?: string, + "promotion"?: string, "photo"?: string, "birthdate"?: string, //"nationality"?: string, - "promotion"?: string, "phone"?: string, - "adresses"?: string, - "mails"?: string[], - "groups"?: string[], - "password"?: string, + "adress"?: string, + "mail"?: string, "ips"?: string[], "directory"?: string, "login"?: string, "readPerm"?: string, "writePerm"?: string, - "forlifes"?: string[], - "admins"?: string[] + "forlifes"?: string[] //"likes"?: string[] } @@ -68,49 +68,26 @@ export class User { * @summary Constructeur vide. */ constructor() {} - + /** - * @summary Fonction qui renvoit certaines informations relatives à un utilisateur particulier. - * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis. + * @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(T)} Informations recueillies ; renvoie une partie du profil de l'utilisateur selon le format choisi. - * Voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes. + * @return {Promise(userData)} Informations recueillies. * @static * @async - * @private */ - private static async genericPeek<T>(uid: string) : Promise<T> { - try { - let fields = []; - fields.push(ldapConfig.user.values()); - let LDAPUserData = await LDAP.search("us", fields, uid); - let cleanUserData : T; - // Rename output - for (let uncleanKey in LDAPUserData) { - for (let cleanKey in cleanUserData) { - if (uncleanKey==ldapConfig.group[cleanKey]) { cleanUserData[cleanKey] = LDAPUserData[uncleanKey]; } - } - } - return cleanUserData; + static async peek(uid: string) : Promise<userData> { + try { + return Tools.genericPeek<userData>("us", uid); } catch(err) { - throw "Erreur lors d'une recherche d'informations sur un individu."; + throw "Error while peeking a user."; } } - - /** - * @summary Fonction qui renvoit les infos de base relatives à un utilisateur particulier. - * @desc Cette fonction utilise {@link User.genericPeek} avec l'interface {@link userData}. - * @arg {string} uid - Identifiant de l'utilisateur - * @return {Promise(userData)} Informations recueillies. - * @static - * @async - */ - static async peek(uid: string) : Promise<userData> { return User.genericPeek<userData>(uid); } /** - * @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é à repliquer TOL - * car moins gourmande envers le LDAP (utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos). + * @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. @@ -123,25 +100,7 @@ export class User { */ static async search(data: userData) : Promise<string[]> { try { - let filter=""; - // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans ldapConfig 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 = ldapConfig.user[key]; - // Creation incrémentale du filtre - 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 avec filtre de l'espace - return LDAP.search("us", [ldapConfig.key_id], null, filter); + return Tools.genericSearch("us", data); } catch(err) { throw "Erreur lors de la recherche approximative d'un utilisateur."; @@ -298,19 +257,24 @@ export class User { /** * @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_ldapConfig est important. - * Une version "nerfée" de cette fonction est envisageable ; elle donne bcp de pouvoir à l'utilisateur. - * @arg {fullUserData} data - Dictionnaire des informations utilisateurs au même format que pour {@link creerUtilisateur} avec tous les champs optionnels sauf 'uid', - * qui permet de savoir qui modifier. Attention toutes les clés de cette entrée seront modifiées dans le LDAP ; les nouveaux résultats écrasant les précédents, - * sauf 'readPerm','writePerm', 'forlifes','ips','groups' et 'groupsIsAdmin' qui sont censurés pour cette fonction). + * @desc Appelle simplement {@link genericEdit}. + * @arg {userData} data - Dictionnaire des informations utilisateurs * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async * @static */ static async edit(data : userData) : Promise<boolean> { try { - return Tools.genericEdit<userData>("us",data); + 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); } catch(err) { throw "Erreur lors de la modification d'un utilisateur."; diff --git a/src/ldap/utilities.ts b/src/ldap/utilities.ts index 55fb907e7b0e65014b2f50f35262fe489a4fe384..dc47a122097126bc9301dc10a96f4ef2e4349acf 100644 --- a/src/ldap/utilities.ts +++ b/src/ldap/utilities.ts @@ -6,10 +6,8 @@ import {ldapConfig} from './config'; import {LDAP} from './basics'; -import { groupData } from './group'; import { userData } from './user'; -import { Group } from './group'; -import { User } from './user'; +import { groupData } from './group'; //------------------------------------------------------------------------------------------------------------------------ // Fonctions intermédiaires TBT @@ -26,23 +24,26 @@ export class Tools { /** * @summary Fonction qui renvoit toutes les infos relatives à un groupe ou un utilisateur particulier. * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis. - * @arg {string} domain - Domaine de la recherche - * @arg {string} id - Identifiant de la feuille cherchée - * @return {Promise(T)} 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. + * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData}) + * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe) + * @arg {string} id - Identifiant de la feuille cherchée (uid ou gid) + * @return {Promise(T)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe tel que défini par le paramètre T. * @static * @async */ - static async genericPeek<T>(domain: 'us' | 'gr', id: string) : Promise<T> { - let fields = []; - if (domain=='gr') { fields.push(ldapConfig.group.values()); } - else { fields.push(ldapConfig.user.values()); } - let LDAPData = await LDAP.search(domain, fields, id); + static async genericPeek<T>(domain: 'us'|'gr', id: string) : Promise<T> { + if (domain='gr') { + var dirtyKeys = ldapConfig.group; + } + else { + var dirtyKeys = ldapConfig.user; + } let cleanData : T; + let dirtyData = await LDAP.search(domain, dirtyKeys.values(), id); // Rename output - for (let uncleanKey in LDAPData) { + for (let uncleanKey in dirtyData) { for (let cleanKey in cleanData) { - if (uncleanKey==ldapConfig.group[cleanKey]) { cleanData[cleanKey] = LDAPData[uncleanKey]; } + if (uncleanKey=dirtyKeys[cleanKey]) { cleanData[cleanKey] = dirtyData[uncleanKey]; } } } return cleanData; @@ -50,29 +51,30 @@ export class Tools { /** - * @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é à repliquer TOL - * car moins gourmande envers le LDAP (utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos). + * @summary Fonction qui retrouve les id des paxs ou groupes validant les critères de recherche. Etape vers vrai TOL (Trombino On Line). + * Utiliser {@link peekUser} 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. - * @arg {T} 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 + * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData}) + * @arg {string} 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. - * @return {Promise(string[])} gids des profils qui "match" les critères proposés. + * @return {Promise(string[])} ids des profils qui "match" les critères proposés. * @static * @async */ - static async genericSearch<T>(domain : "us"|"gr", data : T) : Promise<string[]> { + static async genericSearch(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) { 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 + if (!Array.isArray(data[key])) { data[key]=[data[key]]; } // Génération systématique d'une liste de valeurs à rechercher // Iteration pour chaque valeur fournie par l'utilisateur data[key].forEach(val => { // Traduction en language LDAP let attribute = ""; - if (domain=="us") { attribute = ldapConfig.user[key]; } + if (domain="us") { attribute = ldapConfig.user[key]; } else { attribute = ldapConfig.group[key]; } // Creation incrémentale du filtre filter="(&"+filter+ "(|("+attribute+"="+ val+")"+ // On cherche la valeur exacte @@ -85,50 +87,33 @@ export class Tools { // Appel avec filtre de l'espace return LDAP.search(domain, [ldapConfig.key_id], null, filter); } - /** - * @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 {T} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels... + * @summary Fonction qui édite un groupe existant dans le LDAP. + * @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 * @async * @static */ - static async genericEdit<T>(domain: "us" | "gr", data: T) : Promise<boolean> { - let id = ""; - if (domain == "us") { id=data['uid']; } - else { id=data['gid']; } - // Récupération des anciennes données - let profil = await Tools.genericPeek<T>(domain,id); - if (domain == "us") { - // Régénération du champ manquant dans profil - let lg = await Tools.getGroups(id); - profil['groupsIsAdmin']=[]; - lg.forEach(gid => { - Tools.isGroupAdmin(id, gid).then(res => { - if (res) { profil['groupsIsAdmin'].push(gid); } - }); - }); + static async genericEdit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> { + if (domain = "us") { + var id=data['uid']; + var dirtyKeys=ldapConfig.user; + } + else { + var id=data['gid']; + var dirtyKeys=ldapConfig.group; } - // Surcharge des champs à modifier selon data - Object.keys(data).forEach(function(key: string) { - // Some fields the user cannot change - if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) { profil[key]=data[key]; } - // Specialised management of group membership and admin status - if (key=="groups") { data[key].array.forEach(gid => { Group.addMember(id, gid); }); } - if (key=="groupsIsAdmin") { data[key].array.forEach(gid => { Group.addAdmin(id, gid); }); } - if (key=="members") { data[key].array.forEach(uid => { Group.addMember(uid, id); }); } - if (key=="admins") { data[key].array.forEach(uid => { Group.addMember(uid, id); }); } - }); // Renommage LDAP-friendly - let dirtyProfil = {}; - Object.keys(profil).forEach(function(key: string) { - if (domain=="gr") { dirtyProfil[ldapConfig.group.key]=profil[key]; } - else { dirtyProfil[ldapConfig.user.key]=profil[key]; } + let dirtyData = {}; + Object.keys(data).forEach(function(key: string) { + if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) { + dirtyData[dirtyKeys.key]=data[key]; + } }); - return LDAP.change(domain,id,"replace",dirtyProfil); + return LDAP.change(domain,id,"replace",dirtyData); } /** @@ -158,7 +143,7 @@ export class Tools { return LDAP.search(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; } + else if (matches.length=0) { return value; } // Sinon, on tente de nouveau notre chance avec la valeur suivante else { return Tools.ensureUnique(changeValue(value, n+1), attribute, domain, changeValue, n+1); } }); @@ -182,8 +167,8 @@ export class Tools { try { // normalize et lowerCase standardisent le format return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "us", (id: string, n: number) => { - if (n==1) { id+='.'+promotion; } // Si prénom.nom existe déjà , on rajoute la promo - else if (n==2) { id+='.'+(n-1).toString(); } // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1 + if (n=1) { id+='.'+promotion; } // Si prénom.nom existe déjà , on rajoute la promo + else if (n=2) { id+='.'+(n-1).toString(); } // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1 else if (n>2) { id+=n; } // Ensuite on continue .123, .1234, etc... return id; }); @@ -205,8 +190,8 @@ export class Tools { try { // normalize et lowerCase standardisent le format return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "gr", (id: string, n: number) => { - if (n==1) { id+='.'+n.toString(); } // Si nom existe déjà , on essaie nom.1 - else if (n>1) { id+=n.toString(); } // Ensuite on continue .12, .123, etc... + if (n=1) { id+='.'+n.toString(); } // Si nom existe déjà , on essaie nom.1 + else if (n>1) { id+=n.toString(); } // Ensuite on continue .12, .123, etc... return id; }); }