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.
- checksums.yaml +5 -5
- data/cancancan.gemspec +19 -21
- data/init.rb +2 -0
- data/lib/cancan/ability/actions.rb +93 -0
- data/lib/cancan/ability/rules.rb +96 -0
- data/lib/cancan/ability/strong_parameter_support.rb +41 -0
- data/lib/cancan/ability.rb +114 -146
- data/lib/cancan/class_matcher.rb +30 -0
- data/lib/cancan/conditions_matcher.rb +147 -0
- data/lib/cancan/config.rb +101 -0
- data/lib/cancan/controller_additions.rb +38 -41
- data/lib/cancan/controller_resource.rb +59 -215
- data/lib/cancan/controller_resource_builder.rb +26 -0
- data/lib/cancan/controller_resource_finder.rb +42 -0
- data/lib/cancan/controller_resource_loader.rb +120 -0
- data/lib/cancan/controller_resource_name_finder.rb +23 -0
- data/lib/cancan/controller_resource_sanitizer.rb +32 -0
- data/lib/cancan/exceptions.rb +25 -5
- data/lib/cancan/matchers.rb +17 -3
- data/lib/cancan/model_adapters/abstract_adapter.rb +30 -9
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +43 -15
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +61 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +157 -82
- data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
- data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
- data/lib/cancan/model_adapters/default_adapter.rb +2 -0
- data/lib/cancan/model_adapters/sti_normalizer.rb +47 -0
- data/lib/cancan/model_adapters/strategies/base.rb +40 -0
- data/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +93 -0
- data/lib/cancan/model_adapters/strategies/joined_alias_exists_subquery.rb +31 -0
- data/lib/cancan/model_adapters/strategies/left_join.rb +11 -0
- data/lib/cancan/model_adapters/strategies/subquery.rb +18 -0
- data/lib/cancan/model_additions.rb +6 -3
- data/lib/cancan/parameter_validators.rb +9 -0
- data/lib/cancan/relevant.rb +29 -0
- data/lib/cancan/rule.rb +79 -91
- data/lib/cancan/rules_compressor.rb +23 -0
- data/lib/cancan/sti_detector.rb +12 -0
- data/lib/cancan/unauthorized_message_resolver.rb +24 -0
- data/lib/cancan/version.rb +3 -1
- data/lib/cancan.rb +16 -12
- data/lib/cancancan.rb +2 -0
- data/lib/generators/cancan/ability/ability_generator.rb +4 -2
- data/lib/generators/cancan/ability/templates/ability.rb +9 -9
- metadata +82 -93
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.travis.yml +0 -48
- data/Appraisals +0 -135
- data/CHANGELOG.rdoc +0 -495
- data/CONTRIBUTING.md +0 -23
- data/Gemfile +0 -3
- data/LICENSE +0 -22
- data/README.md +0 -197
- data/Rakefile +0 -9
- data/gemfiles/activerecord_3.0.gemfile +0 -18
- data/gemfiles/activerecord_3.1.gemfile +0 -20
- data/gemfiles/activerecord_3.2.gemfile +0 -20
- data/gemfiles/activerecord_4.0.gemfile +0 -17
- data/gemfiles/activerecord_4.1.gemfile +0 -17
- data/gemfiles/activerecord_4.2.gemfile +0 -17
- data/gemfiles/datamapper_1.x.gemfile +0 -14
- data/gemfiles/mongoid_2.x.gemfile +0 -20
- data/gemfiles/sequel_3.x.gemfile +0 -20
- data/lib/cancan/inherited_resource.rb +0 -20
- data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -47
- data/lib/cancan/model_adapters/data_mapper_adapter.rb +0 -34
- data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -54
- data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
- data/spec/README.rdoc +0 -27
- data/spec/cancan/ability_spec.rb +0 -487
- data/spec/cancan/controller_additions_spec.rb +0 -141
- data/spec/cancan/controller_resource_spec.rb +0 -648
- data/spec/cancan/exceptions_spec.rb +0 -58
- data/spec/cancan/inherited_resource_spec.rb +0 -71
- data/spec/cancan/matchers_spec.rb +0 -29
- data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -40
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -446
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +0 -119
- data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -227
- data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
- data/spec/cancan/rule_spec.rb +0 -52
- data/spec/matchers.rb +0 -13
- data/spec/spec.opts +0 -2
- data/spec/spec_helper.rb +0 -27
- 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
|
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?(
|
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?(
|
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?(
|
31
|
-
raise NotImplemented,
|
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?(
|
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?(
|
42
|
-
raise NotImplemented,
|
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,
|
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 <
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
16
|
-
relation
|
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
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
113
|
+
conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
|
23
114
|
else
|
24
|
-
|
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
|
31
|
-
|
32
|
-
|
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
|
-
|
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(:
|
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
|
-
|
80
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if
|
87
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
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
|