cancancan 2.3.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/cancancan.gemspec +6 -5
  3. data/init.rb +2 -0
  4. data/lib/cancan/ability/actions.rb +2 -0
  5. data/lib/cancan/ability/rules.rb +20 -9
  6. data/lib/cancan/ability/strong_parameter_support.rb +41 -0
  7. data/lib/cancan/ability.rb +54 -24
  8. data/lib/cancan/class_matcher.rb +30 -0
  9. data/lib/cancan/conditions_matcher.rb +72 -18
  10. data/lib/cancan/config.rb +101 -0
  11. data/lib/cancan/controller_additions.rb +5 -2
  12. data/lib/cancan/controller_resource.rb +7 -1
  13. data/lib/cancan/controller_resource_builder.rb +2 -0
  14. data/lib/cancan/controller_resource_finder.rb +2 -0
  15. data/lib/cancan/controller_resource_loader.rb +4 -0
  16. data/lib/cancan/controller_resource_name_finder.rb +2 -0
  17. data/lib/cancan/controller_resource_sanitizer.rb +2 -0
  18. data/lib/cancan/exceptions.rb +18 -2
  19. data/lib/cancan/matchers.rb +7 -2
  20. data/lib/cancan/model_adapters/abstract_adapter.rb +22 -1
  21. data/lib/cancan/model_adapters/active_record_4_adapter.rb +26 -25
  22. data/lib/cancan/model_adapters/active_record_5_adapter.rb +17 -26
  23. data/lib/cancan/model_adapters/active_record_adapter.rb +130 -15
  24. data/lib/cancan/model_adapters/conditions_extractor.rb +7 -7
  25. data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
  26. data/lib/cancan/model_adapters/default_adapter.rb +2 -0
  27. data/lib/cancan/model_adapters/sti_normalizer.rb +47 -0
  28. data/lib/cancan/model_adapters/strategies/base.rb +40 -0
  29. data/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +93 -0
  30. data/lib/cancan/model_adapters/strategies/joined_alias_exists_subquery.rb +31 -0
  31. data/lib/cancan/model_adapters/strategies/left_join.rb +11 -0
  32. data/lib/cancan/model_adapters/strategies/subquery.rb +18 -0
  33. data/lib/cancan/model_additions.rb +6 -2
  34. data/lib/cancan/parameter_validators.rb +9 -0
  35. data/lib/cancan/relevant.rb +29 -0
  36. data/lib/cancan/rule.rb +67 -23
  37. data/lib/cancan/rules_compressor.rb +3 -0
  38. data/lib/cancan/sti_detector.rb +12 -0
  39. data/lib/cancan/unauthorized_message_resolver.rb +24 -0
  40. data/lib/cancan/version.rb +3 -1
  41. data/lib/cancan.rb +11 -0
  42. data/lib/cancancan.rb +2 -0
  43. data/lib/generators/cancan/ability/ability_generator.rb +3 -1
  44. data/lib/generators/cancan/ability/templates/ability.rb +9 -9
  45. metadata +37 -24
  46. data/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f63f1d6ab266068caab6e7baf2b628ae72a7e03a134116eda91473861b9df359
4
- data.tar.gz: fbf6337f058ece9a2f01c990a55398489e0ea56888f0a5ce3b9f8d5e203c31ae
3
+ metadata.gz: bebbba60e68460ec234fc11e8d3cf0414e578a56c0347862c673396eb917dff9
4
+ data.tar.gz: bb07244a17dcf45d1852cf6677864084c2f0db5630ea9b72bdcc0c6055b5c4b6
5
5
  SHA512:
6
- metadata.gz: 24fcc98ce0592b263add65cf0bc75a7020d75777fea7c6902216f97bbc9c13a36b4d379194a142cd8a5e5a24dc1ae82c82a61d6d99143c30a9afe11133048528
7
- data.tar.gz: 8873b440698a941314f67a748db86c2abe33e89417ae54d9f35769c29f904edc9eff5736d0bf55d53badc494b9bca9e0ac1761c1920067238628d23dc8e0c643
6
+ metadata.gz: be9f2b03ae43651ea70a451b97a44fd6ec6e0a09ca444ddf625b91ae3815a245e0669bb80b4d3b0687ca327bc0c4fe81028f7736cb6493ef636b43a4140f4f49
7
+ data.tar.gz: db75441929e737d12699f57324d031e894b5d2cdbe1555451857977c42b0ef28148cc63bb718981c32ac08bafd2873623d2151ba8e98c442f217b3f4affecda9
data/cancancan.gemspec CHANGED
@@ -1,6 +1,6 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'cancan/version'
6
6
 
@@ -10,6 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.authors = ['Alessandro Rodi (Renuo AG)', 'Bryan Rite', 'Ryan Bates', 'Richard Wilson']
11
11
  s.email = 'alessandro.rodi@renuo.ch'
12
12
  s.homepage = 'https://github.com/CanCanCommunity/cancancan'
13
+ s.metadata = { 'funding_uri' => 'https://github.com/sponsors/coorasse' }
13
14
  s.summary = 'Simple authorization solution for Rails.'
14
15
  s.description = 'Simple authorization solution for Rails. All permissions are stored in a single location.'
15
16
  s.platform = Gem::Platform::RUBY
@@ -20,9 +21,9 @@ Gem::Specification.new do |s|
20
21
 
21
22
  s.required_ruby_version = '>= 2.2.0'
22
23
 
23
- s.add_development_dependency 'bundler', '~> 1.3'
24
- s.add_development_dependency 'rubocop', '~> 0.48.1'
24
+ s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
25
+ s.add_development_dependency 'bundler', '~> 2.0'
25
26
  s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
26
27
  s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
27
- s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
28
+ s.add_development_dependency 'rubocop', '~> 1.31.1'
28
29
  end
data/init.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cancan'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module Ability
3
5
  module Actions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module Ability
3
5
  module Rules
@@ -17,12 +19,13 @@ module CanCan
17
19
  end
18
20
 
19
21
  def add_rule_to_index(rule, position)
20
- @rules_index ||= Hash.new { |h, k| h[k] = [] }
22
+ @rules_index ||= {}
21
23
 
22
24
  subjects = rule.subjects.compact
23
25
  subjects << :all if subjects.empty?
24
26
 
25
27
  subjects.each do |subject|
28
+ @rules_index[subject] ||= []
26
29
  @rules_index[subject] << position
27
30
  end
28
31
  end
@@ -31,6 +34,7 @@ module CanCan
31
34
  # This does not take into consideration any hash conditions or block statements
32
35
  def relevant_rules(action, subject)
33
36
  return [] unless @rules
37
+
34
38
  relevant = possible_relevant_rules(subject).select do |rule|
35
39
  rule.expanded_actions = expand_actions(rule.actions)
36
40
  rule.relevant? action, subject
@@ -45,7 +49,9 @@ module CanCan
45
49
  rules
46
50
  else
47
51
  positions = @rules_index.values_at(subject, *alternative_subjects(subject))
48
- positions.flatten!.sort!
52
+ positions.compact!
53
+ positions.flatten!
54
+ positions.sort!
49
55
  positions.map { |i| @rules[i] }
50
56
  end
51
57
  end
@@ -53,19 +59,23 @@ module CanCan
53
59
  def relevant_rules_for_match(action, subject)
54
60
  relevant_rules(action, subject).each do |rule|
55
61
  next unless rule.only_raw_sql?
62
+
56
63
  raise Error,
57
- "The can? and cannot? call cannot be used with a raw sql 'can' definition."\
58
- " The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
64
+ "The can? and cannot? call cannot be used with a raw sql 'can' definition. " \
65
+ "The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
59
66
  end
60
67
  end
61
68
 
62
69
  def relevant_rules_for_query(action, subject)
63
- relevant_rules(action, subject).each do |rule|
64
- if rule.only_block?
65
- raise Error, "The accessible_by call cannot be used with a block 'can' definition."\
66
- " The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
67
- end
70
+ rules = relevant_rules(action, subject).reject do |rule|
71
+ # reject 'cannot' rules with attributes when doing queries
72
+ rule.base_behavior == false && rule.attributes.present?
73
+ end
74
+ if rules.any?(&:only_block?)
75
+ raise Error, "The accessible_by call cannot be used with a block 'can' definition." \
76
+ "The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
68
77
  end
78
+ rules
69
79
  end
70
80
 
71
81
  # Optimizes the order of the rules, so that rules with the :all subject are evaluated first.
@@ -75,6 +85,7 @@ module CanCan
75
85
  (first_can_in_group = -1) && next unless rule.base_behavior
76
86
  (first_can_in_group = i) && next if first_can_in_group == -1
77
87
  next unless rule.subjects == [:all]
88
+
78
89
  rules[i] = rules[first_can_in_group]
79
90
  rules[first_can_in_group] = rule
80
91
  first_can_in_group += 1
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module StrongParameterSupport
6
+ # Returns an array of attributes suitable for use with strong parameters
7
+ #
8
+ # Note: reversing the relevant rules is important. Normal order means that 'cannot'
9
+ # rules will come before 'can' rules. However, you can't remove attributes before
10
+ # they are added. The 'reverse' is so that attributes will be added before the
11
+ # 'cannot' rules remove them.
12
+ def permitted_attributes(action, subject)
13
+ relevant_rules(action, subject)
14
+ .reverse
15
+ .select { |rule| rule.matches_conditions? action, subject }
16
+ .each_with_object(Set.new) do |rule, set|
17
+ attributes = get_attributes(rule, subject)
18
+ # add attributes for 'can', remove them for 'cannot'
19
+ rule.base_behavior ? set.merge(attributes) : set.subtract(attributes)
20
+ end.to_a
21
+ end
22
+
23
+ private
24
+
25
+ def subject_class?(subject)
26
+ klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
27
+ [Class, Module].include? klass
28
+ end
29
+
30
+ def get_attributes(rule, subject)
31
+ klass = subject_class?(subject) ? subject : subject.class
32
+ # empty attributes is an 'all'
33
+ if rule.attributes.empty? && klass < ActiveRecord::Base
34
+ klass.column_names.map(&:to_sym) - Array(klass.primary_key)
35
+ else
36
+ rule.attributes
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'ability/rules.rb'
2
4
  require_relative 'ability/actions.rb'
5
+ require_relative 'unauthorized_message_resolver.rb'
6
+ require_relative 'ability/strong_parameter_support'
7
+
3
8
  module CanCan
4
9
  # This module is designed to be included into an Ability class. This will
5
10
  # provide the "can" methods for defining and checking abilities.
@@ -19,6 +24,8 @@ module CanCan
19
24
  module Ability
20
25
  include CanCan::Ability::Rules
21
26
  include CanCan::Ability::Actions
27
+ include CanCan::UnauthorizedMessageResolver
28
+ include StrongParameterSupport
22
29
 
23
30
  # Check if the user has permission to perform a given action on an object.
24
31
  #
@@ -64,10 +71,10 @@ module CanCan
64
71
  # end
65
72
  #
66
73
  # Also see the RSpec Matchers to aid in testing.
67
- def can?(action, subject, *extra_args)
74
+ def can?(action, subject, attribute = nil, *extra_args)
68
75
  match = extract_subjects(subject).lazy.map do |a_subject|
69
76
  relevant_rules_for_match(action, a_subject).detect do |rule|
70
- rule.matches_conditions?(action, a_subject, extra_args)
77
+ rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
71
78
  end
72
79
  end.reject(&:nil?).first
73
80
  match ? match.base_behavior : false
@@ -134,8 +141,8 @@ module CanCan
134
141
  # # check the database and return true/false
135
142
  # end
136
143
  #
137
- def can(action = nil, subject = nil, conditions = nil, &block)
138
- add_rule(Rule.new(true, action, subject, conditions, block))
144
+ def can(action = nil, subject = nil, *attributes_and_conditions, &block)
145
+ add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
139
146
  end
140
147
 
141
148
  # Defines an ability which cannot be done. Accepts the same arguments as "can".
@@ -150,8 +157,8 @@ module CanCan
150
157
  # product.invisible?
151
158
  # end
152
159
  #
153
- def cannot(action = nil, subject = nil, conditions = nil, &block)
154
- add_rule(Rule.new(false, action, subject, conditions, block))
160
+ def cannot(action = nil, subject = nil, *attributes_and_conditions, &block)
161
+ add_rule(Rule.new(false, action, subject, *attributes_and_conditions, &block))
155
162
  end
156
163
 
157
164
  # User shouldn't specify targets with names of real actions or it will cause Seg fault
@@ -167,10 +174,7 @@ module CanCan
167
174
 
168
175
  # See ControllerAdditions#authorize! for documentation.
169
176
  def authorize!(action, subject, *args)
170
- message = nil
171
- if args.last.is_a?(Hash) && args.last.key?(:message)
172
- message = args.pop[:message]
173
- end
177
+ message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil
174
178
  if cannot?(action, subject, *args)
175
179
  message ||= unauthorized_message(action, subject)
176
180
  raise AccessDenied.new(message, action, subject, args)
@@ -178,14 +182,6 @@ module CanCan
178
182
  subject
179
183
  end
180
184
 
181
- def unauthorized_message(action, subject)
182
- keys = unauthorized_message_keys(action, subject)
183
- variables = { action: action.to_s }
184
- variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase
185
- message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + ['']))
186
- message.blank? ? nil : message
187
- end
188
-
189
185
  def attributes_for(action, subject)
190
186
  attributes = {}
191
187
  relevant_rules(action, subject).map do |rule|
@@ -202,12 +198,13 @@ module CanCan
202
198
  relevant_rules(action, subject).any?(&:only_raw_sql?)
203
199
  end
204
200
 
205
- # Copies all rules of the given +CanCan::Ability+ and adds them to +self+.
201
+ # Copies all rules and aliased actions of the given +CanCan::Ability+ and adds them to +self+.
206
202
  # class ReadAbility
207
203
  # include CanCan::Ability
208
204
  #
209
205
  # def initialize
210
206
  # can :read, User
207
+ # alias_action :show, :index, to: :see
211
208
  # end
212
209
  # end
213
210
  #
@@ -216,6 +213,7 @@ module CanCan
216
213
  #
217
214
  # def initialize
218
215
  # can :edit, User
216
+ # alias_action :create, :update, to: :modify
219
217
  # end
220
218
  # end
221
219
  #
@@ -223,11 +221,35 @@ module CanCan
223
221
  # read_ability.can? :edit, User.new #=> false
224
222
  # read_ability.merge(WritingAbility.new)
225
223
  # read_ability.can? :edit, User.new #=> true
224
+ # read_ability.aliased_actions #=> [:see => [:show, :index], :modify => [:create, :update]]
225
+ #
226
+ # If there are collisions when merging the +aliased_actions+, the actions on +self+ will be
227
+ # overwritten.
226
228
  #
229
+ # class ReadAbility
230
+ # include CanCan::Ability
231
+ #
232
+ # def initialize
233
+ # alias_action :show, :index, to: :see
234
+ # end
235
+ # end
236
+ #
237
+ # class ShowAbility
238
+ # include CanCan::Ability
239
+ #
240
+ # def initialize
241
+ # alias_action :show, to: :see
242
+ # end
243
+ # end
244
+ #
245
+ # read_ability = ReadAbility.new
246
+ # read_ability.merge(ShowAbility)
247
+ # read_ability.aliased_actions #=> [:see => [:show]]
227
248
  def merge(ability)
228
249
  ability.rules.each do |rule|
229
250
  add_rule(rule.dup)
230
251
  end
252
+ @aliased_actions = aliased_actions.merge(ability.aliased_actions)
231
253
  self
232
254
  end
233
255
 
@@ -239,10 +261,13 @@ module CanCan
239
261
  #
240
262
  # Where can_hash and cannot_hash are formatted thusly:
241
263
  # {
242
- # action: array_of_objects
264
+ # action: { subject: [attributes] }
243
265
  # }
244
266
  def permissions
245
- permissions_list = { can: {}, cannot: {} }
267
+ permissions_list = {
268
+ can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } },
269
+ cannot: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }
270
+ }
246
271
  rules.each { |rule| extract_rule_in_permissions(permissions_list, rule) }
247
272
  permissions_list
248
273
  end
@@ -250,8 +275,9 @@ module CanCan
250
275
  def extract_rule_in_permissions(permissions_list, rule)
251
276
  expand_actions(rule.actions).each do |action|
252
277
  container = rule.base_behavior ? :can : :cannot
253
- permissions_list[container][action] ||= []
254
- permissions_list[container][action] += rule.subjects.map(&:to_s)
278
+ rule.subjects.each do |subject|
279
+ permissions_list[container][action][subject.to_s] += rule.attributes
280
+ end
255
281
  end
256
282
  end
257
283
 
@@ -276,7 +302,11 @@ module CanCan
276
302
 
277
303
  def alternative_subjects(subject)
278
304
  subject = subject.class unless subject.is_a?(Module)
279
- [:all, *subject.ancestors, subject.class.to_s]
305
+ if subject.respond_to?(:subclasses) && defined?(ActiveRecord::Base) && subject < ActiveRecord::Base
306
+ [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s]
307
+ else
308
+ [:all, *subject.ancestors, subject.class.to_s]
309
+ end
280
310
  end
281
311
  end
282
312
  end
@@ -0,0 +1,30 @@
1
+ require_relative 'sti_detector'
2
+
3
+ # This class is responsible for matching classes and their subclasses as well as
4
+ # upmatching classes to their ancestors.
5
+ # This is used to generate sti connections
6
+ class SubjectClassMatcher
7
+ def self.matches_subject_class?(subjects, subject)
8
+ subjects.any? do |sub|
9
+ has_subclasses = subject.respond_to?(:subclasses)
10
+ matching_class_check(subject, sub, has_subclasses)
11
+ end
12
+ end
13
+
14
+ def self.matching_class_check(subject, sub, has_subclasses)
15
+ matches = matches_class_or_is_related(subject, sub)
16
+ if has_subclasses
17
+ return matches unless StiDetector.sti_class?(sub)
18
+
19
+ matches || subject.subclasses.include?(sub)
20
+ else
21
+ matches
22
+ end
23
+ end
24
+
25
+ def self.matches_class_or_is_related(subject, sub)
26
+ sub.is_a?(Module) && (subject.is_a?(sub) ||
27
+ subject.class.to_s == sub.to_s ||
28
+ (subject.is_a?(Module) && subject.ancestors.include?(sub)))
29
+ end
30
+ end
@@ -1,49 +1,83 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ConditionsMatcher
3
5
  # Matches the block or conditions hash
4
- def matches_conditions?(action, subject, extra_args)
6
+ def matches_conditions?(action, subject, attribute = nil, *extra_args)
5
7
  return call_block_with_all(action, subject, extra_args) if @match_all
6
- return @block.call(subject, *extra_args) if @block && !subject_class?(subject)
7
- matches_non_block_conditions(subject)
8
+ return matches_block_conditions(subject, attribute, *extra_args) if @block
9
+ return matches_non_block_conditions(subject) unless conditions_empty?
10
+
11
+ true
8
12
  end
9
13
 
10
14
  private
11
15
 
12
16
  def subject_class?(subject)
13
17
  klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
14
- klass == Class || klass == Module
18
+ [Class, Module].include? klass
15
19
  end
16
20
 
17
- def matches_non_block_conditions(subject)
18
- if @conditions.is_a?(Hash)
19
- return nested_subject_matches_conditions?(subject) if subject.class == Hash
20
- return matches_conditions_hash?(subject) unless subject_class?(subject)
21
+ def matches_block_conditions(subject, attribute, *extra_args)
22
+ return @base_behavior if subject_class?(subject)
23
+
24
+ if attribute
25
+ @block.call(subject, attribute, *extra_args)
26
+ else
27
+ @block.call(subject, *extra_args)
21
28
  end
29
+ end
30
+
31
+ def matches_non_block_conditions(subject)
32
+ return nested_subject_matches_conditions?(subject) if subject.class == Hash
33
+ return matches_conditions_hash?(subject) unless subject_class?(subject)
34
+
22
35
  # Don't stop at "cannot" definitions when there are conditions.
23
- conditions_empty? ? true : @base_behavior
36
+ @base_behavior
24
37
  end
25
38
 
26
39
  def nested_subject_matches_conditions?(subject_hash)
27
- parent, _child = subject_hash.first
28
- matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
40
+ parent, child = subject_hash.first
41
+
42
+ adapter = model_adapter(parent)
43
+
44
+ parent_condition_name = adapter.parent_condition_name(parent, child)
45
+
46
+ matches_base_parent_conditions = matches_conditions_hash?(parent,
47
+ @conditions[parent_condition_name] || {})
48
+
49
+ matches_base_parent_conditions &&
50
+ (!adapter.override_nested_subject_conditions_matching?(parent, child, @conditions) ||
51
+ adapter.nested_subject_matches_conditions?(parent, child, @conditions))
29
52
  end
30
53
 
31
54
  # Checks if the given subject matches the given conditions hash.
32
- # This behavior can be overriden by a model adapter by defining two class methods:
55
+ # This behavior can be overridden by a model adapter by defining two class methods:
33
56
  # override_matching_for_conditions?(subject, conditions) and
34
57
  # matches_conditions_hash?(subject, conditions)
35
58
  def matches_conditions_hash?(subject, conditions = @conditions)
36
- return true if conditions.empty?
59
+ return true if conditions.is_a?(Hash) && conditions.empty?
60
+
37
61
  adapter = model_adapter(subject)
38
62
 
39
63
  if adapter.override_conditions_hash_matching?(subject, conditions)
40
64
  return adapter.matches_conditions_hash?(subject, conditions)
41
65
  end
42
66
 
43
- matches_all_conditions?(adapter, conditions, subject)
67
+ matches_all_conditions?(adapter, subject, conditions)
68
+ end
69
+
70
+ def matches_all_conditions?(adapter, subject, conditions)
71
+ if conditions.is_a?(Hash)
72
+ matches_hash_conditions?(adapter, subject, conditions)
73
+ elsif conditions.respond_to?(:include?)
74
+ conditions.include?(subject)
75
+ else
76
+ subject == conditions
77
+ end
44
78
  end
45
79
 
46
- def matches_all_conditions?(adapter, conditions, subject)
80
+ def matches_hash_conditions?(adapter, subject, conditions)
47
81
  conditions.all? do |name, value|
48
82
  if adapter.override_condition_matching?(subject, name, value)
49
83
  adapter.matches_condition?(subject, name, value)
@@ -68,13 +102,30 @@ module CanCan
68
102
 
69
103
  def hash_condition_match?(attribute, value)
70
104
  if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
71
- attribute.any? { |element| matches_conditions_hash?(element, value) }
105
+ array_like_matches_condition_hash?(attribute, value)
72
106
  else
73
107
  attribute && matches_conditions_hash?(attribute, value)
74
108
  end
75
109
  end
76
110
 
77
- def call_block_with_all(action, subject, extra_args)
111
+ def array_like_matches_condition_hash?(attribute, value)
112
+ if attribute.any?
113
+ attribute.any? { |element| matches_conditions_hash?(element, value) }
114
+ else
115
+ # you can use `nil`s in your ability definition to tell cancancan to find
116
+ # objects that *don't* have any children in a has_many relationship.
117
+ #
118
+ # for example, given ability:
119
+ # => can :read, Article, comments: { id: nil }
120
+ # cancancan will return articles where `article.comments == []`
121
+ #
122
+ # this is implemented here. `attribute` is `article.comments`, and it's an empty array.
123
+ # the expression below returns true if this was expected.
124
+ !value.values.empty? && value.values.all?(&:nil?)
125
+ end
126
+ end
127
+
128
+ def call_block_with_all(action, subject, *extra_args)
78
129
  if subject.class == Class
79
130
  @block.call(action, subject, nil, *extra_args)
80
131
  else
@@ -87,7 +138,10 @@ module CanCan
87
138
  end
88
139
 
89
140
  def conditions_empty?
90
- @conditions == {} || @conditions.nil?
141
+ # @conditions might be an ActiveRecord::Associations::CollectionProxy
142
+ # which it's `==` implementation will fetch all records for comparison
143
+
144
+ (@conditions.is_a?(Hash) && @conditions == {}) || @conditions.nil?
91
145
  end
92
146
  end
93
147
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ def self.valid_accessible_by_strategies
5
+ strategies = [:left_join]
6
+
7
+ unless does_not_support_subquery_strategy?
8
+ strategies.push(:joined_alias_exists_subquery, :joined_alias_each_rule_as_exists_subquery, :subquery)
9
+ end
10
+
11
+ strategies
12
+ end
13
+
14
+ # You can disable the rules compressor if it's causing unexpected issues.
15
+ def self.rules_compressor_enabled
16
+ return @rules_compressor_enabled if defined?(@rules_compressor_enabled)
17
+
18
+ @rules_compressor_enabled = true
19
+ end
20
+
21
+ def self.rules_compressor_enabled=(value)
22
+ @rules_compressor_enabled = value
23
+ end
24
+
25
+ def self.with_rules_compressor_enabled(value)
26
+ return yield if value == rules_compressor_enabled
27
+
28
+ begin
29
+ rules_compressor_enabled_was = rules_compressor_enabled
30
+ @rules_compressor_enabled = value
31
+ yield
32
+ ensure
33
+ @rules_compressor_enabled = rules_compressor_enabled_was
34
+ end
35
+ end
36
+
37
+ # Determines how CanCan should build queries when calling accessible_by,
38
+ # if the query will contain a join. The default strategy is `:subquery`.
39
+ #
40
+ # # config/initializers/cancan.rb
41
+ # CanCan.accessible_by_strategy = :subquery
42
+ #
43
+ # Valid strategies are:
44
+ # - :subquery - Creates a nested query with all joins, wrapped by a
45
+ # WHERE IN query.
46
+ # - :left_join - Calls the joins directly using `left_joins`, and
47
+ # ensures records are unique using `distinct`. Note that
48
+ # `distinct` is not reliable in some cases. See
49
+ # https://github.com/CanCanCommunity/cancancan/pull/605
50
+ def self.accessible_by_strategy
51
+ return @accessible_by_strategy if @accessible_by_strategy
52
+
53
+ @accessible_by_strategy = default_accessible_by_strategy
54
+ end
55
+
56
+ def self.default_accessible_by_strategy
57
+ if does_not_support_subquery_strategy?
58
+ # see https://github.com/CanCanCommunity/cancancan/pull/655 for where this was added
59
+ # the `subquery` strategy (from https://github.com/CanCanCommunity/cancancan/pull/619
60
+ # only works in Rails 5 and higher
61
+ :left_join
62
+ else
63
+ :subquery
64
+ end
65
+ end
66
+
67
+ def self.accessible_by_strategy=(value)
68
+ validate_accessible_by_strategy!(value)
69
+
70
+ if value == :subquery && does_not_support_subquery_strategy?
71
+ raise ArgumentError, 'accessible_by_strategy = :subquery requires ActiveRecord 5 or newer'
72
+ end
73
+
74
+ @accessible_by_strategy = value
75
+ end
76
+
77
+ def self.with_accessible_by_strategy(value)
78
+ return yield if value == accessible_by_strategy
79
+
80
+ validate_accessible_by_strategy!(value)
81
+
82
+ begin
83
+ strategy_was = accessible_by_strategy
84
+ @accessible_by_strategy = value
85
+ yield
86
+ ensure
87
+ @accessible_by_strategy = strategy_was
88
+ end
89
+ end
90
+
91
+ def self.validate_accessible_by_strategy!(value)
92
+ return if valid_accessible_by_strategies.include?(value)
93
+
94
+ raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}"
95
+ end
96
+
97
+ def self.does_not_support_subquery_strategy?
98
+ !defined?(CanCan::ModelAdapters::ActiveRecordAdapter) ||
99
+ CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
100
+ end
101
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  # This module is automatically included into all controllers.
3
5
  # It also makes the "can?" and "cannot?" methods available to all views.
@@ -225,7 +227,7 @@ module CanCan
225
227
  cancan_skipper[:authorize][name] = options
226
228
  end
227
229
 
228
- # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call.
230
+ # Add this to a controller to ensure it performs authorization through +authorize+! or +authorize_resource+ call.
229
231
  # If neither of these authorization methods are called,
230
232
  # a CanCan::AuthorizationNotPerformed exception will be raised.
231
233
  # This is normally added to the ApplicationController to ensure all controller actions do authorization.
@@ -260,8 +262,9 @@ module CanCan
260
262
  next if controller.instance_variable_defined?(:@_authorized)
261
263
  next if options[:if] && !controller.send(options[:if])
262
264
  next if options[:unless] && controller.send(options[:unless])
265
+
263
266
  raise AuthorizationNotPerformed,
264
- 'This action failed the check_authorization because it does not authorize_resource. '\
267
+ 'This action failed the check_authorization because it does not authorize_resource. ' \
265
268
  'Add skip_authorization_check to bypass this check.'
266
269
  end
267
270