import RoleUtils from '~/services/rights/roleUtils' import {AbilitiesType} from '~/types/interfaces' import YamlDenormalizer from "~/services/data/serializer/denormalizer/yamlDenormalizer"; import {MongoAbility} from "@casl/ability/dist/types/Ability"; import {AnyJson} from "~/types/data"; import {useEach} from "#imports"; import {ABILITIES} from "~/types/enum/enums"; /** * Classe permettant de mener des opérations sur les habilités */ class AbilityUtils { private readonly ability: MongoAbility = {} as MongoAbility private readonly accessProfile: any private readonly organizationProfile: any /** * @constructor */ constructor( ability: MongoAbility, accessProfile: any, organizationProfile: any, ) { this.ability = ability this.accessProfile = accessProfile this.organizationProfile = organizationProfile } /** * Définit les abilities de l'utilisateur selon son profil */ setupAbilities() { // Nécessaire pour que l'update des habilités soit correcte après la phase SSR this.ability.update(this.accessProfile.abilities) // const abilities: Array = this.buildAbilities(); // this.accessProfile.abilities = abilities // this.ability.update(abilities) // Au moment où l'on effectue une action organizationProfileStore.setProfile, il faut aller récupérer // les différentes habilités que l'utilisateur peut effectuer. (Tout cela se passe en SSR) const unsubscribe = this.organizationProfile.$onAction(({ name, // name of the action store, // store instance, same as `someStore` args, // array of parameters passed to the action after, // hook after the action returns or resolves onError, // hook if the action throws or rejects }: any) => { after((result: any)=>{ if (name === 'setProfile'){ //On récupère les habilités const abilities: Array = this.buildAbilities(); //On les store puis on update le service ability pour le mettre à jour. this.accessProfile.abilities = abilities this.ability.update(abilities) // Unsubscribe pour éviter les memory leaks unsubscribe() } }) }) } /** * Récupération de l'ensemble des habilités de l'utilisateur, qu'elles soient par Roles ou par Config * * @return {Array} */ buildAbilities(): Array { const abilitiesByRoles: Array = this.buildAbilitiesFromRoles(this.accessProfile.roles) const abilitiesByConfig = this.buildAbilitiesFromConfig('./config/abilities/config.yaml') return abilitiesByRoles.concat(abilitiesByConfig) } /** * Adaptation et transformations des roles symfony en abilities Casl * * @param {Array} roles * @return {Array} */ buildAbilitiesFromRoles(roles: Array): Array { return RoleUtils.rolesToAbilities(roles) } /** * Charge les habilités depuis les fichiers de configuration * * @param {string} configPath * @return {Array} */ buildAbilitiesFromConfig(configPath: string): Array { const doc = YamlDenormalizer.denormalize({path: configPath}) const fromConfig = doc.abilities const abilities: Array = [] useEach(fromConfig, (ability: { action: ABILITIES, services: object }, subject: string) => { const { action, services } = ability if (this.hasConfigAbility(services)) { abilities.push({ action, subject }) } }) return abilities } /** * Parcourt les services définis dans la configuration, et établit si oui ou non l'habilité est autorisée * * @return {boolean} * @param services */ hasConfigAbility(services: AnyJson) { const handlerMap: any = { hasAbility: (parameters: any) => this.hasAllAbilities(parameters), hasProfile: (parameters: any) => parameters === null || this.hasAnyProfile(parameters), hasModule: (parameters: any) => this.hasModule(parameters), isAdminAccount: (parameters: any) => this.accessProfile.isAdminAccount, isSchool: (parameters: any) => this.organizationProfile.isSchool, isArtist: (parameters: any) => this.organizationProfile.isArtist, isManagerProduct: (parameters: any) => this.organizationProfile.isManagerProduct, isOrganizationWithChildren: (parameters: any) => this.organizationProfile.hasChildren, isAssociation: (parameters: any) => this.organizationProfile.isAssociation, isShowAdherentList: (parameters: any) => this.organizationProfile.isShowAdherentList, isCmf: (parameters: any) => this.organizationProfile.isCmf, getWebsite: (parameters: any) => this.organizationProfile.getWebsite, } // TODO: renommer les fonctions du handlerMap de la même façon que les fonctions hasAnyXxx, hasAllXxx, etc. // TODO: revoir les fichiers yaml en fonction // TODO: clarifier le fonctionnement du hasAbility (c'est très bizarre de tester des habilités alors qu'on est en train de les construire) // TODO: extraire l'intérieur du useEach dans une méthode séparée pour clarifier l'algo // TODO: voir pourquoi on a besoin d'accepter un param null pour le hasProfile? // TODO: créer un outil pour simplifier le déboguage des droits let hasAbility = true useEach(services, (handlers: Array<{ function: string, parameters?: Array, result?: any }>, service: string) => { useEach(handlers, (handler: { function: string, parameters?: Array, result?: any }) => { const expectedResult: boolean = handler.result ?? true; const parameters = handler.parameters ?? [] const actualResult = handlerMap[handler.function](parameters ?? null) if (actualResult !== expectedResult) { hasAbility = false return false } }) if (!hasAbility) { return false } }) return hasAbility } /** * Est-ce que l'utilisateur possède toutes les habilités passées en paramètre * * @param {Array} abilities Habilités à tester * @return {boolean} */ hasAllAbilities(abilities: Array): boolean { return abilities.every(ability => this.ability.can(ability.action, ability.subject)) } /** * Teste si l'utilisateur possède le profil donné * * @param {string} profile Profil à tester * @return {boolean} */ hasProfile(profile: string): boolean { const factory: {[key: string]: boolean|null} = { 'admin': this.accessProfile.isAdmin, 'administratifManager': this.accessProfile.isAdministratifManager, 'pedagogicManager': this.accessProfile.isPedagogicManager, 'financialManager': this.accessProfile.isFinancialManager, 'caMember': this.accessProfile.isCaMember, 'student': this.accessProfile.isStudent, 'teacher': this.accessProfile.isTeacher, 'member': this.accessProfile.isMember, 'other': this.accessProfile.isOther, 'guardian': this.accessProfile.isGuardian, 'payor': this.accessProfile.isPayer, } return factory[profile] ?? false } /** * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre * * @param {Array} profiles Profils à tester * @return {boolean} */ hasAnyProfile (profiles: Array): boolean { return profiles.some(p => this.hasProfile(p)) } /** * Retourne vrai si l'utilisateur connecté possède tous les profils passés en paramètre * * @param {Array} profiles Profils à tester * @return {boolean} */ hasAllProfiles (profiles: Array): boolean { return profiles.every(p => this.hasProfile(p)) } /** * Est-ce que l'utilisateur possède le rôle donné ? * * @return {boolean} * @param role */ hasRole(role: string): boolean { return this.accessProfile.roles.includes(role) } /** * L'utilisateur possède-t-il au moins l'un des rôles donnés * * @return {boolean} * @param roles */ hasAnyRole(roles: Array): boolean { return roles.some(r => this.hasRole(r)) } /** * L'utilisateur possède-t-il tous les rôles donnés * * @return {boolean} * @param roles */ hasAllRoles(roles: Array): boolean { return roles.every(r => this.hasRole(r)) } /** * Est-ce que l'organisation possède le module donné * * @return {boolean} * @param module */ hasModule(module: string): boolean { return this.organizationProfile.modules.includes(module) } /** * Est-ce que l'organisation possède au moins un des modules donnés * * @param modules * @return {boolean} */ hasAnyModule(modules: Array): boolean { return modules.some(r => this.hasModule(r)) } /** * Est-ce que l'organisation possède-t-il tous les modules donnés * * @param modules * @return {boolean} */ hasAllModules(modules: Array): boolean { return modules.every(r => this.hasModule(r)) } } export default AbilityUtils