cancancan 1.17.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +10 -11
  3. data/init.rb +2 -0
  4. data/lib/cancan/ability/actions.rb +93 -0
  5. data/lib/cancan/ability/rules.rb +96 -0
  6. data/lib/cancan/ability/strong_parameter_support.rb +41 -0
  7. data/lib/cancan/ability.rb +87 -198
  8. data/lib/cancan/class_matcher.rb +30 -0
  9. data/lib/cancan/conditions_matcher.rb +147 -0
  10. data/lib/cancan/config.rb +101 -0
  11. data/lib/cancan/controller_additions.rb +13 -30
  12. data/lib/cancan/controller_resource.rb +33 -225
  13. data/lib/cancan/controller_resource_builder.rb +26 -0
  14. data/lib/cancan/controller_resource_finder.rb +42 -0
  15. data/lib/cancan/controller_resource_loader.rb +120 -0
  16. data/lib/cancan/controller_resource_name_finder.rb +23 -0
  17. data/lib/cancan/controller_resource_sanitizer.rb +32 -0
  18. data/lib/cancan/exceptions.rb +24 -4
  19. data/lib/cancan/matchers.rb +12 -1
  20. data/lib/cancan/model_adapters/abstract_adapter.rb +22 -1
  21. data/lib/cancan/model_adapters/active_record_4_adapter.rb +25 -44
  22. data/lib/cancan/model_adapters/active_record_5_adapter.rb +61 -0
  23. data/lib/cancan/model_adapters/active_record_adapter.rb +157 -83
  24. data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
  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 -90
  37. data/lib/cancan/rules_compressor.rb +23 -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 +15 -10
  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 +64 -86
  46. data/.gitignore +0 -15
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -39
  49. data/.rubocop_todo.yml +0 -54
  50. data/.travis.yml +0 -39
  51. data/Appraisals +0 -105
  52. data/CHANGELOG.rdoc +0 -536
  53. data/CONTRIBUTING.md +0 -23
  54. data/Gemfile +0 -3
  55. data/LICENSE +0 -22
  56. data/README.md +0 -234
  57. data/Rakefile +0 -13
  58. data/gemfiles/activerecord_3.2.gemfile +0 -18
  59. data/gemfiles/activerecord_4.0.gemfile +0 -19
  60. data/gemfiles/activerecord_4.1.gemfile +0 -19
  61. data/gemfiles/activerecord_4.2.gemfile +0 -21
  62. data/gemfiles/activerecord_5.0.gemfile +0 -20
  63. data/gemfiles/mongoid_2.x.gemfile +0 -18
  64. data/gemfiles/sequel_3.x.gemfile +0 -18
  65. data/lib/cancan/inherited_resource.rb +0 -20
  66. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -16
  67. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -80
  68. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  69. data/spec/README.rdoc +0 -27
  70. data/spec/cancan/ability_spec.rb +0 -553
  71. data/spec/cancan/controller_additions_spec.rb +0 -164
  72. data/spec/cancan/controller_resource_spec.rb +0 -645
  73. data/spec/cancan/exceptions_spec.rb +0 -58
  74. data/spec/cancan/inherited_resource_spec.rb +0 -71
  75. data/spec/cancan/matchers_spec.rb +0 -29
  76. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -160
  77. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -415
  78. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  79. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -246
  80. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -129
  81. data/spec/cancan/rule_spec.rb +0 -52
  82. data/spec/matchers.rb +0 -13
  83. data/spec/spec.opts +0 -2
  84. data/spec/spec_helper.rb +0 -27
  85. data/spec/support/ability.rb +0 -6
@@ -1,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class AbstractAdapter
6
+ attr_reader :model_class
7
+
4
8
  def self.inherited(subclass)
5
9
  @subclasses ||= []
6
- @subclasses << subclass
10
+ @subclasses.insert(0, subclass)
7
11
  end
8
12
 
9
13
  def self.adapter_class(model_class)
@@ -31,6 +35,23 @@ module CanCan
31
35
  raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
32
36
  end
33
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
+
43
+ # Used above override_conditions_hash_matching to determine if this model adapter will override the
44
+ # matching behavior for nested subject.
45
+ # If this returns true then nested_subject_matches_conditions? will be called.
46
+ def self.override_nested_subject_conditions_matching?(_parent, _child, _all_conditions)
47
+ false
48
+ end
49
+
50
+ # Override if override_nested_subject_conditions_matching? returns true
51
+ def self.nested_subject_matches_conditions?(_parent, _child, _all_conditions)
52
+ raise NotImplemented, 'This model adapter does not support matching on a nested subject.'
53
+ end
54
+
34
55
  # Used to determine if this model adapter will override the matching behavior for a specific condition.
35
56
  # If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
36
57
  def self.override_condition_matching?(_subject, _name, _value)
@@ -1,29 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
- class ActiveRecord4Adapter < AbstractAdapter
4
- include ActiveRecordAdapter
5
- def self.for_class?(model_class)
6
- model_class <= ActiveRecord::Base
7
- end
5
+ class ActiveRecord4Adapter < ActiveRecordAdapter
6
+ AbstractAdapter.inherited(self)
7
+
8
+ class << self
9
+ def for_class?(model_class)
10
+ version_lower?('5.0.0') && model_class <= ActiveRecord::Base
11
+ end
8
12
 
9
- # TODO: this should be private
10
- def self.override_condition_matching?(subject, name, _value)
11
- # ActiveRecord introduced enums in version 4.1.
12
- (ActiveRecord::VERSION::MAJOR > 4 || ActiveRecord::VERSION::MINOR >= 1) &&
13
+ def override_condition_matching?(subject, name, _value)
13
14
  subject.class.defined_enums.include?(name.to_s)
14
- end
15
+ end
15
16
 
16
- # TODO: this should be private
17
- def self.matches_condition?(subject, name, value)
18
- # Get the mapping from enum strings to values.
19
- enum = subject.class.send(name.to_s.pluralize)
20
- # Get the value of the attribute as an integer.
21
- attribute = enum[subject.send(name)]
22
- # Check to see if the value matches the condition.
23
- if value.is_a?(Enumerable)
24
- value.include? attribute
25
- else
26
- attribute == value
17
+ def matches_condition?(subject, name, value)
18
+ # Get the mapping from enum strings to values.
19
+ enum = subject.class.send(name.to_s.pluralize)
20
+ # Get the value of the attribute as an integer.
21
+ attribute = enum[subject.send(name)]
22
+ # Check to see if the value matches the condition.
23
+ if value.is_a?(Enumerable)
24
+ value.include? attribute
25
+ else
26
+ attribute == value
27
+ end
27
28
  end
28
29
  end
29
30
 
@@ -33,19 +34,14 @@ module CanCan
33
34
  # look inside the where clause to decide to outer join tables
34
35
  # you're using in the where. Instead, `references()` is required
35
36
  # in addition to `includes()` to force the outer join.
36
- def build_relation(*where_conditions)
37
- relation = @model_class.where(*where_conditions)
38
- relation = relation.includes(joins).references(joins) if joins.present?
39
- relation
37
+ def build_joins_relation(relation, *_where_conditions)
38
+ relation.includes(joins).references(joins)
40
39
  end
41
40
 
42
41
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
43
42
  def sanitize_sql(conditions)
44
- if ActiveRecord::VERSION::MAJOR > 4 && conditions.is_a?(Hash)
45
- sanitize_sql_activerecord5(conditions)
46
- elsif ActiveRecord::VERSION::MINOR >= 2 && conditions.is_a?(Hash)
43
+ if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash)
47
44
  sanitize_sql_activerecord4(conditions)
48
-
49
45
  else
50
46
  @model_class.send(:sanitize_sql, conditions)
51
47
  end
@@ -61,21 +57,6 @@ module CanCan
61
57
  @model_class.send(:connection).visitor.compile b
62
58
  end.join(' AND ')
63
59
  end
64
-
65
- def sanitize_sql_activerecord5(conditions)
66
- table = @model_class.send(:arel_table)
67
- table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
68
- predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
69
-
70
- conditions = predicate_builder.resolve_column_aliases(conditions)
71
- conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
72
-
73
- conditions.stringify_keys!
74
-
75
- predicate_builder.build_from_hash(conditions).map do |b|
76
- @model_class.send(:connection).visitor.compile b
77
- end.join(' AND ')
78
- end
79
60
  end
80
61
  end
81
62
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ModelAdapters
5
+ class ActiveRecord5Adapter < ActiveRecord4Adapter
6
+ AbstractAdapter.inherited(self)
7
+
8
+ def self.for_class?(model_class)
9
+ version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base
10
+ end
11
+
12
+ # rails 5 is capable of using strings in enum
13
+ # but often people use symbols in rules
14
+ def self.matches_condition?(subject, name, value)
15
+ return super if Array.wrap(value).all? { |x| x.is_a? Integer }
16
+
17
+ attribute = subject.send(name)
18
+ raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
19
+ !(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
20
+ end
21
+
22
+ private
23
+
24
+ def build_joins_relation(relation, *where_conditions)
25
+ strategy_class.new(adapter: self, relation: relation, where_conditions: where_conditions).execute!
26
+ end
27
+
28
+ def strategy_class
29
+ strategy_class_name = CanCan.accessible_by_strategy.to_s.camelize
30
+ CanCan::ModelAdapters::Strategies.const_get(strategy_class_name)
31
+ end
32
+
33
+ def sanitize_sql(conditions)
34
+ if conditions.is_a?(Hash)
35
+ sanitize_sql_activerecord5(conditions)
36
+ else
37
+ @model_class.send(:sanitize_sql, conditions)
38
+ end
39
+ end
40
+
41
+ def sanitize_sql_activerecord5(conditions)
42
+ table = @model_class.send(:arel_table)
43
+ table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
44
+ predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
45
+
46
+ predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
47
+ end
48
+
49
+ def visit_nodes(node)
50
+ # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
51
+ if self.class.version_greater_or_equal?('5.2.0')
52
+ connection = @model_class.send(:connection)
53
+ collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
54
+ connection.visitor.accept(node, collector).value
55
+ else
56
+ @model_class.send(:connection).visitor.compile(node)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,6 +1,96 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
- module ActiveRecordAdapter
5
+ class ActiveRecordAdapter < AbstractAdapter
6
+ def self.version_greater_or_equal?(version)
7
+ Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version)
8
+ end
9
+
10
+ def self.version_lower?(version)
11
+ Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version)
12
+ end
13
+
14
+ attr_reader :compressed_rules
15
+
16
+ def initialize(model_class, rules)
17
+ super
18
+ @compressed_rules = if CanCan.rules_compressor_enabled
19
+ RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
20
+ else
21
+ @rules
22
+ end
23
+ StiNormalizer.normalize(@compressed_rules)
24
+ ConditionsNormalizer.normalize(model_class, @compressed_rules)
25
+ end
26
+
27
+ class << self
28
+ # When belongs_to parent_id is a condition for a model,
29
+ # we want to check the parent when testing ability for a hash {parent => model}
30
+ def override_nested_subject_conditions_matching?(parent, child, all_conditions)
31
+ parent_child_conditions(parent, child, all_conditions).present?
32
+ end
33
+
34
+ # parent_id condition can be an array of integer or one integer, we check the parent against this
35
+ def nested_subject_matches_conditions?(parent, child, all_conditions)
36
+ id_condition = parent_child_conditions(parent, child, all_conditions)
37
+ return id_condition.include?(parent.id) if id_condition.is_a? Array
38
+ return id_condition == parent.id if id_condition.is_a? Integer
39
+
40
+ false
41
+ end
42
+
43
+ def parent_child_conditions(parent, child, all_conditions)
44
+ child_class = child.is_a?(Class) ? child : child.class
45
+ parent_class = parent.is_a?(Class) ? parent : parent.class
46
+
47
+ foreign_key = child_class.reflect_on_all_associations(:belongs_to).find do |association|
48
+ # Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
49
+ !association.polymorphic? && association.klass == parent.class
50
+ end&.foreign_key&.to_sym
51
+
52
+ # Search again in case of polymorphic associations, this time matching on the :has_many side
53
+ # via the :as option, as well as klass
54
+ foreign_key ||= parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
55
+ !matching_parent_child_polymorphic_association(has_many_assoc, child_class).nil?
56
+ end&.foreign_key&.to_sym
57
+
58
+ foreign_key.nil? ? nil : all_conditions[foreign_key]
59
+ end
60
+
61
+ def matching_parent_child_polymorphic_association(parent_assoc, child_class)
62
+ return nil unless parent_assoc.klass == child_class
63
+ return nil if parent_assoc&.options[:as].nil?
64
+
65
+ child_class.reflect_on_all_associations(:belongs_to).find do |child_assoc|
66
+ # Only match this way for polymorphic associations
67
+ child_assoc.polymorphic? && child_assoc.name == parent_assoc.options[:as]
68
+ end
69
+ end
70
+
71
+ def child_association_to_parent(parent, child)
72
+ child_class = child.is_a?(Class) ? child : child.class
73
+ parent_class = parent.is_a?(Class) ? parent : parent.class
74
+
75
+ association = child_class.reflect_on_all_associations(:belongs_to).find do |association|
76
+ # Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
77
+ !association.polymorphic? && association.klass == parent.class
78
+ end
79
+
80
+ return association unless association.nil?
81
+
82
+ parent_class.reflect_on_all_associations(:has_many).each do |has_many_assoc|
83
+ association ||= matching_parent_child_polymorphic_association(has_many_assoc, child_class)
84
+ end
85
+
86
+ association
87
+ end
88
+
89
+ def parent_condition_name(parent, child)
90
+ child_association_to_parent(parent, child)&.name || parent.class.name.downcase.to_sym
91
+ end
92
+ end
93
+
4
94
  # Returns conditions intended to be used inside a database query. Normally you will not call this
5
95
  # method directly, but instead go through ModelAdditions#accessible_by.
6
96
  #
@@ -17,95 +107,99 @@ module CanCan
17
107
  # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
18
108
  #
19
109
  def conditions
20
- if @rules.size == 1 && @rules.first.base_behavior
110
+ conditions_extractor = ConditionsExtractor.new(@model_class)
111
+ if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
21
112
  # Return the conditions directly if there's just one definition
22
- tableized_conditions(@rules.first.conditions).dup
113
+ conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
23
114
  else
24
- @rules.reverse.inject(false_sql) do |sql, rule|
25
- merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
26
- end
115
+ extract_multiple_conditions(conditions_extractor, @compressed_rules)
27
116
  end
28
117
  end
29
118
 
30
- def tableized_conditions(conditions, model_class = @model_class)
31
- return conditions unless conditions.is_a? Hash
32
- conditions.each_with_object({}) do |(name, value), result_hash|
33
- if value.is_a? Hash
34
- value = value.dup
35
- association_class = model_class.reflect_on_association(name).klass.name.constantize
36
- nested_resulted = value.each_with_object({}) do |(k, v), nested|
37
- if v.is_a? Hash
38
- value.delete(k)
39
- nested[k] = v
40
- else
41
- result_hash[model_class.reflect_on_association(name).table_name.to_sym] = value
42
- end
43
- nested
44
- end
45
- result_hash.merge!(tableized_conditions(nested_resulted, association_class))
46
- else
47
- result_hash[name] = value
48
- end
49
- result_hash
119
+ def extract_multiple_conditions(conditions_extractor, rules)
120
+ rules.reverse.inject(false_sql) do |sql, rule|
121
+ merge_conditions(sql, conditions_extractor.tableize_conditions(rule.conditions).dup, rule.base_behavior)
122
+ end
123
+ end
124
+
125
+ def database_records
126
+ if override_scope
127
+ @model_class.where(nil).merge(override_scope)
128
+ elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
129
+ build_relation(conditions)
130
+ else
131
+ @model_class.all(conditions: conditions, joins: joins)
50
132
  end
51
133
  end
52
134
 
135
+ def build_relation(*where_conditions)
136
+ relation = @model_class.where(*where_conditions)
137
+ return relation unless joins.present?
138
+
139
+ # subclasses must implement `build_joins_relation`
140
+ build_joins_relation(relation, *where_conditions)
141
+ end
142
+
53
143
  # Returns the associations used in conditions for the :joins option of a search.
54
144
  # See ModelAdditions#accessible_by
55
145
  def joins
56
146
  joins_hash = {}
57
- @rules.each do |rule|
58
- merge_joins(joins_hash, rule.associations_hash)
147
+ @compressed_rules.reverse_each do |rule|
148
+ deep_merge(joins_hash, rule.associations_hash)
59
149
  end
60
- clean_joins(joins_hash) unless joins_hash.empty?
150
+ deep_clean(joins_hash) unless joins_hash.empty?
61
151
  end
62
152
 
63
- def database_records
64
- if override_scope
65
- @model_class.where(nil).merge(override_scope)
66
- elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
67
- if mergeable_conditions?
68
- build_relation(conditions)
153
+ private
154
+
155
+ # Removes empty hashes and moves everything into arrays.
156
+ def deep_clean(joins_hash)
157
+ joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
158
+ end
159
+
160
+ # Takes two hashes and does a deep merge.
161
+ def deep_merge(base_hash, added_hash)
162
+ added_hash.each do |key, value|
163
+ if base_hash[key].is_a?(Hash)
164
+ deep_merge(base_hash[key], value) unless value.empty?
69
165
  else
70
- build_relation(*@rules.map(&:conditions))
166
+ base_hash[key] = value
71
167
  end
72
- else
73
- @model_class.all(conditions: conditions, joins: joins)
74
168
  end
75
169
  end
76
170
 
77
- private
171
+ def override_scope
172
+ conditions = @compressed_rules.map(&:conditions).compact
173
+ return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
174
+ return conditions.first if conditions.size == 1
78
175
 
79
- def mergeable_conditions?
80
- @rules.find(&:unmergeable?).blank?
176
+ raise_override_scope_error
81
177
  end
82
178
 
83
- def override_scope
84
- conditions = @rules.map(&:conditions).compact
85
- return unless defined?(ActiveRecord::Relation) && conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
86
- if conditions.size == 1
87
- conditions.first
88
- else
89
- rule_found = @rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
90
- raise Error,
91
- 'Unable to merge an Active Record scope with other conditions. '\
92
- "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
93
- end
179
+ def raise_override_scope_error
180
+ rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
181
+ raise Error,
182
+ 'Unable to merge an Active Record scope with other conditions. ' \
183
+ "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
94
184
  end
95
185
 
96
186
  def merge_conditions(sql, conditions_hash, behavior)
97
187
  if conditions_hash.blank?
98
188
  behavior ? true_sql : false_sql
99
189
  else
100
- conditions = sanitize_sql(conditions_hash)
101
- case sql
102
- when true_sql
103
- behavior ? true_sql : "not (#{conditions})"
104
- when false_sql
105
- behavior ? conditions : false_sql
106
- else
107
- behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
108
- end
190
+ merge_non_empty_conditions(behavior, conditions_hash, sql)
191
+ end
192
+ end
193
+
194
+ def merge_non_empty_conditions(behavior, conditions_hash, sql)
195
+ conditions = sanitize_sql(conditions_hash)
196
+ case sql
197
+ when true_sql
198
+ behavior ? true_sql : "not (#{conditions})"
199
+ when false_sql
200
+ behavior ? conditions : false_sql
201
+ else
202
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
109
203
  end
110
204
  end
111
205
 
@@ -120,30 +214,10 @@ module CanCan
120
214
  def sanitize_sql(conditions)
121
215
  @model_class.send(:sanitize_sql, conditions)
122
216
  end
123
-
124
- # Takes two hashes and does a deep merge.
125
- def merge_joins(base, add)
126
- add.each do |name, nested|
127
- if base[name].is_a?(Hash)
128
- merge_joins(base[name], nested) unless nested.empty?
129
- else
130
- base[name] = nested
131
- end
132
- end
133
- end
134
-
135
- # Removes empty hashes and moves everything into arrays.
136
- def clean_joins(joins_hash)
137
- joins = []
138
- joins_hash.each do |name, nested|
139
- joins << (nested.empty? ? name : { name => clean_joins(nested) })
140
- end
141
- joins
142
- end
143
217
  end
144
218
  end
145
219
  end
146
220
 
147
- ActiveRecord::Base.class_eval do
148
- include CanCan::ModelAdditions
221
+ ActiveSupport.on_load(:active_record) do
222
+ send :include, CanCan::ModelAdditions
149
223
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
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.
7
+ module CanCan
8
+ module ModelAdapters
9
+ class ConditionsExtractor
10
+ def initialize(model_class)
11
+ @names_cache = { model_class.table_name => [] }.with_indifferent_access
12
+ @root_model_class = model_class
13
+ end
14
+
15
+ def tableize_conditions(conditions, model_class = @root_model_class, path_to_key = 0)
16
+ return conditions unless conditions.is_a? Hash
17
+
18
+ conditions.each_with_object({}) do |(key, value), result_hash|
19
+ if value.is_a? Hash
20
+ result_hash.merge!(calculate_result_hash(key, model_class, path_to_key, result_hash, value))
21
+ else
22
+ result_hash[key] = value
23
+ end
24
+ result_hash
25
+ end
26
+ end
27
+
28
+ private
29
+
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}")
35
+ end
36
+
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
47
+ end
48
+ end
49
+
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
52
+
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
55
+
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
60
+ end
61
+ add_to_cache(table_alias, relation_name, path_to_key)
62
+ end
63
+
64
+ def already_used?(table_alias, relation_name, path_to_key)
65
+ @names_cache[table_alias].try(:exclude?, "#{path_to_key}_#{relation_name}")
66
+ end
67
+
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
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,49 @@
1
+ # this class is responsible of normalizing the hash of conditions
2
+ # by exploding has_many through associations
3
+ # when a condition is defined with an has_many through association this is exploded in all its parts
4
+ # TODO: it could identify STI and normalize it
5
+ module CanCan
6
+ module ModelAdapters
7
+ class ConditionsNormalizer
8
+ class << self
9
+ def normalize(model_class, rules)
10
+ rules.each { |rule| rule.conditions = normalize_conditions(model_class, rule.conditions) }
11
+ end
12
+
13
+ def normalize_conditions(model_class, conditions)
14
+ return conditions unless conditions.is_a? Hash
15
+
16
+ conditions.each_with_object({}) do |(key, value), result_hash|
17
+ if value.is_a? Hash
18
+ result_hash.merge!(calculate_result_hash(model_class, key, value))
19
+ else
20
+ result_hash[key] = value
21
+ end
22
+ result_hash
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def calculate_result_hash(model_class, key, value)
29
+ reflection = model_class.reflect_on_association(key)
30
+ unless reflection
31
+ raise WrongAssociationName, "Association '#{key}' not defined in model '#{model_class.name}'"
32
+ end
33
+
34
+ if normalizable_association? reflection
35
+ key = reflection.options[:through]
36
+ value = { reflection.source_reflection_name => value }
37
+ reflection = model_class.reflect_on_association(key)
38
+ end
39
+
40
+ { key => normalize_conditions(reflection.klass.name.constantize, value) }
41
+ end
42
+
43
+ def normalizable_association?(reflection)
44
+ reflection.options[:through].present? && !reflection.options[:source_type].present?
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class DefaultAdapter < AbstractAdapter
@@ -0,0 +1,47 @@
1
+ require_relative '../sti_detector'
2
+
3
+ # this class is responsible for detecting sti classes and creating new rules for the
4
+ # relevant subclasses, using the inheritance_column as a merger
5
+ module CanCan
6
+ module ModelAdapters
7
+ class StiNormalizer
8
+ class << self
9
+ def normalize(rules)
10
+ rules_cache = []
11
+ return unless defined?(ActiveRecord::Base)
12
+
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) }
20
+ end
21
+
22
+ private
23
+
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
29
+ end
30
+
31
+ # create a new rule for the subclasses that links on the inheritance_column
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
+
41
+ CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass,
42
+ new_rule_conditions, rule.block)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end