abilityBuilder.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. // eslint-disable-next-line import/default
  2. import * as yaml from 'yaml-import'
  3. import * as _ from 'lodash-es'
  4. import RoleUtils from '~/services/rights/roleUtils'
  5. import type { AbilitiesType, AccessProfile } from '~/types/interfaces'
  6. import { ABILITIES } from '~/types/enum/enums'
  7. import type OrganizationProfile from '~/models/Organization/OrganizationProfile'
  8. import type { MongoAbility } from '@casl/ability/dist/types/Ability'
  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. const hasAbility = this.hasConfigAbility(
  80. conditions as Array<Condition>,
  81. subject,
  82. )
  83. abilitiesByConfig.push({ action, subject, inverted: !hasAbility })
  84. },
  85. )
  86. return abilitiesByConfig
  87. }
  88. /**
  89. * Parcourt les services définis dans la configuration, et établit si oui ou non l'habilité est autorisée
  90. *
  91. * @return {boolean}
  92. * @param conditions Les conditions à l'obtention de l'habileté, telles que définies dans les fichiers de config
  93. * @param subject For debugging purpose only
  94. */
  95. hasConfigAbility(conditions: Array<Condition>, subject: string = '') {
  96. return conditions.every((condition) =>
  97. this.execAndValidateCondition(condition, subject),
  98. )
  99. }
  100. // noinspection JSUnusedGlobalSymbols
  101. /**
  102. * Correspondances entre les noms des fonctions définies dans les conditions des fichiers de configuration et
  103. * les méthodes correspondantes
  104. *
  105. * TODO: voir pourquoi on a besoin d'accepter un param null pour le hasProfile?
  106. */
  107. handlerMap: Record<
  108. string,
  109. (parameters: Array<ConditionParameters | string>) => boolean
  110. > = {
  111. accessHasAllRoleAbilities: (
  112. parameters: Array<ConditionParameters | string>,
  113. ) => this.hasAllRoleAbilities(parameters),
  114. accessHasAnyRoleAbility: (
  115. parameters: Array<ConditionParameters | string>,
  116. ) => this.hasAnyRoleAbility(parameters),
  117. accessHasAnyProfile: (parameters: Array<ConditionParameters | string>) =>
  118. parameters === null || this.hasAnyProfile(parameters),
  119. organizationHasAllModules: (
  120. parameters: Array<ConditionParameters | string>,
  121. ) => this.hasAllModules(parameters),
  122. organizationHasAnyModule: (
  123. parameters: Array<ConditionParameters | string>,
  124. ) => this.hasAnyModule(parameters),
  125. accessIsAdminAccount: (_) => this.accessProfile.isAdminAccount,
  126. organizationIsSchool: (_) => this.organizationProfile.isSchool,
  127. organizationIsArtist: (_) => this.organizationProfile.isArtist,
  128. organizationIsManagerProduct: (_) =>
  129. this.organizationProfile.isManagerProduct,
  130. organizationHasChildren: (_) => this.organizationProfile.hasChildren,
  131. organizationIsAssociation: (_) => this.organizationProfile.isAssociation,
  132. organizationIsShowAdherentList: (_) =>
  133. this.organizationProfile.isShowAdherentList,
  134. organizationIsCmf: (_) => this.organizationProfile.isCmf,
  135. organizationHasWebsite: (_) => this.organizationProfile.getWebsite,
  136. }
  137. /**
  138. * Exécute la fonction associée à la condition, et compare le résultat obtenu au résultat attendu (true par défaut)
  139. *
  140. * @param condition Un condition à la possession d'une habilité, telle que définie dans les fichiers de config
  141. * @param subject For debugging purpose only
  142. * @private
  143. */
  144. protected execAndValidateCondition(
  145. condition: Condition,
  146. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  147. subject: string = '',
  148. ) {
  149. const expectedResult: boolean = condition.expectedResult ?? true
  150. const parameters = condition.parameters ?? []
  151. if (!(condition.function in this.handlerMap)) {
  152. throw new Error('unknown condition function : ' + condition.function)
  153. }
  154. const actualResult = this.handlerMap[condition.function](parameters ?? null)
  155. return actualResult === expectedResult
  156. }
  157. /**
  158. * Est-ce que l'utilisateur possède l'habilité en paramètre
  159. *
  160. * @return {boolean}
  161. * @param ability
  162. */
  163. hasRoleAbility(ability: ConditionParameters | string): boolean {
  164. if (!Object.prototype.hasOwnProperty.call(ability, 'action')) {
  165. throw new Error(
  166. 'hasRoleAbility except a ConditionParameters object, not a string',
  167. )
  168. }
  169. ability = ability as ConditionParameters
  170. return this.ability.can(ability.action, ability.subject)
  171. }
  172. /**
  173. * Est-ce que l'utilisateur possède toutes les habilités passées en paramètre
  174. *
  175. * @param {Array<AbilitiesType>} abilities Habilités à tester
  176. * @return {boolean}
  177. */
  178. hasAllRoleAbilities(abilities: Array<ConditionParameters | string>): boolean {
  179. return abilities.every((ability) => this.hasRoleAbility(ability))
  180. }
  181. /**
  182. * Est-ce que l'utilisateur possède au moins l'une des habilités passées en paramètre
  183. *
  184. * @param {Array<AbilitiesType>} abilities Habilités à tester
  185. * @return {boolean}
  186. */
  187. hasAnyRoleAbility(abilities: Array<ConditionParameters | string>): boolean {
  188. return abilities.some((ability) => this.hasRoleAbility(ability))
  189. }
  190. /**
  191. * Teste si l'utilisateur possède le profil donné
  192. *
  193. * @param {string} profile Profil à tester
  194. * @return {boolean}
  195. */
  196. hasProfile(profile: ConditionParameters | string): boolean {
  197. if (Object.prototype.hasOwnProperty.call(profile, 'subject')) {
  198. profile = (profile as ConditionParameters).subject
  199. }
  200. return (
  201. {
  202. admin: this.accessProfile.isAdmin,
  203. administratifManager: this.accessProfile.isAdministratifManager,
  204. pedagogicManager: this.accessProfile.isPedagogicManager,
  205. financialManager: this.accessProfile.isFinancialManager,
  206. caMember: this.accessProfile.isCaMember,
  207. student: this.accessProfile.isStudent,
  208. teacher: this.accessProfile.isTeacher,
  209. member: this.accessProfile.isMember,
  210. other: this.accessProfile.isOther,
  211. guardian: this.accessProfile.isGuardian,
  212. payor: this.accessProfile.isPayer,
  213. }[profile as string] ?? false
  214. )
  215. }
  216. /**
  217. * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre
  218. *
  219. * @param {Array<string>} profiles Profils à tester
  220. * @return {boolean}
  221. */
  222. hasAnyProfile(profiles: Array<ConditionParameters | string>): boolean {
  223. return profiles.some((p) => this.hasProfile(p))
  224. }
  225. /**
  226. * Retourne vrai si l'utilisateur connecté possède tous les profils passés en paramètre
  227. *
  228. * @param {Array<string>} profiles Profils à tester
  229. * @return {boolean}
  230. */
  231. hasAllProfiles(profiles: Array<ConditionParameters | string>): boolean {
  232. return profiles.every((p) => this.hasProfile(p))
  233. }
  234. /**
  235. * Est-ce que l'utilisateur possède le rôle donné ?
  236. *
  237. * @return {boolean}
  238. * @param role
  239. */
  240. hasRole(role: string): boolean {
  241. return this.accessProfile.hasRole(role)
  242. }
  243. /**
  244. * L'utilisateur possède-t-il au moins l'un des rôles donnés
  245. *
  246. * @return {boolean}
  247. * @param roles
  248. */
  249. hasAnyRole(roles: Array<string>): boolean {
  250. return roles.some((r) => this.hasRole(r))
  251. }
  252. /**
  253. * L'utilisateur possède-t-il tous les rôles donnés
  254. *
  255. * @return {boolean}
  256. * @param roles
  257. */
  258. hasAllRoles(roles: Array<string>): boolean {
  259. return roles.every((r) => this.hasRole(r))
  260. }
  261. /**
  262. * Est-ce que l'organisation possède le module donné
  263. *
  264. * @return {boolean}
  265. * @param module
  266. */
  267. hasModule(module: ConditionParameters | string): boolean {
  268. if (Object.prototype.hasOwnProperty.call(module, 'subject')) {
  269. module = (module as ConditionParameters).subject
  270. }
  271. return this.organizationProfile.hasModule(module)
  272. }
  273. /**
  274. * Est-ce que l'organisation possède au moins un des modules donnés
  275. *
  276. * @param modules
  277. * @return {boolean}
  278. */
  279. hasAnyModule(modules: Array<ConditionParameters | string>): boolean {
  280. return modules.some((r) => this.hasModule(r))
  281. }
  282. /**
  283. * Est-ce que l'organisation possède-t-il tous les modules donnés
  284. *
  285. * @param modules
  286. * @return {boolean}
  287. */
  288. hasAllModules(modules: Array<ConditionParameters | string>): boolean {
  289. return modules.every((r) => this.hasModule(r))
  290. }
  291. }
  292. export default AbilityBuilder