|
| 1 | +class Filter < ApplicationRecord |
| 2 | + audited :associated_with => :role |
| 3 | + |
| 4 | + include Taxonomix |
| 5 | + include Authorizable |
| 6 | + include TopbarCacheExpiry |
| 7 | + |
| 8 | + attr_writer :resource_type |
| 9 | + attr_accessor :unlimited |
| 10 | + |
| 11 | + class ScopedSearchValidator < ActiveModel::Validator |
| 12 | + def validate(record) |
| 13 | + resource_class = record.resource_class |
| 14 | + resource_class.search_for(record.search) unless (resource_class.nil? || record.search.nil?) |
| 15 | + rescue ScopedSearch::Exception => e |
| 16 | + record.errors.add(:search, _("invalid search query: %s") % e) |
| 17 | + end |
| 18 | + end |
| 19 | + |
| 20 | + # tune up taxonomix for filters, we don't want to set current taxonomy |
| 21 | + def add_current_organization? |
| 22 | + false |
| 23 | + end |
| 24 | + |
| 25 | + def add_current_location? |
| 26 | + false |
| 27 | + end |
| 28 | + |
| 29 | + # allow creating filters for non-taxable resources when user is not admin |
| 30 | + def ensure_taxonomies_not_escalated |
| 31 | + end |
| 32 | + |
| 33 | + belongs_to :role |
| 34 | + has_many :filterings, :autosave => true, :dependent => :destroy |
| 35 | + has_many :permissions, :through => :filterings |
| 36 | + |
| 37 | + validates_lengths_from_database |
| 38 | + |
| 39 | + default_scope -> { order(["#{table_name}.role_id", "#{table_name}.id"]) } |
| 40 | + scope :unlimited, -> { where(:search => nil, :taxonomy_search => nil) } |
| 41 | + scope :limited, -> { where("search IS NOT NULL OR taxonomy_search IS NOT NULL") } |
| 42 | + |
| 43 | + scoped_search :on => :id, :complete_enabled => false, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER |
| 44 | + scoped_search :on => :search, :complete_value => true |
| 45 | + scoped_search :on => :override, :complete_value => { :true => true, :false => false } |
| 46 | + scoped_search :on => :limited, :complete_value => { :true => true, :false => false }, :ext_method => :search_by_limited, :only_explicit => true |
| 47 | + scoped_search :on => :unlimited, :complete_value => { :true => true, :false => false }, :ext_method => :search_by_unlimited, :only_explicit => true |
| 48 | + scoped_search :relation => :role, :on => :id, :rename => :role_id, :complete_enabled => false, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER |
| 49 | + scoped_search :relation => :role, :on => :name, :rename => :role |
| 50 | + scoped_search :relation => :permissions, :on => :resource_type, :rename => :resource |
| 51 | + scoped_search :relation => :permissions, :on => :name, :rename => :permission |
| 52 | + |
| 53 | + before_validation :build_taxonomy_search, :nilify_empty_searches, :enforce_override_flag |
| 54 | + before_save :enforce_inherited_taxonomies, :nilify_empty_searches |
| 55 | + |
| 56 | + validates :search, :presence => true, :unless => proc { |o| o.search.nil? } |
| 57 | + validates_with ScopedSearchValidator |
| 58 | + validates :role, :presence => true |
| 59 | + |
| 60 | + validate :role_not_locked |
| 61 | + before_destroy :role_not_locked |
| 62 | + |
| 63 | + validate :same_resource_type_permissions, :not_empty_permissions, :allowed_taxonomies |
| 64 | + |
| 65 | + def self.allows_taxonomy_filtering?(_taxonomy) |
| 66 | + false |
| 67 | + end |
| 68 | + |
| 69 | + def self.search_by_unlimited(key, operator, value) |
| 70 | + search_by_limited(key, operator, (value == 'true') ? 'false' : 'true') |
| 71 | + end |
| 72 | + |
| 73 | + def self.search_by_limited(key, operator, value) |
| 74 | + value = value == 'true' |
| 75 | + value = !value if operator == '<>' |
| 76 | + conditions = value ? 'search IS NOT NULL OR taxonomy_search IS NOT NULL' : 'search IS NULL AND taxonomy_search IS NULL' |
| 77 | + { :conditions => conditions } |
| 78 | + end |
| 79 | + |
| 80 | + # This method attempts to return an existing class that is derived from the resource_type. |
| 81 | + # In some instances, this may not be a real class (e.g. a typo) or may be nil in the case |
| 82 | + # of a filter not having been saved yet and thus the permissions objects not being currently |
| 83 | + # accessible. |
| 84 | + def self.get_resource_class(resource_type) |
| 85 | + return nil if resource_type.nil? |
| 86 | + resource_type.constantize |
| 87 | + rescue NameError => e |
| 88 | + Foreman::Logging.exception("unknown class #{resource_type}, ignoring", e) |
| 89 | + nil |
| 90 | + end |
| 91 | + |
| 92 | + def unlimited? |
| 93 | + search.nil? && taxonomy_search.nil? |
| 94 | + end |
| 95 | + |
| 96 | + def limited? |
| 97 | + !unlimited? |
| 98 | + end |
| 99 | + |
| 100 | + def to_s |
| 101 | + _('filter for %s role') % role.try(:name) || 'unknown' |
| 102 | + end |
| 103 | + |
| 104 | + def to_label |
| 105 | + permissions.pluck(:name).to_sentence |
| 106 | + end |
| 107 | + |
| 108 | + def resource_type |
| 109 | + type = @resource_type || filterings.first.try(:permission).try(:resource_type) |
| 110 | + type.presence |
| 111 | + end |
| 112 | + |
| 113 | + def resource_type_label |
| 114 | + resource_class.try(:humanize_class_name) || resource_type || N_('(Miscellaneous)') |
| 115 | + end |
| 116 | + |
| 117 | + def resource_class |
| 118 | + @resource_class ||= self.class.get_resource_class(resource_type) |
| 119 | + end |
| 120 | + |
| 121 | + # We detect granularity by inclusion of Authorizable module and scoped_search definition |
| 122 | + # we can define exceptions for resources with more complex hierarchy (e.g. Host is proxy module) |
| 123 | + def granular? |
| 124 | + @granular ||= begin |
| 125 | + return false if resource_class.nil? |
| 126 | + return true if resource_type == 'Host' |
| 127 | + resource_class.included_modules.include?(Authorizable) && resource_class.respond_to?(:search_for) |
| 128 | + end |
| 129 | + end |
| 130 | + |
| 131 | + def resource_taxable? |
| 132 | + resource_taxable_by_organization? || resource_taxable_by_location? |
| 133 | + end |
| 134 | + |
| 135 | + def resource_taxable_by_organization? |
| 136 | + granular? && resource_class.allows_organization_filtering? |
| 137 | + end |
| 138 | + |
| 139 | + def resource_taxable_by_location? |
| 140 | + granular? && resource_class.allows_location_filtering? |
| 141 | + end |
| 142 | + |
| 143 | + def search_condition |
| 144 | + QueryBuilder.join('AND', search_condition_parts) |
| 145 | + end |
| 146 | + |
| 147 | + def search_condition_for_user(user) |
| 148 | + return search_condition if override? || !granular? |
| 149 | + |
| 150 | + parts = search_condition_parts |
| 151 | + parts += taxonomy_search_condition_for_user(user) |
| 152 | + QueryBuilder.join('AND', parts) |
| 153 | + end |
| 154 | + |
| 155 | + def taxonomy_search_condition_for_user(user) |
| 156 | + parts = [] |
| 157 | + if resource_taxable_by_organization? |
| 158 | + parts << merge_taxonomy_search('organization_id', taxonomy_search, user.organization_and_child_ids) |
| 159 | + end |
| 160 | + if resource_taxable_by_location? |
| 161 | + parts << merge_taxonomy_search('location_id', taxonomy_search, user.location_and_child_ids) |
| 162 | + end |
| 163 | + parts |
| 164 | + end |
| 165 | + |
| 166 | + def merge_taxonomy_search(key, search, user_ids) |
| 167 | + ids = if search.nil? |
| 168 | + user_ids |
| 169 | + else |
| 170 | + search.scan(/\d+/).map(&:to_i) & user_ids |
| 171 | + end |
| 172 | + QueryBuilder.key_value_in(key, ids, :block) |
| 173 | + end |
| 174 | + |
| 175 | + def expire_topbar_cache |
| 176 | + role.users.each { |u| u.expire_topbar_cache } |
| 177 | + role.usergroups.each { |g| g.expire_topbar_cache } |
| 178 | + end |
| 179 | + |
| 180 | + def disable_overriding! |
| 181 | + self.override = false |
| 182 | + save! |
| 183 | + end |
| 184 | + |
| 185 | + def enforce_inherited_taxonomies |
| 186 | + inherit_taxonomies! unless override? |
| 187 | + end |
| 188 | + |
| 189 | + def inherit_taxonomies! |
| 190 | + self.organization_ids = role.organization_ids if resource_taxable_by_organization? |
| 191 | + self.location_ids = role.location_ids if resource_taxable_by_location? |
| 192 | + build_taxonomy_search |
| 193 | + end |
| 194 | + |
| 195 | + private |
| 196 | + |
| 197 | + def search_condition_parts |
| 198 | + [search, taxonomy_search].compact |
| 199 | + end |
| 200 | + |
| 201 | + def build_taxonomy_search |
| 202 | + orgs = build_taxonomy_search_string('organization') |
| 203 | + locs = build_taxonomy_search_string('location') |
| 204 | + |
| 205 | + self.taxonomy_search = QueryBuilder.join('AND', [orgs, locs]) |
| 206 | + end |
| 207 | + |
| 208 | + def build_taxonomy_search_string(name) |
| 209 | + return unless send("resource_taxable_by_#{name}?") |
| 210 | + relation = send(name.pluralize).pluck(:id) |
| 211 | + |
| 212 | + QueryBuilder.key_value_in("#{name}_id", relation) |
| 213 | + end |
| 214 | + |
| 215 | + def nilify_empty_searches |
| 216 | + self.search = nil if search.empty? || unlimited == '1' |
| 217 | + self.taxonomy_search = nil if taxonomy_search.empty? |
| 218 | + end |
| 219 | + |
| 220 | + # if we have 0 types, empty validation will set error, we can't have more than one type |
| 221 | + def same_resource_type_permissions |
| 222 | + types = permissions.map(&:resource_type).uniq |
| 223 | + if types.size > 1 |
| 224 | + errors.add( |
| 225 | + :permissions, |
| 226 | + _('must be of same resource type (%{types}) - Role (%{role})') % |
| 227 | + { |
| 228 | + types: types.join(','), |
| 229 | + role: role.name, |
| 230 | + } |
| 231 | + ) |
| 232 | + end |
| 233 | + end |
| 234 | + |
| 235 | + def not_empty_permissions |
| 236 | + errors.add(:permissions, _('You must select at least one permission')) if permissions.blank? && filterings.blank? |
| 237 | + end |
| 238 | + |
| 239 | + def allowed_taxonomies |
| 240 | + if organization_ids.present? && !resource_taxable_by_organization? |
| 241 | + errors.add(:organization_ids, _('You can\'t assign organizations to this resource')) |
| 242 | + end |
| 243 | + |
| 244 | + if location_ids.present? && !resource_taxable_by_location? |
| 245 | + errors.add(:location_ids, _('You can\'t assign locations to this resource')) |
| 246 | + end |
| 247 | + end |
| 248 | + |
| 249 | + def enforce_override_flag |
| 250 | + self.override = false unless resource_taxable? |
| 251 | + true |
| 252 | + end |
| 253 | + |
| 254 | + def role_not_locked |
| 255 | + errors.add(:role_id, _('is locked for user modifications.')) if role.locked? && !role.modify_locked |
| 256 | + errors.empty? |
| 257 | + end |
| 258 | +end |
0 commit comments