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.
@@ -1,74 +1,130 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # this class is responsible of converting the hash of conditions
4
- # in "where conditions" to generate the sql query
5
- # it consists of a names_cache that helps calculating the next name given to the association
6
- # it tries to reflect the behavior of ActiveRecord when generating aliases for tables.
4
+ # in "where conditions" to generate the sql query.
5
+ # Every association is given the table alias that ActiveRecord gives it when joining the associations,
6
+ # so it walks the joins in the same order and registers every joined table, through tables included.
7
7
  module CanCan
8
8
  module ModelAdapters
9
9
  class ConditionsExtractor
10
- def initialize(model_class)
11
- @names_cache = { model_class.table_name => [] }.with_indifferent_access
10
+ # Pass the associations given to `joins` and then to `left_joins`, as they are joined by ActiveRecord.
11
+ def initialize(model_class, *joins)
12
12
  @root_model_class = model_class
13
+ @aliases = Hash.new(0)
14
+ @aliases[model_class.table_name] = 1
15
+ @table_aliases = {}
16
+ @joined_tables = {}
17
+ @model_classes = {}
18
+ joins.each { |associations| register_joins(model_class, joins_tree(associations), []) }
13
19
  end
14
20
 
15
- def tableize_conditions(conditions, model_class = @root_model_class, path_to_key = 0)
21
+ def tableize_conditions(conditions, model_class = @root_model_class, path = [])
16
22
  return conditions unless conditions.is_a? Hash
17
23
 
18
24
  conditions.each_with_object({}) do |(key, value), result_hash|
19
25
  if value.is_a? Hash
20
- result_hash.merge!(calculate_result_hash(key, model_class, path_to_key, result_hash, value))
26
+ tableize_association(model_class, path + [key.to_sym], value, result_hash)
21
27
  else
22
28
  result_hash[key] = value
23
29
  end
24
- result_hash
25
30
  end
26
31
  end
27
32
 
33
+ # The model class of a table alias returned by tableize_conditions.
34
+ def model_class_for(table_alias)
35
+ @model_classes[table_alias.to_s]
36
+ end
37
+
28
38
  private
29
39
 
30
- def calculate_result_hash(key, model_class, path_to_key, result_hash, value)
31
- reflection = model_class.reflect_on_association(key)
32
- nested_resulted = calculate_nested(model_class, result_hash, key, value.dup, path_to_key)
33
- association_class = reflection.klass.name.constantize
34
- tableize_conditions(nested_resulted, association_class, "#{path_to_key}_#{key}")
40
+ def tableize_association(model_class, path, conditions, result_hash)
41
+ reflection = model_class.reflect_on_association(path.last)
42
+ table_alias = @table_aliases[path] || register_association(model_class, reflection, path)
43
+ nested, values = conditions.partition { |_key, value| value.is_a? Hash }.map(&:to_h)
44
+ result_hash[table_alias.to_sym] = values unless values.empty?
45
+ result_hash.merge!(tableize_conditions(nested, reflection.klass, path))
35
46
  end
36
47
 
37
- def calculate_nested(model_class, result_hash, relation_name, value, path_to_key)
38
- value.each_with_object({}) do |(k, v), nested|
39
- if v.is_a? Hash
40
- value.delete(k)
41
- nested[k] = v
42
- else
43
- table_alias = generate_table_alias(model_class, relation_name, path_to_key)
44
- result_hash[table_alias] = value
45
- end
46
- nested
48
+ def joins_tree(joins)
49
+ case joins
50
+ when Hash then joins.to_h { |name, nested| [name.to_sym, joins_tree(nested)] }
51
+ when Array then joins.each_with_object({}) { |nested, tree| tree.deep_merge!(joins_tree(nested)) }
52
+ when nil then {}
53
+ else { joins.to_sym => {} }
47
54
  end
48
55
  end
49
56
 
50
- def generate_table_alias(model_class, relation_name, path_to_key)
51
- table_alias = model_class.reflect_on_association(relation_name).table_name.to_sym
57
+ # Mirrors ActiveRecord::Associations::JoinDependency#walk: the associations already joined come first.
58
+ def register_joins(model_class, tree, path)
59
+ joined, missing = tree.partition { |name, _nested| @table_aliases.key?(path + [name]) }
60
+ (joined + missing).each do |name, nested|
61
+ reflection = model_class.reflect_on_association(name)
62
+ next if reflection.nil? || reflection.polymorphic?
63
+
64
+ register_association(model_class, reflection, path + [name]) unless @table_aliases.key?(path + [name])
65
+ register_joins(reflection.klass, nested, path + [name])
66
+ end
67
+ end
52
68
 
53
- if already_used?(table_alias, relation_name, path_to_key)
54
- table_alias = "#{relation_name.to_s.pluralize}_#{model_class.table_name}".to_sym
69
+ def register_association(parent_class, reflection, path)
70
+ chain = reflection.chain
71
+ table_alias = nil
72
+ chain.each_index do |index|
73
+ table, reused = join_table(parent_class, chain, index)
74
+ table_alias ||= table
75
+ break if reused
76
+ end
77
+ @model_classes[table_alias] = reflection.klass
78
+ @table_aliases[path] = table_alias
79
+ end
55
80
 
56
- index = 1
57
- while already_used?(table_alias, relation_name, path_to_key)
58
- table_alias = "#{table_alias}_#{index += 1}".to_sym
59
- end
81
+ # Mirrors ActiveRecord::Associations::JoinDependency#make_constraints
82
+ def join_table(parent_class, chain, index)
83
+ root = index.zero?
84
+ remaining_chain = chain[index..]
85
+ table, terminated = @joined_tables[remaining_chain]
86
+ return reuse_joined_table(remaining_chain, table, root) if reuse_joined_table?(table, root, terminated)
87
+
88
+ table = aliased_table_for(chain[index].klass.table_name) do
89
+ name = chain[index].alias_candidate(parent_class.table_name)
90
+ root ? name : "#{name}_join"
91
+ end
92
+ @joined_tables[remaining_chain] ||= [table, root]
93
+ [table, false]
94
+ end
95
+
96
+ def reuse_joined_table(remaining_chain, table, root)
97
+ @joined_tables[remaining_chain] = [table, root] if root
98
+ [table, true]
99
+ end
100
+
101
+ # ActiveRecord reuses the through tables of the associations already joined since version 6.1
102
+ def reuse_joined_table?(table, root, terminated)
103
+ table && (!root || !terminated) && ActiveRecordAdapter.version_greater_or_equal?('6.1.0')
104
+ end
105
+
106
+ # Mirrors ActiveRecord::Associations::AliasTracker#aliased_table_for
107
+ def aliased_table_for(table_name)
108
+ if @aliases[table_name].zero?
109
+ @aliases[table_name] = 1
110
+ return table_name
60
111
  end
61
- add_to_cache(table_alias, relation_name, path_to_key)
112
+
113
+ aliased_name = yield[0...table_alias_length].tr('.', '_')
114
+ count = @aliases[aliased_name] += 1
115
+ count > 1 ? "#{aliased_name[0, table_alias_length - 2]}_#{count}" : aliased_name
62
116
  end
63
117
 
64
- def already_used?(table_alias, relation_name, path_to_key)
65
- @names_cache[table_alias].try(:exclude?, "#{path_to_key}_#{relation_name}")
118
+ def table_alias_length
119
+ @table_alias_length ||= connection.table_alias_length
66
120
  end
67
121
 
68
- def add_to_cache(table_alias, relation_name, path_to_key)
69
- @names_cache[table_alias] ||= []
70
- @names_cache[table_alias] << "#{path_to_key}_#{relation_name}"
71
- table_alias
122
+ def connection
123
+ if @root_model_class.respond_to?(:lease_connection)
124
+ @root_model_class.lease_connection
125
+ else
126
+ @root_model_class.connection
127
+ end
72
128
  end
73
129
  end
74
130
  end
@@ -41,7 +41,15 @@ module CanCan
41
41
  end
42
42
 
43
43
  def normalizable_association?(reflection)
44
- reflection.options[:through].present? && !reflection.options[:source_type].present?
44
+ reflection.options[:through].present? &&
45
+ !reflection.options[:source_type].present? &&
46
+ !joinable_with_scope?(reflection)
47
+ end
48
+
49
+ # A through association with its own scope is not exploded, because its parts do not keep
50
+ # that scope. Active Record joins it with the scope, unless the scope takes the record as argument.
51
+ def joinable_with_scope?(reflection)
52
+ reflection.scope.present? && reflection.scope.arity.zero?
45
53
  end
46
54
  end
47
55
  end
@@ -7,25 +7,18 @@ module CanCan
7
7
  class StiNormalizer
8
8
  class << self
9
9
  def normalize(rules)
10
- rules_cache = []
11
10
  return unless defined?(ActiveRecord::Base)
12
11
 
13
- rules.delete_if do |rule|
14
- subjects = rule.subjects.select do |subject|
15
- update_rule(subject, rule, rules_cache)
16
- end
17
- subjects.length == rule.subjects.length
18
- end
19
- rules_cache.each { |rule| rules.push(rule) }
12
+ rules.replace(rules.flat_map { |rule| normalized_rules(rule) })
20
13
  end
21
14
 
22
15
  private
23
16
 
24
- def update_rule(subject, rule, rules_cache)
25
- return false unless StiDetector.sti_class?(subject)
26
-
27
- rules_cache.push(build_rule_for_subclass(rule, subject))
28
- true
17
+ # The rules for the subclasses take the place of the original rule, so their precedence is kept.
18
+ def normalized_rules(rule)
19
+ sti_subjects = rule.subjects.select { |subject| StiDetector.sti_class?(subject) }
20
+ subclass_rules = sti_subjects.map { |subject| build_rule_for_subclass(rule, subject) }
21
+ sti_subjects.length == rule.subjects.length ? subclass_rules : [rule, *subclass_rules]
29
22
  end
30
23
 
31
24
  # create a new rule for the subclasses that links on the inheritance_column
@@ -25,9 +25,9 @@ module CanCan
25
25
  end
26
26
 
27
27
  def sub_query_for_rule(rule)
28
- conditions_extractor = ConditionsExtractor.new(model_class)
29
- rule_where_conditions = extract_multiple_conditions(conditions_extractor, [rule])
30
28
  joins_hash, left_joins_hash = extract_joins_from_rule(rule)
29
+ conditions_extractor = ConditionsExtractor.new(model_class, joins_hash, left_joins_hash)
30
+ rule_where_conditions = extract_multiple_conditions(conditions_extractor, [rule])
31
31
  sub_query_for_rules_and_join_hashes(rule_where_conditions, joins_hash, left_joins_hash)
32
32
  end
33
33
 
@@ -8,6 +8,10 @@ module CanCan
8
8
  @match_all || (matches_action?(action) && matches_subject?(subject))
9
9
  end
10
10
 
11
+ def only_for_subclasses_of?(klass)
12
+ !@subjects.empty? && @subjects.all? { |subject| subject.is_a?(Class) && subject < klass }
13
+ end
14
+
11
15
  private
12
16
 
13
17
  def matches_action?(action)
data/lib/cancan/rule.rb CHANGED
@@ -28,7 +28,7 @@ module CanCan
28
28
  raise Error, "Subject is required for #{action}" if action && subject.nil?
29
29
 
30
30
  @base_behavior = base_behavior
31
- @actions = wrap(action)
31
+ @actions = wrap(action).map(&:to_sym)
32
32
  @subjects = wrap(subject)
33
33
  @attributes = wrap(attributes)
34
34
  @conditions = extra_args || {}
@@ -58,7 +58,8 @@ module CanCan
58
58
 
59
59
  def catch_all?
60
60
  (with_scope? && @conditions.where_values_hash.empty?) ||
61
- (!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions))
61
+ ((@subjects.all? { |subject| !StiDetector.sti_class?(subject) || subject.base_class == subject }) &&
62
+ (!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions)))
62
63
  end
63
64
 
64
65
  def only_block?
@@ -31,9 +31,10 @@ module CanCan
31
31
  def simplify(rules)
32
32
  seen = Set.new
33
33
  rules.reverse_each.filter_map do |rule|
34
- next if seen.include?(rule.conditions)
34
+ subjects_and_conditions = [rule.subjects, rule.conditions]
35
+ next if seen.include?(subjects_and_conditions)
35
36
 
36
- seen.add(rule.conditions)
37
+ seen.add(subjects_and_conditions)
37
38
  rule
38
39
  end.reverse
39
40
  end
@@ -4,7 +4,7 @@ class StiDetector
4
4
  def self.sti_class?(subject)
5
5
  return false unless defined?(ActiveRecord::Base)
6
6
  return false unless subject.respond_to?(:descends_from_active_record?)
7
- return false if subject == :all || subject.descends_from_active_record?
7
+ return false if subject.descends_from_active_record?
8
8
  return false unless subject < ActiveRecord::Base
9
9
 
10
10
  true
@@ -3,6 +3,7 @@
3
3
  module CanCan
4
4
  module UnauthorizedMessageResolver
5
5
  def unauthorized_message(action, subject)
6
+ action = action.to_sym if action.is_a?(String)
6
7
  subject = subject.values.last if subject.is_a?(Hash)
7
8
  keys = unauthorized_message_keys(action, subject)
8
9
  variables = {}
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanCan
4
- VERSION = '3.6.2'.freeze
4
+ VERSION = '3.7.0'.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.6.2
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi (Renuo AG)
@@ -50,22 +50,16 @@ dependencies:
50
50
  name: rake
51
51
  requirement: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - "~>"
54
- - !ruby/object:Gem::Version
55
- version: '10.1'
56
53
  - - ">="
57
54
  - !ruby/object:Gem::Version
58
- version: 10.1.1
55
+ version: '0'
59
56
  type: :development
60
57
  prerelease: false
61
58
  version_requirements: !ruby/object:Gem::Requirement
62
59
  requirements:
63
- - - "~>"
64
- - !ruby/object:Gem::Version
65
- version: '10.1'
66
60
  - - ">="
67
61
  - !ruby/object:Gem::Version
68
- version: 10.1.1
62
+ version: '0'
69
63
  - !ruby/object:Gem::Dependency
70
64
  name: rspec
71
65
  requirement: !ruby/object:Gem::Requirement
@@ -119,11 +113,6 @@ files:
119
113
  - lib/cancan/config.rb
120
114
  - lib/cancan/controller_additions.rb
121
115
  - lib/cancan/controller_resource.rb
122
- - lib/cancan/controller_resource_builder.rb
123
- - lib/cancan/controller_resource_finder.rb
124
- - lib/cancan/controller_resource_loader.rb
125
- - lib/cancan/controller_resource_name_finder.rb
126
- - lib/cancan/controller_resource_sanitizer.rb
127
116
  - lib/cancan/exceptions.rb
128
117
  - lib/cancan/matchers.rb
129
118
  - lib/cancan/model_adapters/abstract_adapter.rb
@@ -155,6 +144,7 @@ homepage: https://github.com/CanCanCommunity/cancancan
155
144
  licenses:
156
145
  - MIT
157
146
  metadata:
147
+ changelog_uri: https://github.com/CanCanCommunity/cancancan/blob/develop/CHANGELOG.md
158
148
  funding_uri: https://github.com/sponsors/coorasse
159
149
  rdoc_options: []
160
150
  require_paths:
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CanCan
4
- module ControllerResourceBuilder
5
- protected
6
-
7
- def build_resource
8
- resource = resource_base.new(resource_params || {})
9
- assign_attributes(resource)
10
- end
11
-
12
- def assign_attributes(resource)
13
- resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
14
- initial_attributes.each do |attr_name, value|
15
- resource.send("#{attr_name}=", value)
16
- end
17
- resource
18
- end
19
-
20
- def initial_attributes
21
- current_ability.attributes_for(@params[:action].to_sym, resource_class).delete_if do |key, _value|
22
- resource_params && resource_params.include?(key)
23
- end
24
- end
25
- end
26
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CanCan
4
- module ControllerResourceFinder
5
- protected
6
-
7
- def find_resource
8
- if @options[:singleton] && parent_resource.respond_to?(name)
9
- parent_resource.send(name)
10
- elsif @options[:find_by]
11
- find_resource_using_find_by
12
- else
13
- adapter.find(resource_base, id_param)
14
- end
15
- end
16
-
17
- def find_resource_using_find_by
18
- find_by_dynamic_finder || find_by_find_by_finder || resource_base.send(@options[:find_by], id_param)
19
- end
20
-
21
- def find_by_dynamic_finder
22
- method_name = "find_by_#{@options[:find_by]}!"
23
- resource_base.send(method_name, id_param) if resource_base.respond_to? method_name
24
- end
25
-
26
- def find_by_find_by_finder
27
- resource_base.find_by(@options[:find_by].to_sym => id_param) if resource_base.respond_to? :find_by
28
- end
29
-
30
- def id_param
31
- @params[id_param_key].to_s if @params[id_param_key].present?
32
- end
33
-
34
- def id_param_key
35
- if @options[:id_param]
36
- @options[:id_param]
37
- else
38
- parent? ? :"#{name}_id" : :id
39
- end
40
- end
41
- end
42
- end
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'controller_resource_finder.rb'
4
- require_relative 'controller_resource_name_finder.rb'
5
- require_relative 'controller_resource_builder.rb'
6
- require_relative 'controller_resource_sanitizer.rb'
7
- module CanCan
8
- module ControllerResourceLoader
9
- include CanCan::ControllerResourceNameFinder
10
- include CanCan::ControllerResourceFinder
11
- include CanCan::ControllerResourceBuilder
12
- include CanCan::ControllerResourceSanitizer
13
-
14
- def load_resource
15
- return if skip?(:load)
16
-
17
- if load_instance?
18
- self.resource_instance ||= load_resource_instance
19
- elsif load_collection?
20
- self.collection_instance ||= load_collection
21
- end
22
- end
23
-
24
- protected
25
-
26
- def new_actions
27
- %i[new create] + Array(@options[:new])
28
- end
29
-
30
- def resource_params_by_key(key)
31
- return unless @options[key] && @params.key?(extract_key(@options[key]))
32
-
33
- @params[extract_key(@options[key])]
34
- end
35
-
36
- def resource_params_by_namespaced_name
37
- resource_params_by_key(:instance_name) || resource_params_by_key(:class) || (
38
- params = @params[extract_key(namespaced_name)]
39
- params.respond_to?(:to_h) ? params : nil)
40
- end
41
-
42
- def resource_params
43
- if parameters_require_sanitizing? && params_method.present?
44
- sanitize_parameters
45
- else
46
- resource_params_by_namespaced_name
47
- end
48
- end
49
-
50
- def fetch_parent(name)
51
- if @controller.instance_variable_defined? "@#{name}"
52
- @controller.instance_variable_get("@#{name}")
53
- elsif @controller.respond_to?(name, true)
54
- @controller.send(name)
55
- end
56
- end
57
-
58
- # The object to load this resource through.
59
- def parent_resource
60
- parent_name && fetch_parent(parent_name)
61
- end
62
-
63
- def parent_name
64
- @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
65
- end
66
-
67
- def resource_base_through_parent_resource
68
- if @options[:singleton]
69
- resource_class
70
- else
71
- parent_resource.send(@options[:through_association] || name.to_s.pluralize)
72
- end
73
- end
74
-
75
- def resource_base_through
76
- if parent_resource
77
- resource_base_through_parent_resource
78
- elsif @options[:shallow]
79
- resource_class
80
- else
81
- # maybe this should be a record not found error instead?
82
- raise AccessDenied.new(nil, authorization_action, resource_class)
83
- end
84
- end
85
-
86
- # The object that methods (such as "find", "new" or "build") are called on.
87
- # If the :through option is passed it will go through an association on that instance.
88
- # If the :shallow option is passed it will use the resource_class if there's no parent
89
- # If the :singleton option is passed it won't use the association because it needs to be handled later.
90
- def resource_base
91
- @options[:through] ? resource_base_through : resource_class
92
- end
93
-
94
- def parent_authorization_action
95
- @options[:parent_action] || :show
96
- end
97
-
98
- def authorization_action
99
- parent? ? parent_authorization_action : @params[:action].to_sym
100
- end
101
-
102
- def load_collection
103
- resource_base.accessible_by(current_ability, authorization_action)
104
- end
105
-
106
- def load_resource_instance
107
- if !parent? && new_actions.include?(@params[:action].to_sym)
108
- build_resource
109
- elsif id_param || @options[:singleton]
110
- find_resource
111
- end
112
- end
113
-
114
- private
115
-
116
- def extract_key(value)
117
- value.to_s.underscore.tr('/', '_')
118
- end
119
- end
120
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CanCan
4
- module ControllerResourceNameFinder
5
- protected
6
-
7
- def name_from_controller
8
- @params[:controller].split('/').last.singularize
9
- end
10
-
11
- def namespaced_name
12
- [namespace, name].join('/').singularize.camelize.safe_constantize || name
13
- end
14
-
15
- def name
16
- @name || name_from_controller
17
- end
18
-
19
- def namespace
20
- @params[:controller].split('/')[0..-2]
21
- end
22
- end
23
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CanCan
4
- module ControllerResourceSanitizer
5
- protected
6
-
7
- def sanitize_parameters
8
- case params_method
9
- when Symbol
10
- @controller.send(params_method)
11
- when String
12
- @controller.instance_eval(params_method)
13
- when Proc
14
- params_method.call(@controller)
15
- end
16
- end
17
-
18
- def params_methods
19
- methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
20
- methods.unshift(@options[:param_method]) if @options[:param_method].present?
21
- methods
22
- end
23
-
24
- def params_method
25
- params_methods.each do |method|
26
- return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
27
- method.is_a?(String) || method.is_a?(Proc)
28
- end
29
- nil
30
- end
31
- end
32
- end