# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ module HasGroups extend ActiveSupport::Concern included do before_destroy :destroy_group_relations attr_accessor :group_access_buffer after_create :check_group_access_buffer after_update :check_group_access_buffer association_attributes_ignored :groups has_many group_through_identifier has_many :groups, through: group_through_identifier do # A helper to join the :through table into the result of groups to access :through attributes # # @param [String, Array] access Limiting to one or more access verbs. 'full' gets added automatically # # @example All access groups # user.groups.access # #=> [#, ...] # # @example Groups for given access(es) plus 'full' # user.groups.access('read') # #=> [#, ...] # # @example Groups for given access(es)es plus 'full' # user.groups.access('read', 'write') # #=> [#, ...] # # @return [ActiveRecord::AssociationRelation<[] List of Groups with :through attributes def access(*access) table_name = proxy_association.owner.class.group_through.table_name query = select("groups.*, #{table_name}.*") return query if access.blank? access.push('full') if !access.include?('full') query.where("#{table_name}.access" => access) end end end # Checks a given Group( ID) for given access(es) for the instance. # Checks indirect access via Roles if instance has Roles, too. # # @example Group ID param # user.group_access?(1, 'read') # #=> true # # @example Group param # user.group_access?(group, 'read') # #=> true # # @example Access list # user.group_access?(group, ['read', 'create']) # #=> true # # @return [Boolean] def group_access?(group_id, access) return false if !active? group_id = self.class.ensure_group_id_parameter(group_id) access = self.class.ensure_group_access_list_parameter(access) # check direct access return true if group_through.klass.includes(:group).exists?( group_through.foreign_key => id, group_id: group_id, access: access, groups: { active: true } ) # check indirect access through Roles if possible return false if !respond_to?(:role_access?) role_access?(group_id, access) end # Lists the Group IDs the instance has the given access(es) plus 'full' to. # Adds indirect accessable Group IDs via Roles if instance has Roles, too. # # @example Single access # user.group_ids_access('read') # #=> [1, 3, ...] # # @example Access list # user.group_ids_access(['read', 'create']) # #=> [1, 3, ...] # # @return [Array] Group IDs the instance has the given access(es) to. def group_ids_access(access) return [] if !active? access = self.class.ensure_group_access_list_parameter(access) foreign_key = group_through.foreign_key klass = group_through.klass # check direct access ids = klass.includes(:group).where(foreign_key => id, access: access, groups: { active: true }).pluck(:group_id) ids ||= [] # check indirect access through roles if possible return ids if !respond_to?(:role_ids) role_group_ids = RoleGroup.includes(:group).where(role_id: role_ids, access: access, groups: { active: true }).pluck(:group_id) # combines and removes duplicates # and returns them in one statement ids | role_group_ids end # Lists Groups the instance has the given access(es) plus 'full' to. # Adds indirect accessable Groups via Roles if instance has Roles, too. # # @example Single access # user.groups_access('read') # #=> [#, ...] # # @example Access list # user.groups_access(['read', 'create']) # #=> [#, ...] # # @return [Array] Groups the instance has the given access(es) to. def groups_access(access) return [] if !active? group_ids = group_ids_access(access) Group.where(id: group_ids) end # Returns a map of Group name to access # # @example # user.group_names_access_map # #=> {'Users' => 'full', 'Support' => ['read', 'write']} # # @return [HashString,Array>] The map of Group name to access def group_names_access_map groups_access_map(:name) end # Stores a map of Group ID to access. Deletes all other relations. # # @example # user.group_names_access_map = {'Users' => 'full', 'Support' => ['read', 'write']} # #=> {'Users' => 'full', 'Support' => ['read', 'write']} # # @return [HashString,Array>] The given map def group_names_access_map=(name_access_map) groups_access_map_store(name_access_map) do |group_name| Group.where(name: group_name).pluck(:id).first end end # Returns a map of Group ID to access # # @example # user.group_ids_access_map # #=> {1 => 'full', 42 => ['read', 'write']} # # @return [HashString,Array>] The map of Group ID to access def group_ids_access_map groups_access_map(:id) end # Stores a map of Group ID to access. Deletes all other relations. # # @example # user.group_ids_access_map = {1 => 'full', 42 => ['read', 'write']} # #=> {1 => 'full', 42 => ['read', 'write']} # # @return [HashString,Array>] The given map def group_ids_access_map=(id_access_map) groups_access_map_store(id_access_map) end # An alias to .groups class method def group_through @group_through ||= self.class.group_through end private def groups_access_map(key) return {} if !active? {}.tap do |hash| groups.access.where(active: true).pluck(key, :access).each do |entry| hash[ entry[0] ] ||= [] hash[ entry[0] ].push(entry[1]) end end end def groups_access_map_store(map) map.each do |group_identifier, accesses| # use given key as identifier or look it up # via the given block which returns the identifier group_id = block_given? ? yield(group_identifier) : group_identifier if !accesses.is_a?(Array) accesses = [accesses] end accesses.each do |access| push_group_access_buffer( group_id: group_id, access: access ) end end check_group_access_buffer if id end def push_group_access_buffer(entry) @group_access_buffer ||= [] @group_access_buffer.push(entry) end def check_group_access_buffer return if group_access_buffer.blank? destroy_group_relations foreign_key = group_through.foreign_key entries = group_access_buffer.collect do |entry| entry[foreign_key] = id entry end group_through.klass.create!(entries) group_access_buffer = nil cache_delete true end def destroy_group_relations group_through.klass.destroy_all(group_through.foreign_key => id) end # methods defined here are going to extend the class, not the instance of it class_methods do # Lists IDs of instances having the given access(es) to the given Group. # # @example Group ID param # User.group_access_ids(1, 'read') # #=> [1, 3, ...] # # @example Group param # User.group_access_ids(group, 'read') # #=> [1, 3, ...] # # @example Access list # User.group_access_ids(group, ['read', 'create']) # #=> [1, 3, ...] # # @return [Array] def group_access_ids(group_id, access) group_id = ensure_group_id_parameter(group_id) access = ensure_group_access_list_parameter(access) # check direct access ids = group_through.klass.includes(name.downcase).where(group_id: group_id, access: access, table_name => { active: true }).pluck(group_through.foreign_key) ids ||= [] # check indirect access through roles if possible return ids if !respond_to?(:role_access_ids) role_instance_ids = role_access_ids(group_id, access) # combines and removes duplicates # and returns them in one statement ids | role_instance_ids end # Lists instances having the given access(es) to the given Group. # # @example Group ID param # User.group_access(1, 'read') # #=> [#, ...] # # @example Group param # User.group_access(group, 'read') # #=> [#, ...] # # @example Access list # User.group_access(group, ['read', 'create']) # #=> [#, ...] # # @return [Array] def group_access(group_id, access) instance_ids = group_access_ids(group_id, access) where(id: instance_ids) end # The reflection instance containing the association data # # @example # User.group_through # #=> # # @return [ActiveRecord::Reflection::HasManyReflection] The given map def group_through @group_through ||= reflect_on_association(group_through_identifier) end # The identifier of the has_many :through relation # # @example # User.group_through_identifier # #=> :user_groups # # @return [Symbol] The relation identifier def group_through_identifier "#{name.downcase}_groups".to_sym end def ensure_group_id_parameter(group_or_id) return group_or_id if group_or_id.is_a?(Integer) group_or_id.id end def ensure_group_access_list_parameter(access) access = [access] if access.is_a?(String) access.push('full') if !access.include?('full') access end end end