has_groups.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module HasGroups
  3. extend ActiveSupport::Concern
  4. included do
  5. before_destroy :destroy_group_relations
  6. attr_accessor :group_access_buffer
  7. after_save :process_group_access_buffer
  8. # add association to Group, too but ignore it in asset output
  9. Group.has_many group_through_identifier
  10. Group.has_many model_name.collection.to_sym, through: group_through_identifier, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
  11. Group.association_attributes_ignored group_through_identifier
  12. association_attributes_ignored :groups, group_through_identifier
  13. has_many group_through_identifier
  14. has_many :groups, through: group_through_identifier do
  15. # A helper to join the :through table into the result of groups to access :through attributes
  16. #
  17. # @param [String, Array<String>] access Limiting to one or more access verbs. 'full' gets added automatically
  18. #
  19. # @example All access groups
  20. # user.groups.access
  21. # #=> [#<Group id: 1, access="read", ...>, ...]
  22. #
  23. # @example Groups for given access(es) plus 'full'
  24. # user.groups.access('read')
  25. # #=> [#<Group id: 1, access="full", ...>, ...]
  26. #
  27. # @example Groups for given access(es)es plus 'full'
  28. # user.groups.access('read', 'change')
  29. # #=> [#<Group id: 1, access="full", ...>, ...]
  30. #
  31. # @return [ActiveRecord::AssociationRelation<[<Group]>] List of Groups with :through attributes
  32. def access(*access)
  33. table_name = proxy_association.owner.class.group_through.table_name
  34. query = select("#{ActiveRecord::Base.connection.quote_table_name('groups')}.*, #{ActiveRecord::Base.connection.quote_table_name(table_name)}.*")
  35. return query if access.blank?
  36. access.push('full') if access.exclude?('full')
  37. query.where("#{table_name}.access" => access)
  38. end
  39. end
  40. end
  41. # Checks a given Group( ID) for given access(es) for the instance.
  42. # Checks indirect access via Roles if instance has Roles, too.
  43. #
  44. # @example Group ID param
  45. # user.group_access?(1, 'read')
  46. # #=> true
  47. #
  48. # @example Group param
  49. # user.group_access?(group, 'read')
  50. # #=> true
  51. #
  52. # @example Access list
  53. # user.group_access?(group, ['read', 'create'])
  54. # #=> true
  55. #
  56. # @return [Boolean]
  57. def group_access?(group_id, access)
  58. Auth::RequestCache.fetch_value("group_access/#{cache_key_with_version}/#{group_id}/#{access}") do
  59. group_access_uncached?(group_id, access)
  60. end
  61. end
  62. def group_access_uncached?(group_id, access)
  63. return false if !active?
  64. return false if !groups_access_permission?
  65. group_id = self.class.ensure_group_id_parameter(group_id)
  66. access = Array(access).map(&:to_sym) | [:full]
  67. # check direct access
  68. return true if group_through.klass.eager_load(:group).exists?(
  69. group_through.foreign_key => id,
  70. group_id: group_id,
  71. access: access,
  72. groups: {
  73. active: true
  74. }
  75. )
  76. # check indirect access through Roles if possible
  77. return false if !respond_to?(:role_access?)
  78. role_access?(group_id, access)
  79. end
  80. # Lists the Group IDs the instance has the given access(es) plus 'full' to.
  81. # Adds indirect accessable Group IDs via Roles if instance has Roles, too.
  82. #
  83. # @example Single access
  84. # user.group_ids_access('read')
  85. # #=> [1, 3, ...]
  86. #
  87. # @example Access list
  88. # user.group_ids_access(['read', 'create'])
  89. # #=> [1, 3, ...]
  90. #
  91. # @return [Array<Integer>] Group IDs the instance has the given access(es) to.
  92. def group_ids_access(access)
  93. return [] if !active?
  94. return [] if !groups_access_permission?
  95. access = Array(access).map(&:to_sym) | [:full]
  96. foreign_key = group_through.foreign_key
  97. klass = group_through.klass
  98. # check direct access
  99. ids = klass.eager_load(:group).where(foreign_key => id, access: access, groups: { active: true }).pluck(:group_id)
  100. ids ||= []
  101. # check indirect access through roles if possible
  102. return ids if !respond_to?(:role_ids)
  103. role_group_ids = RoleGroup.eager_load(:group).where(role_id: role_ids, access: access, groups: { active: true }).pluck(:group_id)
  104. # combines and removes duplicates
  105. # and returns them in one statement
  106. ids | role_group_ids
  107. end
  108. # Lists Groups the instance has the given access(es) plus 'full' to.
  109. # Adds indirect accessable Groups via Roles if instance has Roles, too.
  110. #
  111. # @example Single access
  112. # user.groups_access('read')
  113. # #=> [#<Group id: 1, access="read", ...>, ...]
  114. #
  115. # @example Access list
  116. # user.groups_access(['read', 'create'])
  117. # #=> [#<Group id: 1, access="read", ...>, ...]
  118. #
  119. # @return [Array<Group>] Groups the instance has the given access(es) to.
  120. def groups_access(access)
  121. group_ids = group_ids_access(access)
  122. Group.where(id: group_ids)
  123. end
  124. # Returns a map of Group name to access
  125. #
  126. # @example
  127. # user.group_names_access_map
  128. # #=> {'Users' => 'full', 'Support' => ['read', 'change']}
  129. #
  130. # @return [Hash<String=>String,Array<String>>] The map of Group name to access
  131. def group_names_access_map
  132. groups_access_map(:name)
  133. end
  134. # Stores a map of Group ID to access. Deletes all other relations.
  135. #
  136. # @example
  137. # user.group_names_access_map = {'Users' => 'full', 'Support' => ['read', 'change']}
  138. # #=> {'Users' => 'full', 'Support' => ['read', 'change']}
  139. #
  140. # @return [Hash<String=>String,Array<String>>] The given map
  141. def group_names_access_map=(name_access_map)
  142. groups_access_map_store(name_access_map) do |group_name|
  143. Group.where(name: group_name).pick(:id)
  144. end
  145. end
  146. # Returns a map of Group ID to access
  147. #
  148. # @example
  149. # user.group_ids_access_map
  150. # #=> {1 => 'full', 42 => ['read', 'change']}
  151. #
  152. # @return [Hash<Integer=>String,Array<String>>] The map of Group ID to access
  153. def group_ids_access_map
  154. groups_access_map(:id)
  155. end
  156. # Stores a map of Group ID to access. Deletes all other relations.
  157. #
  158. # @example
  159. # user.group_ids_access_map = {1 => 'full', 42 => ['read', 'change']}
  160. # #=> {1 => 'full', 42 => ['read', 'change']}
  161. #
  162. # @return [Hash<Integer=>String,Array<String>>] The given map
  163. def group_ids_access_map=(id_access_map)
  164. groups_access_map_store(id_access_map)
  165. end
  166. # An alias to .groups class method
  167. def group_through
  168. @group_through ||= self.class.group_through
  169. end
  170. # Checks if the instance has general permission to Group access.
  171. #
  172. # @example
  173. # customer_user.groups_access_permission?
  174. # #=> false
  175. #
  176. # @return [Boolean]
  177. def groups_access_permission?
  178. return true if !respond_to?(:permissions?)
  179. permissions?('ticket.agent')
  180. end
  181. private
  182. def groups_access_map(key)
  183. return {} if !active?
  184. return {} if !groups_access_permission?
  185. groups.access.where(active: true).pluck(key, :access).each_with_object({}) do |entry, hash|
  186. hash[ entry[0] ] ||= []
  187. hash[ entry[0] ].push(entry[1])
  188. end
  189. end
  190. def groups_access_map_store(map)
  191. fill_group_access_buffer do
  192. Hash(map).each do |group_identifier, accesses|
  193. # use given key as identifier or look it up
  194. # via the given block which returns the identifier
  195. group_id = block_given? ? yield(group_identifier) : group_identifier
  196. Array(accesses).each do |access|
  197. push_group_access_buffer(
  198. group_id: group_id,
  199. access: access
  200. )
  201. end
  202. end
  203. end
  204. end
  205. def fill_group_access_buffer
  206. @group_access_buffer = []
  207. yield
  208. process_group_access_buffer if id
  209. end
  210. def push_group_access_buffer(entry)
  211. @group_access_buffer.push(entry)
  212. end
  213. def flush_group_access_buffer
  214. # group_access_buffer is at least an empty Array
  215. # if changes to the map were performed
  216. # otherwise it's just an update of other attributes
  217. return if group_access_buffer.nil?
  218. yield
  219. self.group_access_buffer = nil
  220. end
  221. def process_group_access_buffer
  222. flush_group_access_buffer do
  223. foreign_key = group_through.foreign_key
  224. entries = Array.wrap(group_access_buffer).collect do |entry|
  225. entry[:group_id] = entry[:group_id].to_i
  226. entry[foreign_key] = id
  227. entry.symbolize_keys
  228. end
  229. group_through.klass.where(foreign_key => id).in_batches.each_record do |object|
  230. entry = object.attributes.symbolize_keys
  231. if entries.include?(entry)
  232. entries -= [entry]
  233. next
  234. end
  235. object.destroy!
  236. end
  237. group_through.klass.create!(entries)
  238. end
  239. true
  240. end
  241. def destroy_group_relations
  242. group_through.klass.where(group_through.foreign_key => id).destroy_all
  243. end
  244. # methods defined here are going to extend the class, not the instance of it
  245. class_methods do
  246. # Lists IDs of instances having the given access(es) to the given Group.
  247. #
  248. # @example Group ID param
  249. # User.group_access_ids(1, 'read')
  250. # #=> [1, 3, ...]
  251. #
  252. # @example Group param
  253. # User.group_access_ids(group, 'read')
  254. # #=> [1, 3, ...]
  255. #
  256. # @example Access list
  257. # User.group_access_ids(group, ['read', 'create'])
  258. # #=> [1, 3, ...]
  259. #
  260. # @return [Array<Integer>]
  261. def group_access_ids(group_id, access)
  262. group_access(group_id, access).collect(&:id)
  263. end
  264. # Lists instances having the given access(es) to the given Group.
  265. #
  266. # @example Group ID param
  267. # User.group_access(1, 'read')
  268. # #=> [#<User id: 1, ...>, ...]
  269. #
  270. # @example Group param
  271. # User.group_access(group, 'read')
  272. # #=> [#<User id: 1, ...>, ...]
  273. #
  274. # @example Access list
  275. # User.group_access(group, ['read', 'create'])
  276. # #=> [#<User id: 1, ...>, ...]
  277. #
  278. # @return [Array<Class>]
  279. def group_access(group_id, access)
  280. group_id = ensure_group_id_parameter(group_id)
  281. access = Array(access).map(&:to_sym) | [:full]
  282. # check direct access
  283. instances = Permission.join_with(self, 'ticket.agent').joins(group_through.name)
  284. .where(group_through.table_name => { group_id: group_id, access: access }, active: true)
  285. # check indirect access through roles if possible
  286. return instances if !respond_to?(:role_access)
  287. # combines and removes duplicates
  288. # and returns them in one statement
  289. instances | role_access(group_id, access)
  290. end
  291. # The reflection instance containing the association data
  292. #
  293. # @example
  294. # User.group_through
  295. # #=> <ActiveRecord::Reflection::HasManyReflection:0x007fd2f5785440 @name=:user_groups, ...>
  296. #
  297. # @return [ActiveRecord::Reflection::HasManyReflection] The given map
  298. def group_through
  299. @group_through ||= reflect_on_association(group_through_identifier)
  300. end
  301. # The identifier of the has_many :through relation
  302. #
  303. # @example
  304. # User.group_through_identifier
  305. # #=> :user_groups
  306. #
  307. # @return [Symbol] The relation identifier
  308. def group_through_identifier
  309. :"#{name.downcase}_groups"
  310. end
  311. def ensure_group_id_parameter(group_or_id)
  312. return group_or_id if group_or_id.is_a?(Integer)
  313. group_or_id.id
  314. end
  315. end
  316. end