Forked from an inaccessible project.
-
Olivér FACKLAM authoredOlivér FACKLAM authored
tools.ts 24.39 KiB
/**
* @file Ce fichier regroupe les fonctions génériques de recherche et de test utiles, mais trop puissantes pour être exportées directement.
* @author hawkspar
*/
// Toutes les entrées utilisateur sont escapées par sécurité
import ldapEscape from 'ldap-escape';
// Imports internes
import { ldapConfig, userData, groupData } from './config';
import {Basics} from './basics';
//------------------------------------------------------------------------------------------------------------------------
// Fonctions intermédiaires TBT
//------------------------------------------------------------------------------------------------------------------------
export class Tools {
/**
* @memberof LDAP
* @class Tools
* @classdesc Cette classe contient des fonctions intermédiaires qui ne sont pas destinées à être utilisées dans les resolvers.
* @summary Constructeur vide.
*/
constructor() {}
/**
* @memberof LDAP
* @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. Elle est naïve et n'opère pas de récursion.
* @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 peek<T>(domain: 'user'|'group', id: string, type: new () => T) : Promise<T> {
// Utrilisation du tableu d'équivalence dans le fichier de configuration
let map = ldapConfig[domain];
let cleanKeys = Object.keys(map);
let dirtyKeys = cleanKeys.map(key => map[key]);
// Création de la structure de données renvoyée
let cleanData: T = new type();
let dirtyData = (await Basics.searchMultiple(domain, dirtyKeys, id))[0];
// Renommage des clés
for(let cleanKey of cleanKeys) {
let val = dirtyData[map[cleanKey]];
if (val !== undefined) {
if (Array.isArray(cleanData[cleanKey]) && !Array.isArray(val)) val = [val];
cleanData[cleanKey] = val;
}
}
return cleanData;
}
/**
* @memberof LDAP
* @summary Fonction qui retrouve les ids 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.
* @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData})
* @arg {"us"|"gr"} domain - Domaine de la recherche (utilisateur ou groupe)
* @arg {userData | groupData} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse
* comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exemple pour chercher un membre
* de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined.
* @return {Promise(string[])} ids des profils qui "match" les critères proposés.
* @static
* @async
*/
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 ldapConfig[domain]) {
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]]; // 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 = "";
attribute = ldapConfig[domain][key];
// Escape user input
val = ldapEscape.filter("${fil}", { fil: val });
// 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
if (domain == "group") var att=ldapConfig.group.gid;
else var att=ldapConfig.user.uid;
return Basics.searchSingle(domain, att, null, filter);
}
/**
* @memberof LDAP
* @summary Fonction qui édite un groupe ou utilisateur existant dans le LDAP. N'agit pas sur l'apprtenance à un groupe.
* @desc Appelle {@link LDAP.change}.
* @arg {"us"|"gr"} domain - Domaine de l'opération' (utilisateur ou groupe).
* @arg {userData | groupData} data - Dictionnaire avec les nouvelles valeurs de la feuille.
* @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon.
* @async
* @static
*/
static async 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) {
// Some values edit can't change
if (!['admins','speakers','members','followers','directory','classes','id','cleanFullName'].includes(key)) {
dirtyData[dirtyKeys.key]=data[key];
}
});
return Basics.change(domain, id, "replace", dirtyData);
}
/**
* @memberof LDAP
* @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 ne gère ni la descente des admins ni la remontée des membres et renvoit une réponse naïve.
* @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 get(id : string, domain : "user"|"group", category : string): Promise<string[]> {
try {
if (domain == "group") var dirtyId = ldapConfig.group.gid;
else var dirtyId = ldapConfig.user.uid;
dirtyId += "="+ldapEscape.filter("${txt}", { txt: id }) + "," + ldapConfig.dn[domain];
return await Basics.searchSingle(domain, ldapConfig[domain][category], dirtyId);
}
catch(err) {
throw "Erreur lors d'une recherche générique d'un membre d'une certaine catégorie d'un groupe.";
}
}
/**
* @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".
* Dans l'arbre user, elle gère les récusrions et les inclusions de droit.
* @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.";
}
}
}
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."; }
// Partie interne à l'X non propre à Sigma
// Seule catégorie non récursive
if (category != "followers") {
if (category == "admins") {
// Admin => speaker & member
let spk = ldapConfig.group.speakers;
Basics.change("user", uid, "add", { spk: ldapConfig.group.gid+"="+gid+","+ldapConfig.dn.group });
let mem = ldapConfig.group.members;
Basics.change("user", uid, "add", { mem: ldapConfig.group.gid+"="+gid+","+ldapConfig.dn.group });
// Cas récursif descendant
let to_visit = [gid];
let adm = ldapConfig.group.admins;
while (to_visit.length > 0) {
let cur_gid = to_visit.pop();
Basics.change("user", uid, "add", { adm: ldapConfig.group.gid+"="+cur_gid+","+ldapConfig.dn.group });
to_visit.concat(await Basics.searchSingle("group", ldapConfig.group.childs, cur_gid));
}
}
else if (category == "speakers") {
// Speaker => member
let mem = ldapConfig.group.members;
Basics.change("user", uid, "add", { mem: ldapConfig.group.gid+"="+gid+","+ldapConfig.dn.group });
}
// Cas récursif ascendant
let to_visit = [gid];
let mem = ldapConfig.group.members;
while (to_visit.length > 0) {
let cur_gid = to_visit.pop();
Basics.change("user", uid, "add", { mem: ldapConfig.group.gid+"="+cur_gid+","+ldapConfig.dn.group });
to_visit.concat(await Basics.searchSingle("group", ldapConfig.group.parents, cur_gid));
}
}
return true;
}
/**
* @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!=gid) {
Tools.add(uid, id, category).then(res => {
if (!res) throw "Erreur lors du ré-ajout d'un autre groupe.";
});
}
});
}
}
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.";
}
// Partie interne à l'X non propre à Sigma
// Seule catégorie non récursive
if (category != "followers") {
if (category in ["admins", "members"]) {
// Cas récursif descendant
let to_visit = [gid];
let adm = ldapConfig.group.admins;
while (to_visit.length > 0) {
let cur_gid = to_visit.pop();
// Si uid était un admin hérité
if (!(uid in await Tools.get(cur_gid, "group", "admins"))) {
let lg = await Tools.get(uid, "user", "admins");
if (!await Basics.change("user", uid, "del", adm)) {
throw "Erreur lors de la suppression de tous les groupes d'un membre spé X.";
}
// Les rajoute un par un, sauf pour le supprimé
lg.forEach(id => {
if (id!=gid) Tools.add(uid, id, "admins").then(res => {
if (!res) throw "Erreur lors du ré-ajout d'un autre groupe spé X.";
});
});
to_visit.concat(await Basics.searchSingle("group", ldapConfig.group.childs, cur_gid));
}
}
}
// Très violent ; virer un membre c'est aussi le virer de ses postes de porte-parole, d'admin et de groupes hérités
if (category == "members") {
let spk = ldapConfig.group.speakers;
let lg = await Tools.get(uid, "user", "speakers");
if (!await Basics.change("user", uid, "del", spk)) {
throw "Erreur lors de la suppression de tous les groupes d'un membre spé X.";
}
// Les rajoute un par un, sauf pour le supprimé
lg.forEach(id => {
if (id!=gid) Tools.add(uid, id, "speakers").then(res => {
if (!res) throw "Erreur lors du ré-ajout d'un autre groupe spé X.";
});
});
// Cas récursif montant
let to_visit = [gid];
let mem = ldapConfig.group.members;
while (to_visit.length > 0) {
let cur_gid = to_visit.pop();
// Si uid était un membre hérité
if (!(uid in await Tools.get(cur_gid, "group", "admins"))
&& !(uid in await Tools.get(cur_gid, "group", "speakers"))
&& !(uid in await Tools.get(cur_gid, "group", "members"))) {
let lg = await Tools.get(uid, "user", "members");
if (!await Basics.change("user", uid, "del", mem)) {
throw "Erreur lors de la suppression de tous les groupes d'un membre spé X.";
}
// Les rajoute un par un, sauf pour le supprimé
lg.forEach(id => {
if (id!=gid) Tools.add(uid, id, "admins").then(res => {
if (!res) throw "Erreur lors du ré-ajout d'un autre groupe spé X.";
});
});
to_visit.concat(await Basics.searchSingle("group", ldapConfig.group.parents, cur_gid));
}
}
}
}
return true;
}
/**
* @callback changeValueCallback
* @param {string} id - Id à modifier
* @param {number} n - Nombre d'itérations
* @return {string} Nouveau id
*/
/**
* @memberof LDAP
* @summary Cette fonction teste une valeur d'un attribut (typiquement un identifiant) et le fait évoluer jusqu'à ce qu'il soit unique.
* @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.search} pour vérifier
* qu'il n'y a pas d'autres occurences de cette valeur pour cette attribut
* dans le dn fourni.
* @param {string} value - Valeur de l'attribut (le plus souvent un identifiant) à tester à cette itération
* @param {string} attribute - Attribut à tester
* @param {"gr"|"us"} domain - Domaine dans lequel l'attribut doit être unique
* @param {changeValueCallback} changeValue - Fonction qui prend uniquement en argument l'id courant et
* le nombre d'itérations et qui renvoit la prochaine valeur de l'attribut
* @param {number} n [0] - Nombre d'itérations (à initialiser à 0)
* @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
* @static
* @async
*/
static async ensureUnique(value: string, attribute: string, domain: 'group'|'user', changeValue: (string, number) => string, n: number=0) : Promise<string> {
// Recherche d'autres occurences de l'id
try {
if (domain == "group") var att=ldapConfig.group.gid;
else var att=ldapConfig.user.uid;
let matches = await Basics.searchSingle(domain, att, null, "("+attribute+"="+ldapEscape.filter("${txt}", { txt: value })+")")
if (!matches) { throw ""; }
// On renvoit la valeur si elle est bien unique
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);
}
catch(err) {
throw "Erreur lors de la recherche d'une valeur pour assurer son unicité.";
}
}
/**
* @memberof LDAP
* @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique.
* @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
* @param {"group"|"user"} domain - Arbre à parcourir
* @param {string} givenName - Prénom
* @param {string} lastName - Nom
* @param {string} promotion - Année de promotion
* @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
* @static
* @async
*/
static async generateUid(domain : "group"|"user", givenName: string, lastName: string, promotion: string) : Promise<string> {
try {
if (domain == "group") var att=ldapConfig.group.gid;
else var att=ldapConfig.user.uid;
// normalize et lowerCase standardisent le format
return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), att, "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...
return id;
});
}
catch(err) {
throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
}
}
/**
* @memberof LDAP
* @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique.
* @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
* @param {"group"|"user"} domain - Arbre à parcourir
* @param {string} name - Nom
* @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
* @static
* @async
*/
static async generateReadableId(domain : "group"|"user", name: string) : Promise<string> {
try {
if (domain == "group") var att=ldapConfig.group.gid;
else var att=ldapConfig.user.uid;
// normalize et lowerCase standardisent le format
return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), att, domain, (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;
});
}
catch(err) {
throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
}
}
/**
* @memberof LDAP
* @summary Cette fonction teste une valeur dummy (0) pour un identifiant numérique puis le fait évoluer aléatoirement (entre 1 et 100 000) jusqu'à ce qu'il soit unique.
* @param {string} attribute - Intitulé exact de l'id concerné
* @param {"gr"|"us"} domain - Domaine dans lequel l'attribut doit être unique
* @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
* @static
* @async
*/
static async generateId(attribute: string, domain: "group"|"user") : Promise<string> {
try {
return Tools.ensureUnique("0", attribute, domain, (id,n) => { return Math.floor((Math.random() * 100000) + 1).toString(); });
}
catch(err) {
throw "Erreur lors de l'assurance de l'unicité d'un unique identifier numérique.";
}
}
}