cancancan 3.6.2 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f110f791b83ea1cc157fae66dd576a3dae94f60b983d654e8be252cb6c4e8939
4
- data.tar.gz: cbf9e6c8e6fadca715eeff37cfce3b289550d538e84a5996b4e95aa00632d7fd
3
+ metadata.gz: a89f93b55a85e54a08313de3a4cb8bad1886b3a1325c39953a8667fd58734be9
4
+ data.tar.gz: fdbf729ba98180067f749415815005bb2362e84f3f04014248b88f7974bf3982
5
5
  SHA512:
6
- metadata.gz: 8102b54914e87e65dbd15b27991e33462d5083d37c6f57aaf09a2dfeaff92826420e27863bd88e17a8c1f18939281ec36ed2abb70e2e012df16661997af5f98a
7
- data.tar.gz: 6b1defbddb0262274e8ad349fb6b267fa45b4024ca790ba3ba93f67d741cc38055e37e485d57f7ff17b59244a5c235d42e956b9049b603d2d8aa28898a4dd768
6
+ metadata.gz: 3ae7721feae9b7dfc2e411878bb600f08cf8edfbe42a01989b6c09ad162443714643898129ad15ca24b5f8d1bf30fb916016eac7baf466671a18a0f292f69f5d
7
+ data.tar.gz: '086c7fdf46ab6caa794c425681557469deffb8038f88fc7aba00e1a361fe8dd5b85caaa18884916e5ce908c7e6efec0f02b3b337a1405159b12d785ea2dc6f5d'
data/cancancan.gemspec CHANGED
@@ -10,7 +10,8 @@ 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
+ s.metadata = { 'changelog_uri' => 'https://github.com/CanCanCommunity/cancancan/blob/develop/CHANGELOG.md',
14
+ 'funding_uri' => 'https://github.com/sponsors/coorasse' }
14
15
  s.summary = 'Simple authorization solution for Rails.'
15
16
  s.description = 'Simple authorization solution for Rails. All permissions are stored in a single location.'
16
17
  s.platform = Gem::Platform::RUBY
@@ -23,7 +24,7 @@ Gem::Specification.new do |s|
23
24
 
24
25
  s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
25
26
  s.add_development_dependency 'bundler', '~> 2.0'
26
- s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
27
+ s.add_development_dependency 'rake'
27
28
  s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
28
29
  s.add_development_dependency 'rubocop', '~> 1.31.1'
29
30
  end
@@ -33,10 +33,12 @@ module CanCan
33
33
  #
34
34
  # This way one can use params[:action] in the controller to determine the permission.
35
35
  def alias_action(*args)
36
- target = args.pop[:to]
37
- validate_target(target)
36
+ target = args.pop[:to]&.to_sym
37
+ actions = args.map(&:to_sym)
38
+ validate_target(target, actions)
38
39
  aliased_actions[target] ||= []
39
- aliased_actions[target] += args
40
+ aliased_actions[target] += actions
41
+ @expanded_actions = nil
40
42
  end
41
43
 
42
44
  # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
@@ -47,6 +49,7 @@ module CanCan
47
49
  # Removes previously aliased actions including the defaults.
48
50
  def clear_aliased_actions
49
51
  @aliased_actions = {}
52
+ @expanded_actions = nil
50
53
  end
51
54
 
52
55
  private
@@ -35,6 +35,9 @@ module CanCan
35
35
  def relevant_rules(action, subject)
36
36
  return [] unless @rules
37
37
 
38
+ # Actions are symbols in the rules, so 'destroy' must also match a `cannot :destroy`.
39
+ action = action.to_sym if action.is_a?(String)
40
+
38
41
  relevant = possible_relevant_rules(subject).select do |rule|
39
42
  rule.expanded_actions = expand_actions(rule.actions)
40
43
  rule.relevant? action, subject
@@ -56,8 +59,32 @@ module CanCan
56
59
  end
57
60
  end
58
61
 
62
+ # Returns nil when no rule matches. A class that uses single table inheritance is checked together with
63
+ # its subclasses, each one with the rules of its own ancestors: the check passes when any of them passes,
64
+ # which is also when `accessible_by` can return records.
65
+ def subject_can?(action, subject, attribute, *extra_args)
66
+ results = sti_classes(subject).map do |klass|
67
+ matching_rule(action, klass, attribute, *extra_args)&.base_behavior
68
+ end
69
+ results.compact.empty? ? nil : results.any?
70
+ end
71
+
72
+ def matching_rule(action, subject, attribute, *extra_args)
73
+ relevant_rules_for_match(action, subject).detect do |rule|
74
+ rule.matches_conditions?(action, subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
75
+ end
76
+ end
77
+
78
+ def sti_classes(subject)
79
+ return [subject] unless defined?(ActiveRecord::Base) && subject.is_a?(Class) && subject < ActiveRecord::Base
80
+
81
+ [subject, *subject.descendants.select { |klass| StiDetector.sti_class?(klass) }]
82
+ end
83
+
59
84
  def relevant_rules_for_match(action, subject)
60
- relevant_rules(action, subject).each do |rule|
85
+ rules = relevant_rules(action, subject)
86
+ rules = rules.reject { |rule| rule.only_for_subclasses_of?(subject) } if subject.is_a?(Class)
87
+ rules.each do |rule|
61
88
  next unless rule.only_raw_sql?
62
89
 
63
90
  raise Error,
@@ -4,34 +4,48 @@ module CanCan
4
4
  module Ability
5
5
  module StrongParameterSupport
6
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'
7
+ def permitted_attributes(action, subject)
8
+ fold_attributes(action, subject) { |rule, set| apply_attributes(rule, subject, set) }
9
+ end
10
+
11
+ # This method returns all attributes which are explicity disallowed
12
+ def disallowed_attributes(action, subject)
13
+ fold_attributes(action, subject) do |rule, set|
14
+ attributes = get_attributes(rule, subject)
15
+ rule.base_behavior ? set.subtract(attributes) : set.merge(attributes)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ # Reversing the relevant rules is important. Normal order means that 'cannot'
9
22
  # rules will come before 'can' rules. However, you can't remove attributes before
10
23
  # they are added. The 'reverse' is so that attributes will be added before the
11
24
  # 'cannot' rules remove them.
12
- def permitted_attributes(action, subject)
25
+ def fold_attributes(action, subject, &block)
13
26
  relevant_rules(action, subject)
14
27
  .reverse
15
28
  .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
29
+ .each_with_object(Set.new, &block)
30
+ .to_a
21
31
  end
22
32
 
23
- private
24
-
25
- def subject_class?(subject)
26
- klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
27
- [Class, Module].include? klass
33
+ def apply_attributes(rule, subject, set)
34
+ if rule.base_behavior
35
+ set.merge(get_attributes(rule, subject))
36
+ elsif rule.attributes.empty?
37
+ set.clear
38
+ else
39
+ set.subtract(rule.attributes)
40
+ end
28
41
  end
29
42
 
30
43
  def get_attributes(rule, subject)
31
- klass = subject_class?(subject) ? subject : subject.class
44
+ subject = subject.values.first if subject.is_a?(Hash)
45
+ klass = subject.is_a?(Module) ? subject : subject.class
32
46
  # empty attributes is an 'all'
33
47
  if rule.attributes.empty? && klass < ActiveRecord::Base
34
- klass.attribute_names.map(&:to_sym) - Array(klass.primary_key)
48
+ klass.attribute_names.map(&:to_sym) - Array(klass.primary_key).map(&:to_sym)
35
49
  else
36
50
  rule.attributes
37
51
  end
@@ -49,8 +49,9 @@ module CanCan
49
49
  #
50
50
  # Any additional arguments will be passed into the "can" block definition. This
51
51
  # can be used to pass more information about the user's request for example.
52
+ # The block is not called when a class is passed instead of an instance.
52
53
  #
53
- # can? :create, Project, request.remote_ip
54
+ # can? :create, @project, request.remote_ip
54
55
  #
55
56
  # can :create, Project do |project, remote_ip|
56
57
  # # ...
@@ -72,12 +73,11 @@ module CanCan
72
73
  #
73
74
  # Also see the RSpec Matchers to aid in testing.
74
75
  def can?(action, subject, attribute = nil, *extra_args)
75
- match = extract_subjects(subject).lazy.map do |a_subject|
76
- relevant_rules_for_match(action, a_subject).detect do |rule|
77
- rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
78
- end
79
- end.reject(&:nil?).first
80
- match ? match.base_behavior : false
76
+ action = action.to_sym if action.is_a?(String)
77
+ results = extract_subjects(subject).lazy.map do |a_subject|
78
+ subject_can?(action, a_subject, attribute, *extra_args)
79
+ end
80
+ results.reject(&:nil?).first || false
81
81
  end
82
82
 
83
83
  # Convenience method which works the same as "can?" but returns the opposite value.
@@ -162,9 +162,11 @@ module CanCan
162
162
  end
163
163
 
164
164
  # User shouldn't specify targets with names of real actions or it will cause Seg fault
165
- def validate_target(target)
165
+ def validate_target(target, actions = [])
166
166
  error_message = "You can't specify target (#{target}) as alias because it is real action name"
167
167
  raise Error, error_message if aliased_actions.values.flatten.include? target
168
+ raise Error, "You can't specify target (#{target}) as alias because it is a reserved action" if target == :manage
169
+ raise Error, "You can't alias the action (#{target}) to itself" if actions.include?(target)
168
170
  end
169
171
 
170
172
  def model_adapter(model_class, action)
@@ -250,6 +252,7 @@ module CanCan
250
252
  add_rule(rule.dup)
251
253
  end
252
254
  @aliased_actions = aliased_actions.merge(ability.aliased_actions)
255
+ @expanded_actions = nil
253
256
  self
254
257
  end
255
258
 
@@ -303,9 +306,9 @@ module CanCan
303
306
  def alternative_subjects(subject)
304
307
  subject = subject.class unless subject.is_a?(Module)
305
308
  if subject.respond_to?(:subclasses) && defined?(ActiveRecord::Base) && subject < ActiveRecord::Base
306
- [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s]
309
+ [:all, *(subject.ancestors + subject.descendants)]
307
310
  else
308
- [:all, *subject.ancestors, subject.class.to_s]
311
+ [:all, *subject.ancestors]
309
312
  end
310
313
  end
311
314
  end
@@ -16,7 +16,7 @@ class SubjectClassMatcher
16
16
  if has_subclasses
17
17
  return matches unless StiDetector.sti_class?(sub)
18
18
 
19
- matches || subject.subclasses.include?(sub)
19
+ matches || subject.descendants.include?(sub)
20
20
  else
21
21
  matches
22
22
  end
@@ -41,10 +41,11 @@ module CanCan
41
41
 
42
42
  adapter = model_adapter(parent)
43
43
 
44
- parent_condition_name = adapter.parent_condition_name(parent, child)
44
+ # Like for a class, a "cannot" with conditions on the child does not stop the check.
45
+ return false unless @base_behavior || adapter.conditions_only_on_parent?(parent, child, @conditions)
45
46
 
46
- matches_base_parent_conditions = matches_conditions_hash?(parent,
47
- @conditions[parent_condition_name] || {})
47
+ parent_conditions = @conditions[adapter.parent_condition_name(parent, child)] || {}
48
+ matches_base_parent_conditions = matches_conditions_hash?(parent, parent_conditions)
48
49
 
49
50
  matches_base_parent_conditions &&
50
51
  (!adapter.override_nested_subject_conditions_matching?(parent, child, @conditions) ||
@@ -88,18 +89,26 @@ module CanCan
88
89
  end
89
90
 
90
91
  def condition_match?(attribute, value)
92
+ return relation_match?(attribute, value) if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Relation)
93
+
91
94
  case value
92
- when Hash
93
- hash_condition_match?(attribute, value)
94
- when Range
95
- value.cover?(attribute)
96
- when Enumerable
97
- value.include?(attribute)
98
- else
99
- attribute == value
95
+ when Hash then hash_condition_match?(attribute, value)
96
+ when Range then value.cover?(attribute)
97
+ when Enumerable then value.any? { |element| value_match?(attribute, element) }
98
+ else value_match?(attribute, value)
100
99
  end
101
100
  end
102
101
 
102
+ # Like Active Record in the SQL conditions, a symbol matches the string with the same name.
103
+ def value_match?(attribute, value)
104
+ attribute == value || (value.is_a?(Symbol) && attribute == value.to_s)
105
+ end
106
+
107
+ # Like Active Record with a relation as condition value: the selected column, or the primary key, is matched.
108
+ def relation_match?(attribute, relation)
109
+ relation.where((relation.select_values.first || relation.primary_key) => attribute).exists?
110
+ end
111
+
103
112
  def hash_condition_match?(attribute, value)
104
113
  if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
105
114
  array_like_matches_condition_hash?(attribute, value)
data/lib/cancan/config.rb CHANGED
@@ -47,10 +47,14 @@ module CanCan
47
47
  # ensures records are unique using `distinct`. Note that
48
48
  # `distinct` is not reliable in some cases. See
49
49
  # https://github.com/CanCanCommunity/cancancan/pull/605
50
+ # - :joined_alias_exists_subquery, :joined_alias_each_rule_as_exists_subquery -
51
+ # Build an EXISTS subquery on an aliased table.
52
+ # - a subclass of CanCan::ModelAdapters::Strategies::Base - Your own
53
+ # strategy. Implement `execute!` and return the relation.
50
54
  def self.accessible_by_strategy
51
- return @accessible_by_strategy if @accessible_by_strategy
52
-
53
- @accessible_by_strategy = default_accessible_by_strategy
55
+ Thread.current[:cancan_accessible_by_strategy_override] ||
56
+ @accessible_by_strategy ||
57
+ default_accessible_by_strategy
54
58
  end
55
59
 
56
60
  def self.default_accessible_by_strategy
@@ -74,24 +78,34 @@ module CanCan
74
78
  @accessible_by_strategy = value
75
79
  end
76
80
 
81
+ # Overrides the strategy for the duration of the block. The override is local to the current
82
+ # thread and fiber, so it never leaks into concurrent requests.
77
83
  def self.with_accessible_by_strategy(value)
78
84
  return yield if value == accessible_by_strategy
79
85
 
80
86
  validate_accessible_by_strategy!(value)
81
87
 
82
88
  begin
83
- strategy_was = accessible_by_strategy
84
- @accessible_by_strategy = value
89
+ override_was = Thread.current[:cancan_accessible_by_strategy_override]
90
+ Thread.current[:cancan_accessible_by_strategy_override] = value
85
91
  yield
86
92
  ensure
87
- @accessible_by_strategy = strategy_was
93
+ Thread.current[:cancan_accessible_by_strategy_override] = override_was
88
94
  end
89
95
  end
90
96
 
91
97
  def self.validate_accessible_by_strategy!(value)
92
98
  return if valid_accessible_by_strategies.include?(value)
99
+ return if custom_accessible_by_strategy?(value)
100
+
101
+ raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')} " \
102
+ 'or a subclass of CanCan::ModelAdapters::Strategies::Base'
103
+ end
93
104
 
94
- raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}"
105
+ def self.custom_accessible_by_strategy?(value)
106
+ value.is_a?(Class) &&
107
+ defined?(CanCan::ModelAdapters::Strategies::Base) &&
108
+ value < CanCan::ModelAdapters::Strategies::Base
95
109
  end
96
110
 
97
111
  def self.does_not_support_subquery_strategy?
@@ -74,7 +74,8 @@ module CanCan
74
74
  #
75
75
  # [:+through_association+]
76
76
  # The name of the association to fetch the child records through the parent resource.
77
- # This is normally not needed because it defaults to the pluralized resource name.
77
+ # This is normally not needed because it defaults to the pluralized resource name,
78
+ # or to the singular resource name when +:singleton+ is used.
78
79
  #
79
80
  # [:+shallow+]
80
81
  # Pass +true+ to allow this resource to be loaded directly when parent is +nil+. Defaults to +false+.
@@ -302,7 +303,7 @@ module CanCan
302
303
  # its own. Assign a copy, so a skip in a subclass does not change the superclass or sibling classes.
303
304
  def add_cancan_skip(behavior, name, options)
304
305
  skipper = cancan_skipper
305
- self._cancan_skipper = skipper.merge(behavior => skipper[behavior].merge(name => options))
306
+ self._cancan_skipper = skipper.merge(behavior => skipper[behavior].merge(name&.to_sym => options))
306
307
  end
307
308
  end
308
309