Skip to content

Commit c98febd

Browse files
committed
Fixes #38718 - Register feature scope in belongs_to_proxy
1 parent a271812 commit c98febd

File tree

8 files changed

+278
-12
lines changed

8 files changed

+278
-12
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ gem 'rest-client', '>= 2.0.0', '< 3', :require => 'rest_client'
88
gem 'audited', '~> 5.0', '!= 5.1.0'
99
gem 'will_paginate', '~> 3.3'
1010
gem 'ancestry', '~> 4.0'
11-
gem 'scoped_search', '>= 4.1.10', '< 5'
11+
gem 'scoped_search', '>= 4.3.0', '< 5'
1212
gem 'ldap_fluff', '>= 0.9.0', '< 1.0'
1313
gem 'apipie-rails', '>= 0.8.0', '< 2'
1414
gem 'apipie-dsl', '>= 2.6.2'

app/models/]

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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

app/models/concerns/belongs_to_proxies.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def belongs_to_proxy(name, options)
1616

1717
def register_smart_proxy(name, options)
1818
self.registered_smart_proxies = registered_smart_proxies.merge(name => options)
19-
belongs_to name, :class_name => 'SmartProxy'
19+
belongs_to name, -> { with_features(options[:feature]) }, :class_name => 'SmartProxy'
2020
validates name, :proxy_features => { :feature => options[:feature], :required => options[:required] }
2121
end
2222

app/validators/proxy_features_validator.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ def initialize(args)
55
end
66

77
def validate_each(record, attribute, value)
8-
if !value && @options[:required]
8+
id = record.public_send("#{attribute}_id")
9+
if (!id || !value) && @options[:required]
910
record.errors.add("#{attribute}_id", _('was not found'))
1011
end
1112

12-
if value && !value.has_feature?(@options[:feature])
13+
# Due to scope being set on the association, it can happen that the id is present but the association is nil
14+
if (id || value) && !value.try(:has_feature?, @options[:feature])
1315
if @options[:message].nil?
1416
message = _('does not have the %s feature') % @options[:feature]
1517
else

test/controllers/concerns/auto_complete_search_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ class AutoCompleteSearchTest < ActionController::TestCase
1313
assert_predicate response, :successful?
1414
suggestions = ActiveSupport::JSON.decode(response.body)
1515
assert_equal 1, suggestions.length
16-
assert_equal suggestions.first['part'], "name = #{domain1.name}"
16+
assert_equal suggestions.first['label'], "name = \"#{domain1.name}\""
1717
end
1818
end

test/controllers/subnets_controller_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ class SubnetsControllerTest < ActionController::TestCase
1616
context 'three similar subnets exists' do
1717
def setup
1818
as_admin do
19-
@s1 = FactoryBot.create(:subnet_ipv4, :network => '100.20.100.100', :cidr => '24', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
20-
@s3 = FactoryBot.create(:subnet_ipv4, :network => '200.100.100.100', :cidr => '24', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
21-
@s2 = FactoryBot.create(:subnet_ipv4, :network => '100.100.100.100', :cidr => '24', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
22-
@s4 = FactoryBot.create(:subnet_ipv6, :network => 'beef::', :cidr => '64', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
23-
@s5 = FactoryBot.create(:subnet_ipv6, :network => 'ffee::', :cidr => '64', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
19+
@s1 = FactoryBot.create(:subnet_ipv4, :dhcp, :network => '100.20.100.100', :cidr => '24', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
20+
@s3 = FactoryBot.create(:subnet_ipv4, :dhcp, :network => '200.100.100.100', :cidr => '24', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
21+
@s2 = FactoryBot.create(:subnet_ipv4, :dhcp, :network => '100.100.100.100', :cidr => '24', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
22+
@s4 = FactoryBot.create(:subnet_ipv6, :dhcp, :network => 'beef::', :cidr => '64', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
23+
@s5 = FactoryBot.create(:subnet_ipv6, :dhcp, :network => 'ffee::', :cidr => '64', :organization_ids => [taxonomies(:organization1).id], :location_ids => [taxonomies(:location1).id])
2424
end
2525
end
2626

test/models/concerns/belongs_to_proxies_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class SampleModel
55
include BelongsToProxies
66

77
class << self
8-
def belongs_to(name, options = {})
8+
def belongs_to(name, scope, options = {})
99
end
1010

1111
def validates(name, options = {})

test/unit/validators/proxy_features_validator_test.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class ProxyFeaturesValidatorTest < ActiveSupport::TestCase
44
class Validatable
55
include ActiveModel::Validations
66
validates :proxy, :proxy_features => { :feature => 'DNS' }
7-
attr_accessor :proxy
7+
attr_accessor :proxy, :proxy_id
88
end
99

1010
def setup
@@ -21,4 +21,10 @@ def setup
2121
refute_valid @validatable
2222
assert_equal ['does not have the DNS feature'], @validatable.errors[:proxy_id]
2323
end
24+
25+
test 'should fail when id is present but association returns nil' do
26+
@validatable.proxy_id = FactoryBot.create(:dhcp_smart_proxy).id
27+
refute_valid @validatable
28+
assert_equal ['does not have the DNS feature'], @validatable.errors[:proxy_id]
29+
end
2430
end

0 commit comments

Comments
 (0)