cancancan 1.10.0 → 3.5.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.
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