/** * @file Ce fichier contient la classe de l'API du LDAP qui gère les opérations sur les groupes. * @author hawkspar */ import { ldapConfig } from './config'; import {LDAP} from './basics'; import {Tests} from './utilities'; /** * @interface groupData * @var {string} gid - Identifiant du groupe * @var {string} name - Nom du groupe * @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 interface groupData { "gid": string, "name": string, "type": string, "members": string[], "admins": string[], "description"?: string } //------------------------------------------------------------------------------------------------------------------------ // Classes à exporter TBT //------------------------------------------------------------------------------------------------------------------------ export class Group { /** * @class Cette classe est une des deux classes exportables permettant de faire des opérations sur les groupes. * @summary Constructeur vide. */ constructor() {} /** * @summary Fonction qui renvoit toutes les infos relatives à un groupe particulier. * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis. * @arg {string} gid - Identifiant du groupe * @return {Promise(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. * @static * @async */ static async peek(gid: string) : Promise<groupData> { try { let fields = []; fields.push(ldapConfig.group.values()); let LDAPGroupData = await LDAP.search("gr", fields, gid); let cleanGroupData : groupData; // Rename output for (let uncleanKey in LDAPGroupData) { for (let cleanKey in cleanGroupData) { if (uncleanKey==ldapConfig.group[cleanKey]) { cleanGroupData[cleanKey] = LDAPGroupData[uncleanKey]; } } } return cleanGroupData; } catch(err) { throw "Erreur lors d'une recherche d'informations sur un 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). * @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. * @return {Promise(string[])} Liste des gid dont le nom ressemble à l'input. * @static * @async */ static async search(input: string) : Promise<string[]> { try { // Construction du filtre custom let filter= "(|("+ldapConfig.key_id+"="+ input+")" + // On cherche la valeur exacte "(|("+ldapConfig.key_id+"=*"+input+")" + // La valeur finale avec des trucs avant ; wildcard * "(|("+ldapConfig.key_id+"=*"+input+"*)"+ // La valeur du milieu avec des trucs avant et après "("+ ldapConfig.key_id+"="+ input+"*))))"; // La valeur du début avec des trucs après // Appel rechercheLDAP avec filtre de l'espace return LDAP.search("gr", [ldapConfig.key_id], null, filter); } catch(err) { throw "Erreur lors de la recherche approximative d'un groupe."; } } 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 Tests.getMembers(gid); if (!lm.includes(uid)) { let vals = {}; vals[ldapConfig.group.members] = uid; // Erreur si pb lors de la modification if (!await LDAP.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 Tests.getGroups(uid); if (!lg.includes(gid)) { let vals2 = {}; vals2[ldapConfig.user.groups] = gid; // Erreur si pb lors de la modification if (!await LDAP.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."; } } /** * @summary Fonction qui permet de supprimer un membre existant d'un groupe. * @desc Cette fonction fait essentiellement appel à {@link LDAP.search}, {@link LDAP.change}, {@link Open.getGroups} et {@link Open.getMembers}. * @arg {string} uid - Identifiant de l'ex-membre * @arg {string} gid - Identifiant du groupe * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon * @async * @static */ static async delMember(uid: string, gid: string): Promise<boolean> { try { // Vérifie que l'utilisateur est pas déjà viré pour groupes let lm = await Tests.getMembers(gid); if (lm.includes(uid)) { // Supprime tous les utilisateurs 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) { this.addMember(id, gid).then(res => { if (!res) { throw "Erreur lors du ré-ajout des autres membres"; } }); } }); } } catch(err) { throw "Erreur pour obtenir une liste de membres d'un groupe pour supprimer un membre du groupe."; } try { let lg = await Tests.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) { this.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."; } } /** * @summary Fonction qui permet de promouvoir membre au stade d'administrateur d'un groupe. * @desc Cette fonction fait essentiellement appel à {@link Admin.addGroupMember} {@link LDAP.change} et {@link Open.getAdmins}. Elle n'autorise pas * les doublons et opère dans les deux dns users et groups. * @arg {string} uid - Identifiant du futur membre * @arg {string} gid - Identifiant du groupe * @return {boolean} `true` si la modification s'est bien déroulée, false sinon * @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 Tests.getAdmins(gid); if (!la.includes(uid)) { // Finalement modification, uniquement dans groups let vals = {}; vals[ldapConfig.group.admins] = uid; if (!await LDAP.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."; } } /** * @summary Fonction qui permet de rétrograder un membre du stade d'administrateur d'un groupe au stade d'utilisateur. * @desc Cette fonction fait essentiellement appel à {@link LDAP.change}. * @arg {string} uid - Identifiant du futur membre * @arg {string} gid - Identifiant du groupe * @return {boolean} `true` si la modification s'est bien déroulée, false sinon * @async * @static */ static async delAdmin(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.delMember(uid, gid) && Group.addMember(uid,gid))) { throw "Erreur dans l'éjection/réadmission du futur admin."; } try { // Vérifie que l'utilisateur est bien admin (comme dans delGroupMember) let la = await Tests.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."; } // 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."; } } /** * @summary Fonction qui créé un nouveau groupe dans le LDAP. * @desc Cette fonction fait une utilisation massive d'eval pour anonymiser son code ; c'est mal et cela suppose que beaucoup de soins ont été pris lors de * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Admin.addMemberGroup} et {@link Admin.addAdminGroup} * pour gérer les groupes du nouvel utilisateur. Attention une manip FOIREUSE est cachée dedans. * @arg {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 * @async * @static */ static async create(data: groupData) : Promise<boolean> { // Calcul d'un dictionnaire d'ajout let vals = {}; // gid de base généré à partir du nom standardisé, pas à partir de l'entrée 'gid' ! try { Tests.generateReadableId(data['name']).then(id => { vals[ldapConfig.key_id]=id; vals[ldapConfig.group['name']]=id; }); } catch(err) { throw "Erreur lors de la génération d'un hruid pour créer un nouveau groupe."; } 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] }; // Appel à la fonction de base if (!await LDAP.add("gr", vals)) { throw "Erreur lors de la création d'une nouvelle feuille dans l'arbre des groupes."; } // Certains champs nécessitent de petits calculs let vals2={}; // Encore un cahmp redondant vals2[ldapConfig.group['adress']] = gid; // ?! vals2[ldapConfig.group['password']] = ''; // Génération id aléatoire et test contre le LDAP try { Tests.generateId(ldapConfig.group["idNumber"], "gr").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."; } // FOIREUX : Hypothèse sur la structure du reste des données mais évite un test.assurerUnicite à deux variables vals2[ldapConfig.group['idNumber2']]=vals2[ldapConfig.group['idNumber']]; // Stockage machine ; dépend du prénom vals2[ldapConfig.group['directory']] = '/hosting/groups/'+gid; // Code root vals2[ldapConfig.group['cleanFullName']]=data['name'].replace(':', ';').toLowerCase().normalize('UFD'); // Adressage root vals2[ldapConfig.group['login']] = "/sbin/nologin"; // Permissions BR vals2[ldapConfig.group['readPerm']] = '!*'; vals2[ldapConfig.group['writePerm']] = '!*'; // Inscription des valeurs calculées par effet de bord if (!await LDAP.change("gr", 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; LDAP.change("gr", 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."; } }); }); 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. * @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> { try { // Gestion des membres et administrateurs d'abord let profil = await Group.peek(gid); // Ordre important profil[ldapConfig.group['admin']].forEach( id => { Group.delAdmin( 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.delMember(id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un membre."; } }); }); // Elimination if (!await LDAP.clear("gr",gid)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; } return true; } catch(err) { throw "Erreur lors de l'obtention du profil d'un groupe pour le supprimer."; } } /** * @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. * @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 { let gid = data['gid']; // Récupération des anciennes données let profil = await Group.peek(gid); // Surcharge des champs à modifier selon data for (let key in data) { profil[key]=data[key]; } // Modification propre if (!await Group.delete(gid) && await Group.create(profil)) { throw "Erreur de la destruction/recréation du groupe pour le modifier."; } return true; } catch(err) { throw "Erreur lors de l'obtention du profil d'un groupe pour le modifier."; } } }