From 714685287e357c79a1b85b646d54ac073f0819aa Mon Sep 17 00:00:00 2001
From: hawkspar <quentin.chevalier@polytechnique.edu>
Date: Sat, 19 Jan 2019 13:28:44 +0100
Subject: [PATCH] Ramp up du LDAP ; refacto add et rem, categories speaker et
 follower, inclut DFS pour les admins et les membres

---
 ldap_config.json            |  25 ++-
 src/ldap/export/group.ts    | 317 +++++++++++++++---------------------
 src/ldap/export/user.ts     | 122 +++++---------
 src/ldap/internal/basics.ts |  24 ++-
 src/ldap/internal/config.ts |  97 ++++++++++-
 src/ldap/internal/tools.ts  | 223 ++++++++++++++++---------
 6 files changed, 436 insertions(+), 372 deletions(-)

diff --git a/ldap_config.json b/ldap_config.json
index efe56e0..c3b3bd2 100644
--- a/ldap_config.json
+++ b/ldap_config.json
@@ -1,11 +1,15 @@
 {
 	"comment_1": "Tout ce fichier sert à protéger les vrais champs du LDAP dans les scripts dans src/ldap. Les champs ci-dessous contiennent le nécessaire à une première connexion par exemple.",
-	"server_prod": "ldap://frankiz.eleves.polytechnique.fr:389",
-	"server_dev": "ldap://129.104.201.10:389",
+	"server": {
+		"prod": "ldap://frankiz.eleves.polytechnique.fr:389",
+		"dev": "ldap://129.104.201.10:389"
+	},
 
 	"comment_2": "Noms de domaines dans LDAP ; le niv d'après est en uid=, voir Wikipedia",
-	"dn_groups":"ou=groups,dc=frankiz,dc=net",
-	"dn_users": "ou=eleves,dc=frankiz,dc=net",
+	"dn":{
+		"group":"ou=groups,dc=frankiz,dc=net",
+		"user": "ou=eleves,dc=frankiz,dc=net"
+	},
 	"key_id": "uid",
 	
 	"comment_3": "Placeholders et indications de contenu de certains champs du LDAP généré par frankiz pour les utilisateurs",
@@ -32,7 +36,10 @@
 		"mail": "mail",
 		"ips": "brIP",
 		"forlifes": "brAlias",
-		"groups": "brMemberOf",
+		"admins": "TBC",
+		"speakers": "TBC",
+		"members": "brMemberOf",
+		"followers": "TBC",
 		"classes": "objectClass"
 	},
 	"comment_4": "Placeholders et indications de contenu de certains champs du LDAP généré par frankiz pour les groupes",
@@ -40,8 +47,10 @@
 		"gid": "uid",
 		"name": "brAlias",
 		"category": "brNS",
-		"members": "restrictedMemberUid",
 		"admins": "memberUid",
+		"speakers": "TBC",
+		"members": "restrictedMemberUid",
+		"followers": "TBC",
 		"adress":"cn",
 		"idNumber": "uidNumber",
 		"idNumber2": "gidNumber",
@@ -50,7 +59,7 @@
 		"directory": "homeDirectory",
 		"cleanFullName": "gecos",
 		"classes": "objectClass",
-		"child": "child",
-		"parent": "parent"
+		"childs": "TBC",
+		"parents": "TBC"
 	}
 }
\ No newline at end of file
diff --git a/src/ldap/export/group.ts b/src/ldap/export/group.ts
index 2c2bb8a..b586f3f 100644
--- a/src/ldap/export/group.ts
+++ b/src/ldap/export/group.ts
@@ -3,27 +3,10 @@
  * @author hawkspar
  */
 
-import {ldapConfig} from '../internal/config';
+import { ldapConfig, groupData, categories } from '../internal/config';
 import {Basics} from '../internal/basics';
 import {Tools} from '../internal/tools';
-
-/**
- * @interface groupData
- * @var {string} gid - Identifiant 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
- * @var {string} description - Description du groupe (facultatif)
- */
-export class groupData {
-    "gid": string;
-	"name": string;
-	"type": string;
-    "members": string[];
-    "admins": string[];
-    "description"?: string;
-}
+import { visit } from 'graphql';
 
 //------------------------------------------------------------------------------------------------------------------------
 // Classes à exporter TBT
@@ -49,7 +32,7 @@ export class Group {
      */
     static async peek(gid: string) : Promise<groupData> {
         try {
-            return Tools.peek<groupData>("gr", gid, groupData);
+            return Tools.peek<groupData>("group", gid, groupData);
         }
         catch(err) {
             throw "Erreur lors d'une recherche d'informations sur un groupe.";
@@ -67,17 +50,79 @@ export class Group {
     */
     static async search(data: groupData) : Promise<string[]> {
         try {
-            return Tools.search("gr", data);
+            return Tools.search("group", data);
         }
         catch(err) {
             throw "Erreur lors de la recherche approximative d'un groupe.";
         }
     }
 
+    /**
+     * @memberof LDAP
+     * @summary Fonction qui permet de rajouter un administrateur à un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.add}. Le nouvel administrateur devient aussi membre et porte-parole du groupe,
+     * mais hérite aussi de son statut d'administrateur sur tous les groupes qui héritent du sien.
+     * @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
+     * @static
+     */
+    static async addAdmin(uid: string, gid: string): Promise<boolean> {
+        return  await Tools.add(uid, gid, "members") &&
+                await Tools.add(uid, gid, "speakers") &&
+                await Tools.add(uid, gid, "admins");
+    }
+
+    /**
+     * @memberof LDAP
+     * @summary Fonction qui permet de supprimer un administrateur.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.remove}.
+     * Elle ne remonte pas les échelons, car cela permettrait à un admin d'un petit groupe de supprimer un admin d'un grand. 
+     * @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
+     * @static
+     */
+    static async remAdmin(uid: string, gid: string): Promise<boolean> {
+        return Tools.remove(uid, gid, "admins");
+    }
+
+    /**
+     * @memberof LDAP
+     * @summary Fonction qui permet de rajouter un porte-parole à un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.add}. Elle rajoute également l'utilisateur au groupe.
+     * @arg {string} uid - Identifiant du membre futur porte-parole
+     * @arg {string} gid - Identifiant du groupe
+     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async addSpeaker(uid: string, gid: string): Promise<boolean> {
+        return  await Tools.add(uid, gid, "members") &&
+                await Tools.add(uid, gid, "speakers");
+    }
+
+    /**
+     * @memberof LDAP
+     * @summary Fonction qui permet de rétrograder un membre du stade de porte-parole d'un groupe au stade d'utilisateur.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.remove}. Elle dégrade aussi d'un éventuel statut d'administrateur.
+     * @arg {string} uid - Identifiant de l'admin à dégrader (pas supposé valide)
+     * @arg {string} gid - Identifiant du groupe
+     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async remSpeaker(uid: string, gid: string): Promise<boolean> {
+        return  await Tools.remove(uid, gid, "admins") &&
+                await Tools.remove(uid, gid, "speakers");
+    }
+
     /**
      * @memberof LDAP
      * @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}.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.add}.
      * @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
@@ -85,166 +130,66 @@ export class Group {
      * @static
      */
     static async addMember(uid: string, gid: string) : Promise<boolean> {
-        try {
-            // Vérifie que l'utilisateur est pas déjà membre pour groupes
-            let lm = await Tools.getMembers(gid);
-            if (!lm.includes(uid)) {
-                let vals = {};
-                vals[ldapConfig.group.members] = uid;
-                // Erreur si pb lors de la modification
-                if (!await Basics.change("gr", gid, "add", vals)) {
-                    throw "Erreur lors de la modification dans l'arbre des groupes pour ajouter un membre.";
-                }
-            }
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
-        }
-        try {
-            // Vérifie que l'utilisateur est pas déjà membre pour users
-            let lg = await Tools.getGroups(uid);
-            if (!lg.includes(gid)) {
-                let vals2 = {};
-                vals2[ldapConfig.user.groups] = gid;
-                // Erreur si pb lors de la modification
-                if (!await Basics.change("us", uid, "add", vals2)) {
-                    throw "Erreur lors de la modification dans l'arbre des utilisateurs pour ajouter un membre.";
-                }
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
-        }
+        return Tools.add(uid, gid, "members");
     }
 
     /**
      * @memberof LDAP
      * @summary Fonction qui permet de supprimer un membre existant d'un groupe.
-     * @desc Cette fonction fait essentiellement appel à {@link Tools.getMembers}, {@link Tools.getGroups} et {@link LDAP.change}.
-     * @arg {string} uid - Identifiant de l'ex-membre
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.add}.
+     * Cette fonction supprime tous les droits de l'utilisateur sur le groupe, mais aussi sur les groupes sources si sont statut de membre était hérité.
+     * @arg {string} uid - Identifiant de l'ex-membre (pas supposé valide)
      * @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 remMember(uid: string, gid: string): Promise<boolean> {
-        try {
-            // Vérifie que l'utilisateur est pas déjà viré pour groupes
-            let lm = await Tools.getMembers(gid);
-            if (lm.includes(uid)) {
-                // Supprime tous les utilisateurs
-                if (!await Basics.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"; }
-                        });
-                    }
-                });
-            }
-        }
-        catch(err) {
-            throw "Erreur pour obtenir une liste de membres d'un groupe pour supprimer un membre du groupe.";
-        }
-        try {
-            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 Basics.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"; }
-                        });
-                    }
-                });
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur pour obtenir une liste de groupes d'un membres pour le supprimer du groupe.";
+        let stack = [];
+        let res = true;
+        let visited = {};
+        stack.push(gid);
+        while (stack.length>0) {
+            let cur_id = stack.pop();
+            if (visited[cur_id] == undefined) {
+                visited[cur_id] = true;
+                res = res   &&  await Tools.remove(uid, cur_id, "admins") &&
+                                await Tools.remove(uid, cur_id, "speakers") &&
+                                await Tools.remove(uid, cur_id, "members");
+                stack.concat(await Basics.searchSingle("group", ldapConfig.group.childs, cur_id));
         }
+        return  res;
     }
 
     /**
      * @memberof LDAP
-     * @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 membre futur admin
+     * @summary Fonction qui permet d'ajouter un sympathisant à un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.add}.
+     * @arg {string} uid - Identifiant de l'utilisateur à ajouter
      * @arg {string} gid - Identifiant du groupe
-     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @async
      * @static
      */
-    static async addAdmin(uid: string, gid: string): Promise<boolean> {
-        // Ajoute le membre au groupe avant d'en faire un admin
-        if (!await Group.addMember(uid,gid)) { throw "Erreur lors de l'ajout du futur admin en tant que membre."; }
-        try {
-            let la = await Tools.getAdmins(gid);
-            if (!la.includes(uid)) {
-                // Finalement modification, uniquement dans groups
-                let vals = {};
-                vals[ldapConfig.group.admins] = uid;
-                if (!await Basics.change("gr", gid, "add", vals)) {
-                    throw "Erreur lors de l'ajout de l'admin dans l'arbre des groupes.";
-                }
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
-        }
-    }
+    static async addFollower(uid: string, gid: string) : Promise<boolean> { return Tools.add(uid, gid, "followers"); }
 
     /**
      * @memberof LDAP
-     * @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 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
+     * @summary Fonction qui permet de supprimer un sympathisant d'un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.add}.
+     * @arg {string} uid - Identifiant de l'ex-sympathisant (pas supposé valide)
      * @arg {string} gid - Identifiant du groupe
-     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @async
      * @static
      */
-    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 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 Basics.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 => {
-                        if (!res) { throw "Erreur dans le réajout d'un des autres admins."; }
-                    }); }
-                });
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
-        }
-    }
+    static async remFollower(uid: string, gid: string): Promise<boolean> { return Tools.remove(uid, gid, "followers"); }
+
     /**
      * @memberof LDAP
      * @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 Group.addMember} et {@link Group.addAdmin}
+     * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Tools.add}
      * 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
@@ -272,7 +217,7 @@ export class Group {
         for (let key_att in data) { vals[ldapConfig.group[key_att]]=data[key_att] };
 
         // Appel à la fonction de base
-        if (!await Basics.add("gr", vals)) {
+        if (!await Basics.add("group", vals)) {
             throw "Erreur lors de la création d'une nouvelle feuille dans l'arbre des groupes.";
         }
         // Certains champs nécessitent de petits calculs
@@ -286,7 +231,7 @@ export class Group {
 
         // Génération id aléatoire et test contre le LDAP
         try {
-            Tools.generateId(ldapConfig.group["idNumber"], "gr").then(id => { vals2[ldapConfig.group['idNumber']]=id; });
+            Tools.generateId(ldapConfig.group["idNumber"], "group").then(id => { vals2[ldapConfig.group['idNumber']]=id; });
         }
         catch(err) {
             throw "Erreur lors de la génération d'un id numérique pour créer un nouveau groupe.";
@@ -308,29 +253,26 @@ export class Group {
         vals2[ldapConfig.group['writePerm']] = '!*';
 
         // Inscription des valeurs calculées par effet de bord
-        if (!await Basics.change("gr", gid, "add", vals2)) {
+        if (!await Basics.change("group", gid, "add", vals2)) {
             throw "Erreur lors de l'ajout des valeurs intelligentes du nouveau groupe.";
         }
 
         ["posixAccount", "posixGroup", "brAccount"].forEach(cst => {
             let vals3={};
             vals3[ldapConfig.group['classes']]=cst;
-            Basics.change("gr", gid, "add", vals3).then(res => {
+            Basics.change("group", gid, "add", vals3).then(res => {
                 if (!res) { throw "Erreur lors de l'ajout des valeurs constantes du nouveau groupe."; }
             });
         });
 
         // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
-        data['members'].forEach(uid => {
-            Group.addMember(uid, gid).then(res => {
-                if (!res) { throw "Erreur de l'ajout d'un membre au groupe."; }
-            });
-        });
-        data['admins'].forEach(uid => {
-            Group.addAdmin(uid, gid).then(res => {
-                if (!res) { throw "Erreur de l'ajout d'un admin au groupe."; }
+        for (let cat in categories) {
+            data[cat].forEach(uid => {
+                Tools.add(uid, gid, cat).then(res => {
+                    if (!res) { throw "Erreur de l'ajout d'un membre au nouveau groupe."; }
+                });
             });
-        });
+        }
         return true;
     }
 
@@ -338,7 +280,7 @@ export class Group {
      * @memberof LDAP
      * @summary Fonction qui supprime un groupe du LDAP.
      * @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.
+     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Tools.remove} 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
@@ -346,30 +288,31 @@ export class Group {
      */
     static async delete(gid: string): Promise<boolean> {
         try {
-            // Gestion des administrateur et membres d'abord
+            // Gestion des catégories en bloc d'abord
             let profil = await Group.peek(gid);
-            // 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 Basics.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"; }
-                            });
+            for (let cat of ["admins", "speakers", "members", "followers"]) {
+                profil[ldapConfig.group[cat]].forEach(async function quickPartRemUser(uid: string) {
+                    // Modification des profils de tous les utilisateurs
+                    let lg = await Tools.get(uid, "user", cat);
+                    // Vérifie que l'utilisateur est pas déjà viré pour users
+                    if (lg.includes(gid)) {
+                        // Supprime tous les groupes
+                        if (!await Basics.change("user", uid, "del", ldapConfig.user[cat])) {
+                            throw "Erreur lors de la suppression de tous les groupes du membre.";
                         }
-                    });
-                }
-            });
+                        // Les rajoute un par un, sauf pour le groupe supprimé
+                        lg.forEach(id => {
+                            if (id!=gid) {
+                                Tools.add(uid, id, cat).then(res => {
+                                    if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; }
+                                });
+                            }
+                        });
+                    }
+                });
+            }
             // Elimination
-            if (!await Basics.clear("gr",gid)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; }
+            if (!await Basics.clear("group",gid)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; }
             return true;
         }
         catch(err) {
@@ -388,7 +331,7 @@ export class Group {
      */
     static async edit(data: groupData) : Promise<boolean> {
         try {
-            return Tools.edit("gr",data);
+            return Tools.edit("group",data);
         }
         catch(err) {
             throw "Erreur lors de la modification d'un groupe.";
diff --git a/src/ldap/export/user.ts b/src/ldap/export/user.ts
index 8eb37d2..d893b77 100644
--- a/src/ldap/export/user.ts
+++ b/src/ldap/export/user.ts
@@ -3,60 +3,9 @@
  * @author hawkspar
  */
 
-import {ldapConfig} from '../internal/config';
+import {ldapConfig, userData, categories} from '../internal/config';
 import {Basics} from '../internal/basics';
 import {Tools} from '../internal/tools';
-import {Group} from './group';
-
-/**
- * @interface userData
- * @desc Interface avec toutes les données extractables pour un utilisateur.
- * @var {string} uid - Identifiant utilisateur
- * @var {string} givenName - Prénom
- * @var {string} lastName - Nom
- * @var {string} nickname - Surnom
- * @var {string} photo - Bytestring de la photo de l'utilisateur
- * @var {string} birthdate - Date d'anniversaire
- * TBA @var {string} nationality - Nationalité d'origine
- * @var {string} promotion - Année(s) de promo
- * @var {string} phone - Numéro(s) de téléphone
- * @var {string[]} address - Adresse(s)
- * @var {string[]} mail - Adresse(s) courriel
- * @var {string[]} groups - Un ou plusieurs groupes dont l'utilisateur est membre (inclus section sportive, binet, PA...)
- * @var {string} password - Mot de passe généré en amont
- * @var {string[]} ips - Adresse(s) ip
- * @var {string} directory - Adresse soft des données utilisateurs
- * @var {string} login - Astuce de root flemmard
- * @arg {string} readPerm - Permissions spéciales BR
- * @var {string} writePerm - Permissions spéciales BR
- * @var {string[]} forlifes - Alias BR (attention le filtre .fkz n'est plus fonctionnel)
- * @var {string[]} admins - Liste des gid dont l'utilisateur est admin ; supposé sous-liste de groups
- * TBA @var {string[]} likes - Liste des gid dont l'utilisateur est sympathisant
- */
-export class userData {
-    uid?: string;
-    groups?: string[];
-    groupsIsAdmin?: string[];
-    password?: string;
-    givenName?: string;
-    lastName?: string;
-    nickname?: string;
-    promotion?: string;
-    photo?: string;
-    birthdate?: string;
-    nationality?: string;
-    phone?: string;
-    address?: string;
-    mail?: string;
-    ips?: string[];
-    directory?: string;
-    login?: string;
-    readPerm?: string;
-    writePerm?: string;
-    forlifes?: string[];
-    sport?: string;
-    //"likes"?: string[]
-}
 
 //------------------------------------------------------------------------------------------------------------------------
 // Classes à exporter TBT
@@ -82,7 +31,7 @@ export class User {
      */
     static async peek(uid: string) : Promise<userData> {
         try { 
-            return Tools.peek<userData>("us", uid, userData);
+            return Tools.peek<userData>("user", uid, userData);
         }
         catch(err) {
             throw "Error while peeking a user.";
@@ -102,7 +51,7 @@ export class User {
      */
     static async search(data: userData) : Promise<string[]> {
         try {
-            return Tools.search("us", data);
+            return Tools.search("user", data);
         }
         catch(err) {
             throw "Erreur lors de la recherche approximative d'un utilisateur.";
@@ -112,8 +61,8 @@ export class User {
     /**
      * @memberof LDAP
      * @summary Fonction qui créé un nouvel utilisateur dans le LDAP.
-     * @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.
+     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link Tools.add} pour gérer les groupes du nouvel utilisateur.
+     * @arg {userData} 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
@@ -141,7 +90,7 @@ export class User {
         }
 
         // Appel à la fonction de base
-        if (!await Basics.add("us", vals)) { throw "Erreur de l'ajout de la feuille à l'arbre utilisateur."; }
+        if (!await Basics.add("user", vals)) { throw "Erreur de l'ajout de la feuille à l'arbre utilisateur."; }
         
         for (let key_att in data) {
             // Modifications multiples pour avoir plusieurs champs de même type ; boucle sur les attributs multiples
@@ -150,7 +99,7 @@ export class User {
                 data[key_att].forEach(val => {
                     let vals2 = {};
                     vals2[ldapConfig.user[key_att]]=val;
-                    Basics.change("us", uid, "add", vals2).then(res => {
+                    Basics.change("user", uid, "add", vals2).then(res => {
                         if (!res) { throw "Erreur lors de l'ajout d'une valeur pour un champ à valeurs multiples à la feuille du nouvel utilisateur."; }
                     });
                 });
@@ -173,7 +122,7 @@ export class User {
         }
         try {
             // Génération id aléatoire unique
-            vals3[ldapConfig.user['id']]= await Tools.generateId(ldapConfig.user['id'], "us");
+            vals3[ldapConfig.user['id']]= await Tools.generateId(ldapConfig.user['id'], "user");
         }
         catch(err) {
             throw "Erreur lors de la génération d'un id numérique pour un nouvel utilisateur.";
@@ -199,17 +148,24 @@ export class User {
         vals3[ldapConfig.user['idNum']] ='5000';
 
         // Inscription des valeurs calculées
-        if (!await Basics.change("us", uid, "add", vals3)) {
+        if (!await Basics.change("user", uid, "add", vals3)) {
             throw "Erreur lors de l'ajout des valeurs calculées à la feuille du nouvel utilisateur.";
         }
 
         ["posixAccount", "shadowAccount", "inetOrgPerson", "brAccount"].forEach(cst => {
             let val3={};
             vals3[ldapConfig.user['class']]=cst;
-            Basics.change("us", uid, "add", vals3).then(res => {
+            Basics.change("user", uid, "add", vals3).then(res => {
                 if (!res) { throw "Erreur lors de l'ajout d'une valeur constante à la feuille du nouvel utilisateur."; }
             });
         });
+
+        // Ajout dans les groupes à la catégorie voulue
+        for (let cat of categories) {
+            for (let gid of data[cat]) {
+                Tools.add(uid, gid, cat);
+            }
+        }
         return true;
     }
 
@@ -221,7 +177,7 @@ export class User {
      * @memberof LDAP
      * @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 Group.remMember} et {@link Group.remAdmin} pour gérer les groupes de l'utilisateur sortant.
+     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Tools.remove} 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
@@ -231,32 +187,32 @@ export class User {
         try {
             // Gestion des groupes d'abord
             let profil = await User.peek(uid);
-            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 Basics.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"; }
-                            });
+            for (let cat of categories) {
+                profil[ldapConfig.user[cat]].forEach(async function (gid: string) {
+                    // Enlever de la liste des membres
+                    let lm = await Tools.get(gid, "group", cat);
+                    if (lm.includes(uid)) {
+                        // Supprime tous les membres
+                        if (!await Basics.change("group", gid, "del", ldapConfig.group[cat])) {
+                            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) {
+                                Tools.add(id, gid, cat).then(res => {
+                                    if (!res) { throw "Erreur lors du ré-ajout d'un autre membre"; }
+                                });
+                            }
+                        });
+                    }
+                });
+            }
         }
         catch(err) {
             throw "Erreur lors de l'obtention des informations de l'utilisateur à supprimer.";
         }
         // Elimination
-        if (!Basics.clear("us", uid)) { throw "Erreur lors de la suppression de l'utilisateur."; }
+        if (!Basics.clear("user", uid)) { throw "Erreur lors de la suppression de l'utilisateur."; }
         return true;
     }
 
@@ -271,7 +227,7 @@ export class User {
      */
     static async edit(data : userData) : Promise<boolean> {
         try {
-            return Tools.edit("us",data);
+            return Tools.edit("user",data);
         }
         catch(err) {
             throw "Erreur lors de la modification d'un utilisateur.";
diff --git a/src/ldap/internal/basics.ts b/src/ldap/internal/basics.ts
index 10dfabe..7097d58 100644
--- a/src/ldap/internal/basics.ts
+++ b/src/ldap/internal/basics.ts
@@ -98,12 +98,11 @@ export class Basics {
      * @static
      * @async
      */
-    static search(domain: 'gr'|'us', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<void> {
+    static search(domain: 'group'|'user', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<void> {
         Basics.adminBind();
         let dn ="";
         if (id != null)     { dn+=ldapConfig.key_id+'='+ ldapEscape.dn("${txt}", { txt: id}) +','; }
-        if (domain == "gr") { dn+=ldapConfig.dn_groups; }
-        else                { dn+=ldapConfig.dn_users; }
+        dn+=ldapConfig.dn[domain];
         // Interrogation LDAP selon filter
         let promise = new Promise<void>(function(resolve, reject) {
             client.search(dn, {            // Must be escaped in case of a malignious false id
@@ -141,7 +140,7 @@ export class Basics {
      * @static
      * @async
      */
-    static async searchSingle(domain: 'gr'|'us', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> {
+    static async searchSingle(domain: 'group'|'user', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> {
         let vals=[];
         await Basics.search(domain, [attribute], id, filter,  entry => {
             // Cas un seul attribut où le résultat est une liste directement
@@ -164,7 +163,7 @@ export class Basics {
      * @static
      * @async
      */
-    static async searchMultiple(domain: 'gr'|'us', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> {
+    static async searchMultiple(domain: 'group'|'user', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> {
         let vals=[];
         await Basics.search(domain, attributes, id, filter,  entry => {
             // Cas plusieurs attributs donc résultat dictionnaire
@@ -191,11 +190,10 @@ export class Basics {
      * @static
      * @async
      */
-    static async change(domain: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> {
+    static async change(domain: 'group'|'user', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> {
         Basics.adminBind();
         let dn = ldapConfig.key_id+'='+id+','
-        if (domain == "gr") { dn+=ldapConfig.dn_groups }
-        else                { dn+=ldapConfig.dn_users }
+        dn+=ldapConfig.dn[domain];
         // Modification LDAP selon dn fourni en argument (pourrait prendre une liste de Changes)
         client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({
             operation: op,
@@ -220,11 +218,10 @@ export class Basics {
      * @static
      * @async
      */
-    static async add(domain: 'gr'|'us', vals) : Promise<boolean> {
+    static async add(domain: 'group'|'user', vals) : Promise<boolean> {
         Basics.adminBind();
         let dn = ldapConfig.key_id+"="+vals[ldapConfig.key_id];
-        if (domain == "gr") { dn+=ldapConfig.dn_groups; }
-        else                { dn+=ldapConfig.dn_users; }
+        dn+=ldapConfig.dn[domain];
         // Ajout LDAP selon la ldapConfiguration en argument
         client.add(ldapEscape.dn("${txt}", { txt: dn}), vals, err => {
             throw "Erreur lors d'une opération d'ajout sur le LDAP.";
@@ -245,11 +242,10 @@ export class Basics {
      * @static
      * @async
      */
-    static async clear(domain: 'gr'|'us', id: string) : Promise<boolean> {
+    static async clear(domain: 'group'|'user', id: string) : Promise<boolean> {
         Basics.adminBind();
         let dn = ldapConfig.key_id+'='+id+','
-        if (domain == "gr") { dn+=ldapConfig.dn_groups; }
-        else                { dn+=ldapConfig.dn_users; }
+        dn+=ldapConfig.dn[domain];
         // 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/internal/config.ts b/src/ldap/internal/config.ts
index ad76623..d836656 100644
--- a/src/ldap/internal/config.ts
+++ b/src/ldap/internal/config.ts
@@ -12,18 +12,103 @@
 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(__dirname,'..', '..', '..', 'ldap_config.json');
+let path_config = path.resolve(__dirname, '..', '..', '..', 'ldap_config.json');
 console.log(colors.cyan("Loading LDAP config file from "+path_config));
 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) {
     ldapConfig.server = process.env.LDAP_URI;
 }
 else {
-    if (process.env.TARGET_ENV == `production`)     { ldapConfig.server = ldapConfig.server_prod; }
-    else                                            { ldapConfig.server = ldapConfig.server_dev; }
+    if (process.env.TARGET_ENV == `production`)     { ldapConfig.server = ldapConfig.server.prod; }
+    else                                            { ldapConfig.server = ldapConfig.server.dev; }
+}
+
+// Gestion des super-identifiants
+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());
+
+// Data formats and useful constants
+export const categories = ["admins","speakers","members","followers"];
+
+/**
+ * @interface userData
+ * @desc Interface avec toutes les données extractables pour un utilisateur.
+ * @var {string?} uid - Identifiant utilisateur
+ * TBA @var {string[]?} admins - Liste des gid (group id, inclus section sportive, binet, PA...) dont l'utilisateur est admin ; pas forcément sous-liste de groups
+ * TBA @var {string[]?} speakers - Liste des gid dont l'utilisateur est porte-parole ; pas forcément sous-liste de groups
+ * @var {string[]?} members - Liste des gid dont l'utilisateur est membre
+ * TBA @var {string[]?} followers - Liste des gid dont l'utilisateur est sympathisant
+ * @var {string?} givenName - Prénom
+ * @var {string?} lastName - Nom
+ * @var {string?} nickname - Surnom
+ * @var {string?} photo - Bytestring de la photo de l'utilisateur
+ * @var {string?} birthdate - Date d'anniversaire
+ * TBA @var {string?} nationality - Nationalité d'origine
+ * @var {string?} promotion - Année(s) de promo
+ * @var {string?} phone - Numéro(s) de téléphone
+ * @var {string[]} address - Adresse(s)
+ * @var {string[]?} mail - Adresse(s) courriel
+ * @var {string?} password - Mot de passe généré en amont (utilisé seulement à l'initialialisation, pas stocké bien sûr)
+ * @var {string[]?} ips - Adresse(s) ip
+ * @var {string?} directory - Adresse soft des données utilisateurs
+ * @var {string?} login - Astuce de root flemmard
+ * @arg {string?} readPerm - Permissions spéciales BR
+ * @var {string?} writePerm - Permissions spéciales BR
+ * @var {string[]?} forlifes - Alias BR (attention le filtre .fkz n'est plus fonctionnel)
+ * @memberof LDAP
+ */
+export class userData {
+    uid?: string;
+    admins?: string[];
+    speakers?: string[];
+    members?: string[];
+    followers?: string[];
+    password?: string;
+    givenName?: string;
+    lastName?: string;
+    nickname?: string;
+    promotion?: string;
+    photo?: string;
+    birthdate?: string;
+    nationality?: string;
+    phone?: string;
+    address?: string;
+    mail?: string;
+    ips?: string[];
+    directory?: string;
+    login?: string;
+    readPerm?: string;
+    writePerm?: string;
+    forlifes?: string[];
+    sport?: string;
+}
+
+/**
+ * @interface groupData
+ * @var {string} gid - Identifiant 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
+ * @var {string} description - Description du groupe (facultatif)
+ * @var {string[]} childs - Liste des groupes enfants de première génération de celui-ci (les admins du groupe seront admins de ce groupe et des enfants suivants)
+ * @var {string[]} parents - Liste des groupes directement parents de celui-ci (les membres du groupe seront membres de ce groupe et des parents suivants) ; symétrique du précédent
+ * @memberof LDAP
+ */
+export class groupData {
+    gid: string;
+	name: string;
+	type: string;
+    members: string[];
+    speakers: string[];
+    followers: string[];
+    admins: string[];
+    description?: string;
+    childs?: string[];
+    parents?: string[];
 }
\ No newline at end of file
diff --git a/src/ldap/internal/tools.ts b/src/ldap/internal/tools.ts
index 4adaf70..acb065f 100644
--- a/src/ldap/internal/tools.ts
+++ b/src/ldap/internal/tools.ts
@@ -6,10 +6,8 @@
 // Toutes les entrées utilisateur sont escapées par sécurité
 import ldapEscape from 'ldap-escape';
 // Imports internes
-import {ldapConfig} from './config';
+import {ldapConfig, userData, groupData} from './config';
 import {Basics} from './basics';
-import {userData} from '../export/user';
-import {groupData} from '../export/group';
 
 //------------------------------------------------------------------------------------------------------------------------
 // Fonctions intermédiaires TBT
@@ -23,7 +21,7 @@ export class Tools {
      * @summary Constructeur vide.
      */
     constructor() {}
-     
+    
     /**
      * @memberof LDAP
      * @summary Fonction qui renvoit toutes les infos relatives à un groupe ou un utilisateur particulier.
@@ -35,13 +33,8 @@ export class Tools {
      * @static
      * @async
      */
-    static async peek<T>(domain: 'us'|'gr', id: string, type: new () => T) : Promise<T> {
-        if (domain=='gr') {
-            var dirtyKeys = ldapConfig.group;
-        }
-        else {
-            var dirtyKeys = ldapConfig.user;
-        }
+    static async peek<T>(domain: 'user'|'group', id: string, type: new () => T) : Promise<T> {
+        var dirtyKeys = ldapConfig[domain];
         let cleanData : T = new type();
         let attr = Object.keys(dirtyKeys).map(key => dirtyKeys[key]);
         //console.log(attr);
@@ -57,7 +50,6 @@ export class Tools {
         return cleanData;
     }
 
-    
     /**
      * @memberof LDAP
      * @summary Fonction qui retrouve les id des paxs ou groupes validant les critères de recherche. Etape vers vrai TOL (Trombino On Line).
@@ -73,7 +65,7 @@ export class Tools {
      * @static
      * @async
      */
-    static async search(domain : "us"|"gr", data : userData|groupData) : Promise<string[]> {
+    static async search(domain: "user"|"group", 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) {
@@ -83,8 +75,7 @@ export class Tools {
                 data[key].forEach(val => {
                     // Traduction en language LDAP
                     let attribute = "";
-                    if (domain="us")    { attribute = ldapConfig.user[key]; }
-                    else                { attribute = ldapConfig.group[key]; }
+                    attribute = ldapConfig[domain][key];
                     // Escape user input
                     val = ldapEscape.filter("${fil}", { fil: val});
                     // Creation incrémentale du filtre
@@ -109,15 +100,10 @@ export class Tools {
      * @async
      * @static
      */
-    static async edit(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;
-        }
+    static async edit(domain: "user"|"group", data: userData|groupData) : Promise<boolean> {
+        if (domain == "user")   { var id=data['uid']; }
+        else                    { var id=data['gid']; }
+        var dirtyKeys=ldapConfig[domain];
         // Rename in an LDAP-friendly way
         let dirtyData = {};
         Object.keys(data).forEach(function(key: string) {
@@ -129,6 +115,109 @@ export class Tools {
         return Basics.change(domain,id,"replace",dirtyData);
     }
 
+    /**
+     * @memberof LDAP
+     * @summary Fonction qui permet d'ajouter un utilisateur à une catégorie  d'un groupe.
+     * @desc Cette fonction fait essentiellement appel à d'autres fonctions de {@link Tools} passées en argument et {@link LDAP.change}.
+     * Cette fonction ne créé pas de doublon et opère conjointement dans les deux arbres "group" et "user"
+     * @arg {string} uid - Identifiant du futur membre
+     * @arg {string} gid - Identifiant du groupe
+     * @arg {"admins"|"speakers"|"members"|"followers"} category - Categorie de l'utilisateur concerné (admin, speaker, member ou follower)
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async add(uid: string, gid: string, category: string): Promise<boolean> {
+        try {
+            // Vérifie que l'utilisateur est pas déjà membre pour groupes
+            let lu = await Tools.get(gid, "group", category);
+            let catName = ldapConfig.group[category];
+            if (!lu.includes(uid)) {
+                // Ajoute l'utilisateur dans la catégorie concernée
+                if (!await Basics.change("group", gid, "add", catName)) {
+                    throw "Erreur lors de la modification dans l'arbre des groupes pour ajouter un membre dans la catégorie voulue.";
+                }
+            }
+        }
+        catch(err) {
+            throw "Erreur pour obtenir une liste de membres d'une catégorie d'un groupe pour ajouter un membre de cette categorie du groupe.";
+        }
+        try {
+            // Vérifie que l'utilisateur est pas déjà membre pour user
+            let lg = await Tools.get(uid, "user", category);
+            let catName = ldapConfig.user[category];
+            if (!lg.includes(gid)) {
+                // Ajoute l'utilisateur dans la categorie voulue
+                if (!await Basics.change("user", uid, "add", catName)) {
+                    throw "Erreur lors de l'ajout d'un utilisateur dans une catégorie d'un groupe.";
+                }
+            }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur pour obtenir une liste de groupes d'une categorie d'un membre pour ajouter un groupe de cette category pour le membre.";
+        }
+    }
+
+    /**
+     * @memberof LDAP
+     * @summary Fonction qui permet de supprimer un membre d'une catégorie existant d'un groupe.
+     * @desc Cette fonction fait essentiellement appel à d'autres fonctions de {@link Tools} passées en argument et {@link LDAP.change}.
+     * @arg {string} uid - Identifiant de l'ex-membre
+     * @arg {string} gid - Identifiant du groupe
+     * @arg {"admins"|"speakers"|"members"|"followers"} category - Categorie de l'utilisateur concerné (admin, speaker, member ou follower)
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async remove(uid: string, gid: string, category : string): Promise<boolean> {
+        try {
+            // Vérifie que l'utilisateur est pas déjà viré pour groupes
+            let lu = await Tools.get(gid, "group", category);
+            let catName = ldapConfig.group[category];
+            if (lu.includes(uid)) {
+                // Supprime tous les utilisateurs
+                if (!await Basics.change("group", gid, "del", catName)) {
+                    throw "Erreur lors de la suppression de tous les membres d'une catégorie du groupe.";
+                }
+                // Les rajoute un par un, sauf pour le supprimé
+                lu.forEach(id => {
+                    if (id!=uid) {
+                        Tools.add(id, gid, category).then(res => {
+                            if (!res) { throw "Erreur lors du ré-ajout d'un autre membre d'une catégorie."; }
+                        });
+                    }
+                });
+            }
+        }
+        catch(err) {
+            throw "Erreur pour obtenir une liste de membres d'une catégorie d'un groupe pour supprimer un membre de cette categorie du groupe.";
+        }
+        try {
+            // Vérifie que l'utilisateur est pas déjà viré pour user
+            let lg = await Tools.get(uid, "user", category);
+            let catName = ldapConfig.user[category];
+            if (lg.includes(gid)) {
+                // Supprime tous les groupes de la catégorie pour l'utilisateur
+                if (!await Basics.change("user", uid, "del", catName)) {
+                    throw "Erreur lors de la suppression de tous les groupes d'un membre.";
+                }
+                // Les rajoute un par un, sauf pour le supprimé
+                lg.forEach(id => {
+                    if (id!=uid) {
+                        Tools.add(id, gid, category).then(res => {
+                            if (!res) { throw "Erreur lors du ré-ajout d'un autre groupe."; }
+                        });
+                    }
+                });
+            }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur pour obtenir une liste de groupes d'une categorie d'un membre pour supprimer un groupe de cette category pour le membre.";
+        }
+    }
+    
     /**
      * @callback changeValueCallback
      * @param {string} id - Id à modifier
@@ -151,7 +240,7 @@ export class Tools {
      * @static
      * @async
      */
-    static async ensureUnique(value: string, attribute: string, domain: 'gr'|'us', changeValue: (string, number) => string, n: number=0) : Promise<string> {
+    static async ensureUnique(value: string, attribute: string, domain: 'group'|'user', changeValue: (string, number) => string, n: number=0) : Promise<string> {
         // Recherche d'autres occurences de l'id
         try {
             return Basics.searchSingle(domain, ldapConfig.key_id, null, "("+attribute+"="+value+")").then(function (matches: string[]) {
@@ -181,7 +270,7 @@ export class Tools {
     static async generateUid(givenName: string, lastName: string, promotion: string) : Promise<string> {
         try {
             // normalize et lowerCase standardisent le format
-            return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "us", (id: string, n: number) => {
+            return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "user", (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
                 else if (n>2) { id+=n; }                        // Ensuite on continue .123, .1234, etc...
@@ -205,7 +294,7 @@ export class Tools {
     static async generateReadableId(name: string) : Promise<string> {
         try {
             // normalize et lowerCase standardisent le format
-            return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "gr", (id: string, n: number) => {
+            return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "group", (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...
                 return id;
@@ -225,7 +314,7 @@ export class Tools {
      * @static
      * @async
      */
-    static async generateId(attribut: string, domain: "gr"|"us") : Promise<string> {
+    static async generateId(attribut: string, domain: "group"|"user") : Promise<string> {
         try {
             return Tools.ensureUnique("0", attribut, domain, (id,n) => { return Math.floor((Math.random() * 100000) + 1).toString(); });
         }
@@ -236,57 +325,43 @@ export class Tools {
 
     /**
      * @memberof LDAP
-     * @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
+     * @summary Fonction qui retrouve les utilisateurs ou groupes respectivement correspondant à un groupe ou un utilisateur de la même catégorie.
+     * @desc Cette fonction utilise {@link LDAP.search} et va directement à la feuille de l'utilisateur ou du groupe interrogé.
+     * Pour autant, elle est moins naïve qu'elle en a l'air. Elle gère la descente des admins et la remontée des membres.
+     * @param {string} id - Identifiant du groupe ou de l'individu à interroger (supposé valide)
+     * @param {"user"|"group"} domain - Arbre à interroger 
+     * @param {"admins"|"speakers"|"members"|"followers"} category - Catégorie considérée 
+     * @return {Promise(string[])} Liste des id de groupes ou d'utilisateurs de la bonne catégorie associé à l'id
      * @static
      * @async
      */
-    static async getGroups(uid: string) : Promise<string[]> {
+    static async get(id : string, domain : "user"|"group", category : string): Promise<string[]> { //"admins"|"speakers"|"members"|"followers") {
         try {
-            return Basics.searchSingle("us", ldapConfig.user.groups, uid);
+            if (!(category in ["admins","members"]) || domain=="group") {
+                return await Basics.searchSingle(domain, ldapConfig[domain][category], id);
+            }
+            else {
+                // Clean depth-first search for inherited members and admins
+                let stack = [];
+                let res = [];
+                let visited = {};
+                stack.push(id);
+                while (stack.length>0) {
+                    let cur_id = stack.pop();
+                    if (visited[cur_id] == undefined) {
+                        visited[cur_id] = true;
+                        res.concat(await Basics.searchSingle("group", ldapConfig.group[category], cur_id));
+                        // In the end, the precise category only changes the iteration direction
+                        if (category == "members")  { stack.concat(await Basics.searchSingle("group", ldapConfig.group.childs, cur_id)); }
+                        else                        { stack.concat(await Basics.searchSingle("group", ldapConfig.group.parents, cur_id)); }
+                    }
+                }
+            }
         }
         catch(err) {
             throw "Erreur lors de la recherche des groupes d'un individu.";
         }
     }
-    
-    /**
-     * @memberof LDAP
-     * @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 ldapConfig.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
-     * @async
-     */
-    static async getMembers(gid: string) : Promise<string[]> {
-        try {
-            return Basics.searchSingle("gr", ldapConfig.group.members, gid);
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche des membres d'un groupe.";
-        }
-    }
-    
-    /**
-     * @memberof LDAP
-     * @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 ldapConfig.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
-     * @async
-     */
-    static async getAdmins(gid: string) : Promise<string[]> {
-        try {
-            return Basics.searchSingle("gr", ldapConfig.group.admins, gid);
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche des admins d'un groupe.";
-        }
-    }
 
     /**
      * @memberof LDAP
@@ -300,8 +375,8 @@ export class Tools {
      */
     static async isGroupMember(uid: string, gid: string) : Promise<boolean> {
         try {
-            let lg = await Tools.getGroups(uid);
-            let lm = await Tools.getMembers(gid);
+            let lg = await Tools.get(uid, "user", "members");
+            let lm = await Tools.get(gid, "group", "members");
             if (lg.includes(gid) && lm.includes(uid)) {
                 return true;
             }
@@ -324,8 +399,8 @@ export class Tools {
      */
     static async isGroupAdmin(uid: string, gid: string) : Promise<boolean> {
         try {
-            let lm = await Tools.getMembers(gid);
-            let la = await Tools.getAdmins(gid);
+            let lm = await Tools.get(uid, "user", "admins");
+            let la = await Tools.get(gid, "group", "admins");
             if (la.includes(uid) && lm.includes(uid)) { return true; }
             else { return false; }
         }
-- 
GitLab