abilityBuilder.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import type { MongoAbility } from '@casl/ability/dist/types/Ability'
  2. // eslint-disable-next-line import/default
  3. import yaml from 'yaml-import'
  4. import * as _ from 'lodash-es'
  5. import RoleUtils from '~/services/rights/roleUtils'
  6. import type { AbilitiesType, AccessProfile } from '~/types/interfaces'
  7. import { ABILITIES } from '~/types/enum/enums'
  8. import type OrganizationProfile from '~/models/Organization/OrganizationProfile'
  9. interface ConditionParameters {
  10. action: string
  11. subject: string
  12. }
  13. interface Condition {
  14. function: string
  15. parameters?: Array<string | ConditionParameters>
  16. expectedResult?: boolean
  17. }
  18. /**
  19. * Classe permettant de mener des opérations sur les habilités
  20. */
  21. class AbilityBuilder {
  22. private readonly ability: MongoAbility = {} as MongoAbility
  23. private readonly accessProfile: AccessProfile
  24. private readonly organizationProfile: OrganizationProfile
  25. private readonly configFile = './config/abilities/config.yaml'
  26. private abilities: Array<AbilitiesType> = []
  27. /**
  28. * @constructor
  29. */
  30. constructor(
  31. ability: MongoAbility,
  32. accessProfile: AccessProfile,
  33. organizationProfile: OrganizationProfile,
  34. ) {
  35. this.ability = ability
  36. this.accessProfile = accessProfile
  37. this.organizationProfile = organizationProfile
  38. }
  39. /**
  40. * Construit et renvoie l'ensemble des habilités de l'utilisateur, qu'elles soient issues de ses roles
  41. * ou de la configuration
  42. *
  43. * @return {Array<AbilitiesType>}
  44. */
  45. buildAbilities(): Array<AbilitiesType> {
  46. // Build from roles
  47. this.abilities = this.buildAbilitiesFromRoles()
  48. this.ability.update(this.abilities)
  49. // Build from config
  50. this.abilities = this.abilities.concat(this.buildAbilitiesFromConfig())
  51. this.ability.update(this.abilities)
  52. return this.abilities
  53. }
  54. /**
  55. * Adaptation et transformations des roles symfony en abilities Casl
  56. */
  57. buildAbilitiesFromRoles() {
  58. return RoleUtils.rolesToAbilities(this.accessProfile.roles)
  59. }
  60. /**
  61. * Charge les habilités depuis les fichiers de configuration
  62. */
  63. buildAbilitiesFromConfig() {
  64. const abilitiesByConfig: Array<AbilitiesType> = []
  65. const doc = yaml.read(this.configFile)
  66. const fromConfig = doc.abilities
  67. _.each(
  68. fromConfig,
  69. (
  70. ability: { action: ABILITIES; conditions: Array<Condition> },
  71. subject: string,
  72. ) => {
  73. // eslint-disable-next-line prefer-const
  74. let { action, conditions } = ability
  75. if (!Array.isArray(conditions)) {
  76. // Special: la denormalization ne produit pas une array s'il n'y a qu'un seul élément
  77. conditions = [conditions]
  78. }
  79. if (this.hasConfigAbility(conditions as Array<Condition>, subject)) {
  80. abilitiesByConfig.push({ action, subject })
  81. }
  82. },
  83. )
  84. return abilitiesByConfig
  85. }
  86. /**
  87. * Parcourt les services définis dans la configuration, et établit si oui ou non l'habilité est autorisée
  88. *
  89. * @return {boolean}
  90. * @param conditions Les conditions à l'obtention de l'habileté, telles que définies dans les fichiers de config
  91. * @param subject For debugging purpose only
  92. */
  93. hasConfigAbility(conditions: Array<Condition>, subject: string = '') {
  94. return conditions.every((condition) =>
  95. this.execAndValidateCondition(condition, subject),
  96. )
  97. }
  98. // noinspection JSUnusedGlobalSymbols
  99. /**
  100. * Correspondances entre les noms des fonctions définies dans les conditions des fichiers de configuration et
  101. * les méthodes correspondantes
  102. *
  103. * TODO: voir pourquoi on a besoin d'accepter un param null pour le hasProfile?
  104. */
  105. handlerMap: Record<
  106. string,
  107. (parameters: Array<ConditionParameters | string>) => boolean
  108. > = {
  109. accessHasAllRoleAbilities: (
  110. parameters: Array<ConditionParameters | string>,
  111. ) => this.hasAllRoleAbilities(parameters),
  112. accessHasAnyRoleAbility: (
  113. parameters: Array<ConditionParameters | string>,
  114. ) => this.hasAnyRoleAbility(parameters),
  115. accessHasAnyProfile: (parameters: Array<ConditionParameters | string>) =>
  116. parameters === null || this.hasAnyProfile(parameters),
  117. organizationHasAllModules: (
  118. parameters: Array<ConditionParameters | string>,
  119. ) => this.hasAllModules(parameters),
  120. organizationHasAnyModule: (
  121. parameters: Array<ConditionParameters | string>,
  122. ) => this.hasAnyModule(parameters),
  123. accessIsAdminAccount: (_) => this.accessProfile.isAdminAccount,
  124. organizationIsSchool: (_) => this.organizationProfile.isSchool,
  125. organizationIsArtist: (_) => this.organizationProfile.isArtist,
  126. organizationIsManagerProduct: (_) =>
  127. this.organizationProfile.isManagerProduct,
  128. organizationHasChildren: (_) => this.organizationProfile.hasChildren,
  129. organizationIsAssociation: (_) => this.organizationProfile.isAssociation,
  130. organizationIsShowAdherentList: (_) =>
  131. this.organizationProfile.isShowAdherentList,
  132. organizationIsCmf: (_) => this.organizationProfile.isCmf,
  133. organizationHasWebsite: (_) => this.organizationProfile.getWebsite,
  134. }
  135. /**
  136. * Exécute la fonction associée à la condition, et compare le résultat obtenu au résultat attendu (true par défaut)
  137. *
  138. * @param condition Un condition à la possession d'une habilité, telle que définie dans les fichiers de config
  139. * @param subject For debugging purpose only
  140. * @private
  141. */
  142. protected execAndValidateCondition(
  143. condition: Condition,
  144. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  145. subject: string = '',
  146. ) {
  147. const expectedResult: boolean = condition.expectedResult ?? true
  148. const parameters = condition.parameters ?? []
  149. if (!(condition.function in this.handlerMap)) {
  150. throw new Error('unknown condition function : ' + condition.function)
  151. }
  152. const actualResult = this.handlerMap[condition.function](parameters ?? null)
  153. return actualResult === expectedResult
  154. }
  155. /**
  156. * Est-ce que l'utilisateur possède l'habilité en paramètre
  157. *
  158. * @return {boolean}
  159. * @param ability
  160. */
  161. hasRoleAbility(ability: ConditionParameters | string): boolean {
  162. if (!Object.prototype.hasOwnProperty.call(ability, 'action')) {
  163. throw new Error(
  164. 'hasRoleAbility except a ConditionParameters object, not a string',
  165. )
  166. }
  167. ability = ability as ConditionParameters
  168. return this.ability.can(ability.action, ability.subject)
  169. }
  170. /**
  171. * Est-ce que l'utilisateur possède toutes les habilités passées en paramètre
  172. *
  173. * @param {Array<AbilitiesType>} abilities Habilités à tester
  174. * @return {boolean}
  175. */
  176. hasAllRoleAbilities(abilities: Array<ConditionParameters | string>): boolean {
  177. return abilities.every((ability) => this.hasRoleAbility(ability))
  178. }
  179. /**
  180. * Est-ce que l'utilisateur possède au moins l'une des habilités passées en paramètre
  181. *
  182. * @param {Array<AbilitiesType>} abilities Habilités à tester
  183. * @return {boolean}
  184. */
  185. hasAnyRoleAbility(abilities: Array<ConditionParameters | string>): boolean {
  186. return abilities.some((ability) => this.hasRoleAbility(ability))
  187. }
  188. /**
  189. * Teste si l'utilisateur possède le profil donné
  190. *
  191. * @param {string} profile Profil à tester
  192. * @return {boolean}
  193. */
  194. hasProfile(profile: ConditionParameters | string): boolean {
  195. if (Object.prototype.hasOwnProperty.call(profile, 'subject')) {
  196. profile = (profile as ConditionParameters).subject
  197. }
  198. return (
  199. {
  200. admin: this.accessProfile.isAdmin,
  201. administratifManager: this.accessProfile.isAdministratifManager,
  202. pedagogicManager: this.accessProfile.isPedagogicManager,
  203. financialManager: this.accessProfile.isFinancialManager,
  204. caMember: this.accessProfile.isCaMember,
  205. student: this.accessProfile.isStudent,
  206. teacher: this.accessProfile.isTeacher,
  207. member: this.accessProfile.isMember,
  208. other: this.accessProfile.isOther,
  209. guardian: this.accessProfile.isGuardian,
  210. payor: this.accessProfile.isPayer,
  211. }[profile as string] ?? false
  212. )
  213. }
  214. /**
  215. * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre
  216. *
  217. * @param {Array<string>} profiles Profils à tester
  218. * @return {boolean}
  219. */
  220. hasAnyProfile(profiles: Array<ConditionParameters | string>): boolean {
  221. return profiles.some((p) => this.hasProfile(p))
  222. }
  223. /**
  224. * Retourne vrai si l'utilisateur connecté possède tous les profils passés en paramètre
  225. *
  226. * @param {Array<string>} profiles Profils à tester
  227. * @return {boolean}
  228. */
  229. hasAllProfiles(profiles: Array<ConditionParameters | string>): boolean {
  230. return profiles.every((p) => this.hasProfile(p))
  231. }
  232. /**
  233. * Est-ce que l'utilisateur possède le rôle donné ?
  234. *
  235. * @return {boolean}
  236. * @param role
  237. */
  238. hasRole(role: string): boolean {
  239. return this.accessProfile.hasRole(role)
  240. }
  241. /**
  242. * L'utilisateur possède-t-il au moins l'un des rôles donnés
  243. *
  244. * @return {boolean}
  245. * @param roles
  246. */
  247. hasAnyRole(roles: Array<string>): boolean {
  248. return roles.some((r) => this.hasRole(r))
  249. }
  250. /**
  251. * L'utilisateur possède-t-il tous les rôles donnés
  252. *
  253. * @return {boolean}
  254. * @param roles
  255. */
  256. hasAllRoles(roles: Array<string>): boolean {
  257. return roles.every((r) => this.hasRole(r))
  258. }
  259. /**
  260. * Est-ce que l'organisation possède le module donné
  261. *
  262. * @return {boolean}
  263. * @param module
  264. */
  265. hasModule(module: ConditionParameters | string): boolean {
  266. if (Object.prototype.hasOwnProperty.call(module, 'subject')) {
  267. module = (module as ConditionParameters).subject
  268. }
  269. return this.organizationProfile.hasModule(module)
  270. }
  271. /**
  272. * Est-ce que l'organisation possède au moins un des modules donnés
  273. *
  274. * @param modules
  275. * @return {boolean}
  276. */
  277. hasAnyModule(modules: Array<ConditionParameters | string>): boolean {
  278. return modules.some((r) => this.hasModule(r))
  279. }
  280. /**
  281. * Est-ce que l'organisation possède-t-il tous les modules donnés
  282. *
  283. * @param modules
  284. * @return {boolean}
  285. */
  286. hasAllModules(modules: Array<ConditionParameters | string>): boolean {
  287. return modules.every((r) => this.hasModule(r))
  288. }
  289. }
  290. export default AbilityBuilder