cancancan 3.4.0 → 3.6.1

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: fc128fc4996aed8edaee7dcce7178ec47d06c63591bf03c7cb98dd3deec5c213
4
- data.tar.gz: 4e3ee983874f8fbeb3f566c6e12aaba275990581e77938b73d0349ec1e867f6e
3
+ metadata.gz: e0d4a5b11aba155764cf54465d5d8bf88872fed61f4a33736d45a531c619ad6e
4
+ data.tar.gz: 12b3b41403f3fdaba3fdb97c30ce17e319d1bfdef05fcb7700d41ba754c75a55
5
5
  SHA512:
6
- metadata.gz: 4511dd58be6c2a2ce4bc28b382ece40ad367a1cf72f56d521fe2f38ca038eb6cb11a249a1d029346038cfec0e1828acadddf8ff9be14d6d59e5228b165811d49
7
- data.tar.gz: 8d24834c3362f708a9979079eee089537975aefa25144a65eea006c137a75297f1341b6df24877c0c517b0e1c33e935a7c4d82bbedcca84e00100d0806812ff5
6
+ metadata.gz: beb6989dbb2554678fa17626b6138aab396419b6d01ed8ce3ee3781030086da48985837a01bef50ab111fb47273454df8c7e3d6ced11b10a036ffe2e494feb15
7
+ data.tar.gz: 405b0e6eb4ab73f04964651ab27e106701c014cd706f74012e5f30aada5796de6541f812593b57e058031af0991890884701136229f0405900e3bc99ce3747ee
data/cancancan.gemspec CHANGED
@@ -25,5 +25,5 @@ Gem::Specification.new do |s|
25
25
  s.add_development_dependency 'bundler', '~> 2.0'
26
26
  s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
27
27
  s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
28
- s.add_development_dependency 'rubocop', '~> 1.26'
28
+ s.add_development_dependency 'rubocop', '~> 1.31.1'
29
29
  end
@@ -61,8 +61,8 @@ module CanCan
61
61
  next unless rule.only_raw_sql?
62
62
 
63
63
  raise Error,
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}"
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}"
66
66
  end
67
67
  end
68
68
 
@@ -72,7 +72,7 @@ module CanCan
72
72
  rule.base_behavior == false && rule.attributes.present?
73
73
  end
74
74
  if rules.any?(&:only_block?)
75
- raise Error, "The accessible_by call cannot be used with a block 'can' definition."\
75
+ raise Error, "The accessible_by call cannot be used with a block 'can' definition." \
76
76
  "The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
77
77
  end
78
78
  rules
@@ -31,7 +31,7 @@ module CanCan
31
31
  klass = subject_class?(subject) ? subject : subject.class
32
32
  # empty attributes is an 'all'
33
33
  if rule.attributes.empty? && klass < ActiveRecord::Base
34
- klass.column_names.map(&:to_sym) - Array(klass.primary_key)
34
+ klass.attribute_names.map(&:to_sym) - Array(klass.primary_key)
35
35
  else
36
36
  rule.attributes
37
37
  end
@@ -18,10 +18,14 @@ module CanCan
18
18
  [Class, Module].include? klass
19
19
  end
20
20
 
21
- def matches_block_conditions(subject, *extra_args)
21
+ def matches_block_conditions(subject, attribute, *extra_args)
22
22
  return @base_behavior if subject_class?(subject)
23
23
 
24
- @block.call(subject, *extra_args.compact)
24
+ if attribute
25
+ @block.call(subject, attribute, *extra_args)
26
+ else
27
+ @block.call(subject, *extra_args)
28
+ end
25
29
  end
26
30
 
27
31
  def matches_non_block_conditions(subject)
@@ -35,11 +39,13 @@ module CanCan
35
39
  def nested_subject_matches_conditions?(subject_hash)
36
40
  parent, child = subject_hash.first
37
41
 
38
- matches_base_parent_conditions = matches_conditions_hash?(parent,
39
- @conditions[parent.class.name.downcase.to_sym] || {})
40
-
41
42
  adapter = model_adapter(parent)
42
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
+
43
49
  matches_base_parent_conditions &&
44
50
  (!adapter.override_nested_subject_conditions_matching?(parent, child, @conditions) ||
45
51
  adapter.nested_subject_matches_conditions?(parent, child, @conditions))
@@ -63,16 +69,15 @@ module CanCan
63
69
 
64
70
  def matches_all_conditions?(adapter, subject, conditions)
65
71
  if conditions.is_a?(Hash)
66
- matches_hash_conditions(adapter, subject, conditions)
72
+ matches_hash_conditions?(adapter, subject, conditions)
67
73
  elsif conditions.respond_to?(:include?)
68
74
  conditions.include?(subject)
69
75
  else
70
- puts "does #{subject} match #{conditions}?"
71
76
  subject == conditions
72
77
  end
73
78
  end
74
79
 
75
- def matches_hash_conditions(adapter, subject, conditions)
80
+ def matches_hash_conditions?(adapter, subject, conditions)
76
81
  conditions.all? do |name, value|
77
82
  if adapter.override_condition_matching?(subject, name, value)
78
83
  adapter.matches_condition?(subject, name, value)
@@ -97,12 +102,29 @@ module CanCan
97
102
 
98
103
  def hash_condition_match?(attribute, value)
99
104
  if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
100
- attribute.to_a.any? { |element| matches_conditions_hash?(element, value) }
105
+ array_like_matches_condition_hash?(attribute, value)
101
106
  else
102
107
  attribute && matches_conditions_hash?(attribute, value)
103
108
  end
104
109
  end
105
110
 
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
+
106
128
  def call_block_with_all(action, subject, *extra_args)
107
129
  if subject.class == Class
108
130
  @block.call(action, subject, nil, *extra_args)
data/lib/cancan/config.rb CHANGED
@@ -11,6 +11,29 @@ module CanCan
11
11
  strategies
12
12
  end
13
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
+
14
37
  # Determines how CanCan should build queries when calling accessible_by,
15
38
  # if the query will contain a join. The default strategy is `:subquery`.
16
39
  #
@@ -171,6 +171,11 @@ module CanCan
171
171
  # [:+instance_name+]
172
172
  # The name of the instance variable for this resource.
173
173
  #
174
+ # [:+id_param+]
175
+ # Find using a param key other than :id. For example:
176
+ #
177
+ # load_resource :id_param => :url # will use find(params[:url])
178
+ #
174
179
  # [:+through+]
175
180
  # Authorize conditions on this parent resource when instance isn't available.
176
181
  #
@@ -264,7 +269,7 @@ module CanCan
264
269
  next if options[:unless] && controller.send(options[:unless])
265
270
 
266
271
  raise AuthorizationNotPerformed,
267
- 'This action failed the check_authorization because it does not authorize_resource. '\
272
+ 'This action failed the check_authorization because it does not authorize_resource. ' \
268
273
  'Add skip_authorization_check to bypass this check.'
269
274
  end
270
275
 
@@ -35,6 +35,11 @@ module CanCan
35
35
  raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
36
36
  end
37
37
 
38
+ # Override if parent condition could be under a different key in conditions
39
+ def self.parent_condition_name(parent, _child)
40
+ parent.class.name.downcase.to_sym
41
+ end
42
+
38
43
  # Used above override_conditions_hash_matching to determine if this model adapter will override the
39
44
  # matching behavior for nested subject.
40
45
  # If this returns true then nested_subject_matches_conditions? will be called.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/AbcSize
4
+ # rubocop:disable Metrics/CyclomaticComplexity
5
+ # rubocop:disable Metrics/PerceivedComplexity
3
6
  module CanCan
4
7
  module ModelAdapters
5
8
  class ActiveRecordAdapter < AbstractAdapter
@@ -15,7 +18,11 @@ module CanCan
15
18
 
16
19
  def initialize(model_class, rules)
17
20
  super
18
- @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
21
+ @compressed_rules = if CanCan.rules_compressor_enabled
22
+ RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
23
+ else
24
+ @rules
25
+ end
19
26
  StiNormalizer.normalize(@compressed_rules)
20
27
  ConditionsNormalizer.normalize(model_class, @compressed_rules)
21
28
  end
@@ -38,11 +45,53 @@ module CanCan
38
45
 
39
46
  def parent_child_conditions(parent, child, all_conditions)
40
47
  child_class = child.is_a?(Class) ? child : child.class
48
+ parent_class = parent.is_a?(Class) ? parent : parent.class
49
+
41
50
  foreign_key = child_class.reflect_on_all_associations(:belongs_to).find do |association|
42
- association.klass == parent.class
43
- end&.foreign_key&.to_sym
51
+ # Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
52
+ !association.polymorphic? && association.klass == parent.class
53
+ end&.foreign_key&.to_sym
54
+
55
+ # Search again in case of polymorphic associations, this time matching on the :has_many side
56
+ # via the :as option, as well as klass
57
+ foreign_key ||= parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
58
+ matching_parent_child_polymorphic_association(has_many_assoc, child_class)
59
+ end&.foreign_key&.to_sym
60
+
44
61
  foreign_key.nil? ? nil : all_conditions[foreign_key]
45
62
  end
63
+
64
+ def matching_parent_child_polymorphic_association(parent_assoc, child_class)
65
+ return nil unless parent_assoc.klass == child_class
66
+ return nil if parent_assoc&.options[:as].nil?
67
+
68
+ child_class.reflect_on_all_associations(:belongs_to).find do |child_assoc|
69
+ # Only match this way for polymorphic associations
70
+ child_assoc.polymorphic? && child_assoc.name == parent_assoc.options[:as]
71
+ end
72
+ end
73
+
74
+ def child_association_to_parent(parent, child)
75
+ child_class = child.is_a?(Class) ? child : child.class
76
+ parent_class = parent.is_a?(Class) ? parent : parent.class
77
+
78
+ association = child_class.reflect_on_all_associations(:belongs_to).find do |belongs_to_assoc|
79
+ # Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
80
+ !belongs_to_assoc.polymorphic? && belongs_to_assoc.klass == parent.class
81
+ end
82
+
83
+ return association if association
84
+
85
+ parent_class.reflect_on_all_associations(:has_many).each do |has_many_assoc|
86
+ association ||= matching_parent_child_polymorphic_association(has_many_assoc, child_class)
87
+ end
88
+
89
+ association
90
+ end
91
+
92
+ def parent_condition_name(parent, child)
93
+ child_association_to_parent(parent, child)&.name || parent.class.name.downcase.to_sym
94
+ end
46
95
  end
47
96
 
48
97
  # Returns conditions intended to be used inside a database query. Normally you will not call this
@@ -133,7 +182,7 @@ module CanCan
133
182
  def raise_override_scope_error
134
183
  rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
135
184
  raise Error,
136
- 'Unable to merge an Active Record scope with other conditions. '\
185
+ 'Unable to merge an Active Record scope with other conditions. ' \
137
186
  "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
138
187
  end
139
188
 
@@ -171,6 +220,9 @@ module CanCan
171
220
  end
172
221
  end
173
222
  end
223
+ # rubocop:enable Metrics/PerceivedComplexity
224
+ # rubocop:enable Metrics/CyclomaticComplexity
225
+ # rubocop:enable Metrics/AbcSize
174
226
 
175
227
  ActiveSupport.on_load(:active_record) do
176
228
  send :include, CanCan::ModelAdditions
@@ -30,8 +30,16 @@ module CanCan
30
30
 
31
31
  # create a new rule for the subclasses that links on the inheritance_column
32
32
  def build_rule_for_subclass(rule, subject)
33
+ sti_conditions = { subject.inheritance_column => subject.sti_name }
34
+ new_rule_conditions =
35
+ if rule.with_scope?
36
+ rule.conditions.where(sti_conditions)
37
+ else
38
+ rule.conditions.merge(sti_conditions)
39
+ end
40
+
33
41
  CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass,
34
- rule.conditions.merge(subject.inheritance_column => subject.sti_name), rule.block)
42
+ new_rule_conditions, rule.block)
35
43
  end
36
44
  end
37
45
  end
data/lib/cancan/rule.rb CHANGED
@@ -123,7 +123,7 @@ module CanCan
123
123
  def condition_and_block_check(conditions, block, action, subject)
124
124
  return unless conditions.is_a?(Hash) && block
125
125
 
126
- raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\
126
+ raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. ' \
127
127
  "Check \":#{action} #{subject}\" ability."
128
128
  end
129
129
 
@@ -11,6 +11,7 @@ module CanCan
11
11
  end
12
12
 
13
13
  def compress(array)
14
+ array = simplify(array)
14
15
  idx = array.rindex(&:catch_all?)
15
16
  return array unless idx
16
17
 
@@ -19,5 +20,22 @@ module CanCan
19
20
  .drop_while { |n| n.base_behavior == value.base_behavior }
20
21
  .tap { |a| a.unshift(value) unless value.cannot_catch_all? }
21
22
  end
23
+
24
+ # If we have A OR (!A AND anything ), then we can simplify to A OR anything
25
+ # If we have A OR (A OR anything ), then we can simplify to A OR anything
26
+ # If we have !A AND (A OR something), then we can simplify it to !A AND something
27
+ # If we have !A AND (!A AND something), then we can simplify it to !A AND something
28
+ #
29
+ # So as soon as we see a condition that is the same as the previous one,
30
+ # we can skip it, no matter of the base_behavior
31
+ def simplify(rules)
32
+ seen = Set.new
33
+ rules.reverse_each.filter_map do |rule|
34
+ next if seen.include?(rule.conditions)
35
+
36
+ seen.add(rule.conditions)
37
+ rule
38
+ end.reverse
39
+ end
22
40
  end
23
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanCan
4
- VERSION = '3.4.0'.freeze
4
+ VERSION = '3.6.1'.freeze
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cancancan
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi (Renuo AG)
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2022-06-23 00:00:00.000000000 Z
14
+ date: 2024-05-28 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: appraisal
@@ -93,14 +93,14 @@ dependencies:
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '1.26'
96
+ version: 1.31.1
97
97
  type: :development
98
98
  prerelease: false
99
99
  version_requirements: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '1.26'
103
+ version: 1.31.1
104
104
  description: Simple authorization solution for Rails. All permissions are stored in
105
105
  a single location.
106
106
  email: alessandro.rodi@renuo.ch