abilityBuilder.ts 10 KB

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