cancancan 2.2.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 +4 -4
- data/cancancan.gemspec +6 -5
- data/init.rb +2 -0
- data/lib/cancan/ability/actions.rb +2 -0
- data/lib/cancan/ability/rules.rb +20 -9
- data/lib/cancan/ability/strong_parameter_support.rb +41 -0
- data/lib/cancan/ability.rb +54 -24
- data/lib/cancan/class_matcher.rb +30 -0
- data/lib/cancan/conditions_matcher.rb +72 -18
- data/lib/cancan/config.rb +101 -0
- data/lib/cancan/controller_additions.rb +9 -4
- data/lib/cancan/controller_resource.rb +7 -1
- data/lib/cancan/controller_resource_builder.rb +2 -0
- data/lib/cancan/controller_resource_finder.rb +2 -0
- data/lib/cancan/controller_resource_loader.rb +4 -0
- data/lib/cancan/controller_resource_name_finder.rb +2 -0
- data/lib/cancan/controller_resource_sanitizer.rb +2 -0
- data/lib/cancan/exceptions.rb +21 -2
- data/lib/cancan/matchers.rb +7 -2
- data/lib/cancan/model_adapters/abstract_adapter.rb +22 -1
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +26 -25
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +17 -26
- data/lib/cancan/model_adapters/active_record_adapter.rb +134 -45
- 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 -2
- data/lib/cancan/parameter_validators.rb +9 -0
- data/lib/cancan/relevant.rb +29 -0
- data/lib/cancan/rule.rb +76 -20
- 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 +13 -0
- data/lib/cancancan.rb +2 -0
- data/lib/generators/cancan/ability/ability_generator.rb +3 -1
- data/lib/generators/cancan/ability/templates/ability.rb +9 -9
- metadata +39 -24
- data/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +0 -39
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'controller_resource_loader.rb'
|
2
4
|
module CanCan
|
3
5
|
# Handle the load and authorization controller logic
|
@@ -34,6 +36,7 @@ module CanCan
|
|
34
36
|
|
35
37
|
def authorize_resource
|
36
38
|
return if skip?(:authorize)
|
39
|
+
|
37
40
|
@controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
|
38
41
|
end
|
39
42
|
|
@@ -43,6 +46,7 @@ module CanCan
|
|
43
46
|
|
44
47
|
def skip?(behavior)
|
45
48
|
return false unless (options = @controller.class.cancan_skipper[behavior][@name])
|
49
|
+
|
46
50
|
options == {} ||
|
47
51
|
options[:except] && !action_exists_in?(options[:except]) ||
|
48
52
|
action_exists_in?(options[:only])
|
@@ -50,7 +54,7 @@ module CanCan
|
|
50
54
|
|
51
55
|
protected
|
52
56
|
|
53
|
-
# Returns the class used for this resource. This can be
|
57
|
+
# Returns the class used for this resource. This can be overridden by the :class option.
|
54
58
|
# If +false+ is passed in it will use the resource name as a symbol in which case it should
|
55
59
|
# only be used for authorization, not loading since there's no class to load through.
|
56
60
|
def resource_class
|
@@ -90,6 +94,7 @@ module CanCan
|
|
90
94
|
|
91
95
|
def resource_instance
|
92
96
|
return unless load_instance? && @controller.instance_variable_defined?("@#{instance_name}")
|
97
|
+
|
93
98
|
@controller.instance_variable_get("@#{instance_name}")
|
94
99
|
end
|
95
100
|
|
@@ -99,6 +104,7 @@ module CanCan
|
|
99
104
|
|
100
105
|
def collection_instance
|
101
106
|
return unless @controller.instance_variable_defined?("@#{instance_name.to_s.pluralize}")
|
107
|
+
|
102
108
|
@controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
|
103
109
|
end
|
104
110
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'controller_resource_finder.rb'
|
2
4
|
require_relative 'controller_resource_name_finder.rb'
|
3
5
|
require_relative 'controller_resource_builder.rb'
|
@@ -11,6 +13,7 @@ module CanCan
|
|
11
13
|
|
12
14
|
def load_resource
|
13
15
|
return if skip?(:load)
|
16
|
+
|
14
17
|
if load_instance?
|
15
18
|
self.resource_instance ||= load_resource_instance
|
16
19
|
elsif load_collection?
|
@@ -26,6 +29,7 @@ module CanCan
|
|
26
29
|
|
27
30
|
def resource_params_by_key(key)
|
28
31
|
return unless @options[key] && @params.key?(extract_key(@options[key]))
|
32
|
+
|
29
33
|
@params[extract_key(@options[key])]
|
30
34
|
end
|
31
35
|
|
data/lib/cancan/exceptions.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
# A general CanCan exception
|
3
5
|
class Error < StandardError; end
|
@@ -8,9 +10,18 @@ module CanCan
|
|
8
10
|
# Raised when removed code is called, an alternative solution is provided in message.
|
9
11
|
class ImplementationRemoved < Error; end
|
10
12
|
|
11
|
-
# Raised when using check_authorization without calling
|
13
|
+
# Raised when using check_authorization without calling authorize!
|
12
14
|
class AuthorizationNotPerformed < Error; end
|
13
15
|
|
16
|
+
# Raised when a rule is created with both a block and a hash of conditions
|
17
|
+
class BlockAndConditionsError < Error; end
|
18
|
+
|
19
|
+
# Raised when an unexpected argument is passed as an attribute
|
20
|
+
class AttributeArgumentError < Error; end
|
21
|
+
|
22
|
+
# Raised when using a wrong association name
|
23
|
+
class WrongAssociationName < Error; end
|
24
|
+
|
14
25
|
# This error is raised when a user isn't allowed to access a given controller action.
|
15
26
|
# This usually happens within a call to ControllerAdditions#authorize! but can be
|
16
27
|
# raised manually.
|
@@ -30,7 +41,7 @@ module CanCan
|
|
30
41
|
# exception.default_message = "Default error message"
|
31
42
|
# exception.message # => "Default error message"
|
32
43
|
#
|
33
|
-
# See ControllerAdditions#
|
44
|
+
# See ControllerAdditions#authorize! for more information on rescuing from this exception
|
34
45
|
# and customizing the message using I18n.
|
35
46
|
class AccessDenied < Error
|
36
47
|
attr_reader :action, :subject, :conditions
|
@@ -47,5 +58,13 @@ module CanCan
|
|
47
58
|
def to_s
|
48
59
|
@message || @default_message
|
49
60
|
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
details = %i[action subject conditions message].map do |attribute|
|
64
|
+
value = instance_variable_get "@#{attribute}"
|
65
|
+
"#{attribute}: #{value.inspect}" if value.present?
|
66
|
+
end.compact.join(', ')
|
67
|
+
"#<#{self.class.name} #{details}>"
|
68
|
+
end
|
50
69
|
end
|
51
70
|
end
|
data/lib/cancan/matchers.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # RSpec 1 compatability
|
2
4
|
|
3
5
|
if rspec_module == 'RSpec'
|
@@ -11,8 +13,11 @@ Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
|
|
11
13
|
match do |ability|
|
12
14
|
actions = args.first
|
13
15
|
if actions.is_a? Array
|
14
|
-
|
15
|
-
|
16
|
+
if actions.empty?
|
17
|
+
false
|
18
|
+
else
|
19
|
+
actions.all? { |action| ability.can?(action, *args[1..-1]) }
|
20
|
+
end
|
16
21
|
else
|
17
22
|
ability.can?(*args)
|
18
23
|
end
|
@@ -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)
|
@@ -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,27 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
module ModelAdapters
|
3
|
-
class ActiveRecord4Adapter <
|
4
|
-
|
5
|
-
def self.for_class?(model_class)
|
6
|
-
ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base
|
7
|
-
end
|
5
|
+
class ActiveRecord4Adapter < ActiveRecordAdapter
|
6
|
+
AbstractAdapter.inherited(self)
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
class << self
|
9
|
+
def for_class?(model_class)
|
10
|
+
version_lower?('5.0.0') && model_class <= ActiveRecord::Base
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
@@ -31,15 +34,13 @@ module CanCan
|
|
31
34
|
# look inside the where clause to decide to outer join tables
|
32
35
|
# you're using in the where. Instead, `references()` is required
|
33
36
|
# in addition to `includes()` to force the outer join.
|
34
|
-
def
|
35
|
-
relation
|
36
|
-
relation = relation.includes(joins).references(joins) if joins.present?
|
37
|
-
relation
|
37
|
+
def build_joins_relation(relation, *_where_conditions)
|
38
|
+
relation.includes(joins).references(joins)
|
38
39
|
end
|
39
40
|
|
40
41
|
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
41
42
|
def sanitize_sql(conditions)
|
42
|
-
if
|
43
|
+
if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash)
|
43
44
|
sanitize_sql_activerecord4(conditions)
|
44
45
|
else
|
45
46
|
@model_class.send(:sanitize_sql, conditions)
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
module ModelAdapters
|
3
5
|
class ActiveRecord5Adapter < ActiveRecord4Adapter
|
4
6
|
AbstractAdapter.inherited(self)
|
5
7
|
|
6
8
|
def self.for_class?(model_class)
|
7
|
-
|
9
|
+
version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base
|
8
10
|
end
|
9
11
|
|
10
12
|
# rails 5 is capable of using strings in enum
|
@@ -13,26 +15,21 @@ module CanCan
|
|
13
15
|
return super if Array.wrap(value).all? { |x| x.is_a? Integer }
|
14
16
|
|
15
17
|
attribute = subject.send(name)
|
16
|
-
|
17
|
-
|
18
|
-
else
|
19
|
-
attribute == value.to_s
|
20
|
-
end
|
18
|
+
raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
|
19
|
+
!(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
|
21
20
|
end
|
22
21
|
|
23
22
|
private
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
relation
|
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)
|
33
31
|
end
|
34
32
|
|
35
|
-
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
36
33
|
def sanitize_sql(conditions)
|
37
34
|
if conditions.is_a?(Hash)
|
38
35
|
sanitize_sql_activerecord5(conditions)
|
@@ -46,23 +43,17 @@ module CanCan
|
|
46
43
|
table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
|
47
44
|
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
|
48
45
|
|
49
|
-
conditions
|
50
|
-
|
51
|
-
conditions.stringify_keys!
|
52
|
-
|
53
|
-
predicate_builder.build_from_hash(conditions).map do |b|
|
54
|
-
visit_nodes(b)
|
55
|
-
end.join(' AND ')
|
46
|
+
predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
|
56
47
|
end
|
57
48
|
|
58
|
-
def visit_nodes(
|
49
|
+
def visit_nodes(node)
|
59
50
|
# Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
|
60
|
-
if
|
51
|
+
if self.class.version_greater_or_equal?('5.2.0')
|
61
52
|
connection = @model_class.send(:connection)
|
62
53
|
collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
|
63
|
-
connection.visitor.accept(
|
54
|
+
connection.visitor.accept(node, collector).value
|
64
55
|
else
|
65
|
-
@model_class.send(:connection).visitor.compile(
|
56
|
+
@model_class.send(:connection).visitor.compile(node)
|
66
57
|
end
|
67
58
|
end
|
68
59
|
end
|
@@ -1,8 +1,95 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module CanCan
|
3
4
|
module ModelAdapters
|
4
|
-
|
5
|
-
|
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
|
6
93
|
|
7
94
|
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
8
95
|
# method directly, but instead go through ModelAdditions#accessible_by.
|
@@ -20,47 +107,18 @@ module CanCan
|
|
20
107
|
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
21
108
|
#
|
22
109
|
def conditions
|
23
|
-
|
110
|
+
conditions_extractor = ConditionsExtractor.new(@model_class)
|
111
|
+
if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
|
24
112
|
# Return the conditions directly if there's just one definition
|
25
|
-
|
113
|
+
conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
|
26
114
|
else
|
27
|
-
extract_multiple_conditions
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def extract_multiple_conditions
|
32
|
-
@rules.reverse.inject(false_sql) do |sql, rule|
|
33
|
-
merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def tableized_conditions(conditions, model_class = @model_class)
|
38
|
-
return conditions unless conditions.is_a? Hash
|
39
|
-
conditions.each_with_object({}) do |(name, value), result_hash|
|
40
|
-
calculate_result_hash(model_class, name, result_hash, value)
|
115
|
+
extract_multiple_conditions(conditions_extractor, @compressed_rules)
|
41
116
|
end
|
42
117
|
end
|
43
118
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
nested_resulted = calculate_nested(model_class, name, result_hash, value.dup)
|
48
|
-
result_hash.merge!(tableized_conditions(nested_resulted, association_class))
|
49
|
-
else
|
50
|
-
result_hash[name] = value
|
51
|
-
end
|
52
|
-
result_hash
|
53
|
-
end
|
54
|
-
|
55
|
-
def calculate_nested(model_class, name, result_hash, value)
|
56
|
-
value.each_with_object({}) do |(k, v), nested|
|
57
|
-
if v.is_a? Hash
|
58
|
-
value.delete(k)
|
59
|
-
nested[k] = v
|
60
|
-
else
|
61
|
-
result_hash[model_class.reflect_on_association(name).table_name.to_sym] = value
|
62
|
-
end
|
63
|
-
nested
|
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)
|
64
122
|
end
|
65
123
|
end
|
66
124
|
|
@@ -68,29 +126,60 @@ module CanCan
|
|
68
126
|
if override_scope
|
69
127
|
@model_class.where(nil).merge(override_scope)
|
70
128
|
elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
|
71
|
-
|
129
|
+
build_relation(conditions)
|
72
130
|
else
|
73
131
|
@model_class.all(conditions: conditions, joins: joins)
|
74
132
|
end
|
75
133
|
end
|
76
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)
|
149
|
+
end
|
150
|
+
deep_clean(joins_hash) unless joins_hash.empty?
|
151
|
+
end
|
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) } }
|
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?
|
165
|
+
else
|
166
|
+
base_hash[key] = value
|
167
|
+
end
|
168
|
+
end
|
81
169
|
end
|
82
170
|
|
83
171
|
def override_scope
|
84
|
-
conditions = @
|
172
|
+
conditions = @compressed_rules.map(&:conditions).compact
|
85
173
|
return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
|
86
174
|
return conditions.first if conditions.size == 1
|
175
|
+
|
87
176
|
raise_override_scope_error
|
88
177
|
end
|
89
178
|
|
90
179
|
def raise_override_scope_error
|
91
|
-
rule_found = @
|
180
|
+
rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
|
92
181
|
raise Error,
|
93
|
-
'Unable to merge an Active Record scope with other conditions. '\
|
182
|
+
'Unable to merge an Active Record scope with other conditions. ' \
|
94
183
|
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
95
184
|
end
|
96
185
|
|
@@ -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
|