cancancan 1.10.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +19 -21
  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 +114 -146
  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 +38 -41
  12. data/lib/cancan/controller_resource.rb +59 -215
  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 +25 -5
  19. data/lib/cancan/matchers.rb +17 -3
  20. data/lib/cancan/model_adapters/abstract_adapter.rb +30 -9
  21. data/lib/cancan/model_adapters/active_record_4_adapter.rb +43 -15
  22. data/lib/cancan/model_adapters/active_record_5_adapter.rb +61 -0
  23. data/lib/cancan/model_adapters/active_record_adapter.rb +157 -82
  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 -3
  34. data/lib/cancan/parameter_validators.rb +9 -0
  35. data/lib/cancan/relevant.rb +29 -0
  36. data/lib/cancan/rule.rb +79 -91
  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 +16 -12
  42. data/lib/cancancan.rb +2 -0
  43. data/lib/generators/cancan/ability/ability_generator.rb +4 -2
  44. data/lib/generators/cancan/ability/templates/ability.rb +9 -9
  45. metadata +82 -93
  46. data/.gitignore +0 -15
  47. data/.rspec +0 -1
  48. data/.travis.yml +0 -48
  49. data/Appraisals +0 -135
  50. data/CHANGELOG.rdoc +0 -495
  51. data/CONTRIBUTING.md +0 -23
  52. data/Gemfile +0 -3
  53. data/LICENSE +0 -22
  54. data/README.md +0 -197
  55. data/Rakefile +0 -9
  56. data/gemfiles/activerecord_3.0.gemfile +0 -18
  57. data/gemfiles/activerecord_3.1.gemfile +0 -20
  58. data/gemfiles/activerecord_3.2.gemfile +0 -20
  59. data/gemfiles/activerecord_4.0.gemfile +0 -17
  60. data/gemfiles/activerecord_4.1.gemfile +0 -17
  61. data/gemfiles/activerecord_4.2.gemfile +0 -17
  62. data/gemfiles/datamapper_1.x.gemfile +0 -14
  63. data/gemfiles/mongoid_2.x.gemfile +0 -20
  64. data/gemfiles/sequel_3.x.gemfile +0 -20
  65. data/lib/cancan/inherited_resource.rb +0 -20
  66. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -47
  67. data/lib/cancan/model_adapters/data_mapper_adapter.rb +0 -34
  68. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -54
  69. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  70. data/spec/README.rdoc +0 -27
  71. data/spec/cancan/ability_spec.rb +0 -487
  72. data/spec/cancan/controller_additions_spec.rb +0 -141
  73. data/spec/cancan/controller_resource_spec.rb +0 -648
  74. data/spec/cancan/exceptions_spec.rb +0 -58
  75. data/spec/cancan/inherited_resource_spec.rb +0 -71
  76. data/spec/cancan/matchers_spec.rb +0 -29
  77. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -40
  78. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -446
  79. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +0 -119
  80. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  81. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -227
  82. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
  83. data/spec/cancan/rule_spec.rb +0 -52
  84. data/spec/matchers.rb +0 -13
  85. data/spec/spec.opts +0 -2
  86. data/spec/spec_helper.rb +0 -27
  87. data/spec/support/ability.rb +0 -7
@@ -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)
@@ -11,7 +15,7 @@ module CanCan
11
15
  end
12
16
 
13
17
  # Used to determine if the given adapter should be used for the passed in class.
14
- def self.for_class?(member_class)
18
+ def self.for_class?(_member_class)
15
19
  false # override in subclass
16
20
  end
17
21
 
@@ -22,24 +26,41 @@ module CanCan
22
26
 
23
27
  # Used to determine if this model adapter will override the matching behavior for a hash of conditions.
24
28
  # If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
25
- def self.override_conditions_hash_matching?(subject, conditions)
29
+ def self.override_conditions_hash_matching?(_subject, _conditions)
26
30
  false
27
31
  end
28
32
 
29
33
  # Override if override_conditions_hash_matching? returns true
30
- def self.matches_conditions_hash?(subject, conditions)
31
- raise NotImplemented, "This model adapter does not support matching on a conditions hash."
34
+ def self.matches_conditions_hash?(_subject, _conditions)
35
+ raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
36
+ end
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.'
32
53
  end
33
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
- def self.override_condition_matching?(subject, name, value)
57
+ def self.override_condition_matching?(_subject, _name, _value)
37
58
  false
38
59
  end
39
60
 
40
61
  # Override if override_condition_matching? returns true
41
- def self.matches_condition?(subject, name, value)
42
- raise NotImplemented, "This model adapter does not support matching on a specific condition."
62
+ def self.matches_condition?(_subject, _name, _value)
63
+ raise NotImplemented, 'This model adapter does not support matching on a specific condition.'
43
64
  end
44
65
 
45
66
  def initialize(model_class, rules)
@@ -49,7 +70,7 @@ module CanCan
49
70
 
50
71
  def database_records
51
72
  # This should be overridden in a subclass to return records which match @rules
52
- raise NotImplemented, "This model adapter does not support fetching records from the database."
73
+ raise NotImplemented, 'This model adapter does not support fetching records from the database.'
53
74
  end
54
75
  end
55
76
  end
@@ -1,9 +1,31 @@
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
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
12
+
13
+ def override_condition_matching?(subject, name, _value)
14
+ subject.class.defined_enums.include?(name.to_s)
15
+ end
16
+
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
28
+ end
7
29
  end
8
30
 
9
31
  private
@@ -12,22 +34,28 @@ module CanCan
12
34
  # look inside the where clause to decide to outer join tables
13
35
  # you're using in the where. Instead, `references()` is required
14
36
  # in addition to `includes()` to force the outer join.
15
- def build_relation(*where_conditions)
16
- relation = @model_class.where(*where_conditions)
17
- relation = relation.includes(joins).references(joins) if joins.present?
18
- relation
37
+ def build_joins_relation(relation, *_where_conditions)
38
+ relation.includes(joins).references(joins)
19
39
  end
20
40
 
21
41
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
22
42
  def sanitize_sql(conditions)
23
- if ActiveRecord::VERSION::MINOR >= 2 && Hash === conditions
24
- relation = @model_class.unscoped.where(conditions)
25
- predicates = relation.where_values
26
- bind_values = relation.bind_values
27
- query = Arel::Nodes::And.new(predicates).to_sql
28
- conditions = [query, *bind_values.map { |col, val| val }]
43
+ if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash)
44
+ sanitize_sql_activerecord4(conditions)
45
+ else
46
+ @model_class.send(:sanitize_sql, conditions)
29
47
  end
30
- @model_class.send(:sanitize_sql, conditions)
48
+ end
49
+
50
+ def sanitize_sql_activerecord4(conditions)
51
+ table = Arel::Table.new(@model_class.send(:table_name))
52
+
53
+ conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
54
+ conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
55
+
56
+ ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map do |b|
57
+ @model_class.send(:connection).visitor.compile b
58
+ end.join(' AND ')
31
59
  end
32
60
  end
33
61
  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,94 +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.kind_of? Hash
32
- conditions.inject({}) do |result_hash, (name, value)|
33
- if value.kind_of? Hash
34
- value = value.dup
35
- association_class = model_class.reflect_on_association(name).klass.name.constantize
36
- nested = value.inject({}) do |nested,(k,v)|
37
- if v.kind_of? 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,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)
50
122
  end
51
123
  end
52
124
 
53
- # Returns the associations used in conditions for the :joins option of a search.
54
- # See ModelAdditions#accessible_by
55
- def joins
56
- joins_hash = {}
57
- @rules.each do |rule|
58
- merge_joins(joins_hash, rule.associations_hash)
59
- end
60
- clean_joins(joins_hash) unless joins_hash.empty?
61
- end
62
-
63
125
  def database_records
64
126
  if override_scope
65
127
  @model_class.where(nil).merge(override_scope)
66
128
  elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
67
- if mergeable_conditions?
68
- build_relation(conditions)
69
- else
70
- build_relation(*(@rules.map(&:conditions)))
71
- end
129
+ build_relation(conditions)
72
130
  else
73
- @model_class.all(:conditions => conditions, :joins => joins)
131
+ @model_class.all(conditions: conditions, joins: joins)
132
+ end
133
+ end
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
+
143
+ # Returns the associations used in conditions for the :joins option of a search.
144
+ # See ModelAdditions#accessible_by
145
+ def joins
146
+ joins_hash = {}
147
+ @compressed_rules.reverse_each do |rule|
148
+ deep_merge(joins_hash, rule.associations_hash)
74
149
  end
150
+ deep_clean(joins_hash) unless joins_hash.empty?
75
151
  end
76
152
 
77
153
  private
78
154
 
79
- def mergeable_conditions?
80
- @rules.find {|rule| rule.unmergeable? }.blank?
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) } }
81
158
  end
82
159
 
83
- def override_scope
84
- conditions = @rules.map(&:conditions).compact
85
- if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
86
- if conditions.size == 1
87
- conditions.first
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?
88
165
  else
89
- rule = @rules.detect { |rule| rule.conditions.kind_of?(ActiveRecord::Relation) }
90
- raise Error, "Unable to merge an Active Record scope with other conditions. Instead use a hash or SQL for #{rule.actions.first} #{rule.subjects.first} ability."
166
+ base_hash[key] = value
91
167
  end
92
168
  end
93
169
  end
94
170
 
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
175
+
176
+ raise_override_scope_error
177
+ end
178
+
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."
184
+ end
185
+
95
186
  def merge_conditions(sql, conditions_hash, behavior)
96
187
  if conditions_hash.blank?
97
188
  behavior ? true_sql : false_sql
98
189
  else
99
- conditions = sanitize_sql(conditions_hash)
100
- case sql
101
- when true_sql
102
- behavior ? true_sql : "not (#{conditions})"
103
- when false_sql
104
- behavior ? conditions : false_sql
105
- else
106
- behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
107
- 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})"
108
203
  end
109
204
  end
110
205
 
@@ -119,30 +214,10 @@ module CanCan
119
214
  def sanitize_sql(conditions)
120
215
  @model_class.send(:sanitize_sql, conditions)
121
216
  end
122
-
123
- # Takes two hashes and does a deep merge.
124
- def merge_joins(base, add)
125
- add.each do |name, nested|
126
- if base[name].is_a?(Hash)
127
- merge_joins(base[name], nested) unless nested.empty?
128
- else
129
- base[name] = nested
130
- end
131
- end
132
- end
133
-
134
- # Removes empty hashes and moves everything into arrays.
135
- def clean_joins(joins_hash)
136
- joins = []
137
- joins_hash.each do |name, nested|
138
- joins << (nested.empty? ? name : {name => clean_joins(nested)})
139
- end
140
- joins
141
- end
142
217
  end
143
218
  end
144
219
  end
145
220
 
146
- ActiveRecord::Base.class_eval do
147
- include CanCan::ModelAdditions
221
+ ActiveSupport.on_load(:active_record) do
222
+ send :include, CanCan::ModelAdditions
148
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