cancancan 1.11.0 → 2.3.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 +15 -19
- data/lib/cancan/ability/actions.rb +91 -0
- data/lib/cancan/ability/rules.rb +85 -0
- data/lib/cancan/ability.rb +74 -136
- data/lib/cancan/conditions_matcher.rb +93 -0
- data/lib/cancan/controller_additions.rb +34 -40
- data/lib/cancan/controller_resource.rb +47 -212
- data/lib/cancan/controller_resource_builder.rb +24 -0
- data/lib/cancan/controller_resource_finder.rb +40 -0
- data/lib/cancan/controller_resource_loader.rb +116 -0
- data/lib/cancan/controller_resource_name_finder.rb +21 -0
- data/lib/cancan/controller_resource_sanitizer.rb +30 -0
- data/lib/cancan/exceptions.rb +7 -3
- data/lib/cancan/matchers.rb +12 -3
- data/lib/cancan/model_adapters/abstract_adapter.rb +8 -8
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +33 -10
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +70 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +41 -81
- data/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +39 -0
- data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
- data/lib/cancan/model_additions.rb +0 -1
- data/lib/cancan/rule.rb +36 -92
- data/lib/cancan/rules_compressor.rb +20 -0
- data/lib/cancan/version.rb +1 -1
- data/lib/cancan.rb +5 -12
- data/lib/generators/cancan/ability/ability_generator.rb +1 -1
- metadata +54 -65
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.travis.yml +0 -55
- data/Appraisals +0 -136
- data/CHANGELOG.rdoc +0 -503
- data/CONTRIBUTING.md +0 -23
- data/Gemfile +0 -3
- data/LICENSE +0 -22
- data/README.md +0 -188
- 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 -18
- 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 -632
- 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 -85
- 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 -28
- data/spec/support/ability.rb +0 -7
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative 'controller_resource_finder.rb'
|
2
|
+
require_relative 'controller_resource_name_finder.rb'
|
3
|
+
require_relative 'controller_resource_builder.rb'
|
4
|
+
require_relative 'controller_resource_sanitizer.rb'
|
5
|
+
module CanCan
|
6
|
+
module ControllerResourceLoader
|
7
|
+
include CanCan::ControllerResourceNameFinder
|
8
|
+
include CanCan::ControllerResourceFinder
|
9
|
+
include CanCan::ControllerResourceBuilder
|
10
|
+
include CanCan::ControllerResourceSanitizer
|
11
|
+
|
12
|
+
def load_resource
|
13
|
+
return if skip?(:load)
|
14
|
+
if load_instance?
|
15
|
+
self.resource_instance ||= load_resource_instance
|
16
|
+
elsif load_collection?
|
17
|
+
self.collection_instance ||= load_collection
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def new_actions
|
24
|
+
%i[new create] + Array(@options[:new])
|
25
|
+
end
|
26
|
+
|
27
|
+
def resource_params_by_key(key)
|
28
|
+
return unless @options[key] && @params.key?(extract_key(@options[key]))
|
29
|
+
@params[extract_key(@options[key])]
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_params_by_namespaced_name
|
33
|
+
resource_params_by_key(:instance_name) || resource_params_by_key(:class) || (
|
34
|
+
params = @params[extract_key(namespaced_name)]
|
35
|
+
params.respond_to?(:to_h) ? params : nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource_params
|
39
|
+
if parameters_require_sanitizing? && params_method.present?
|
40
|
+
sanitize_parameters
|
41
|
+
else
|
42
|
+
resource_params_by_namespaced_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_parent(name)
|
47
|
+
if @controller.instance_variable_defined? "@#{name}"
|
48
|
+
@controller.instance_variable_get("@#{name}")
|
49
|
+
elsif @controller.respond_to?(name, true)
|
50
|
+
@controller.send(name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# The object to load this resource through.
|
55
|
+
def parent_resource
|
56
|
+
parent_name && fetch_parent(parent_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def parent_name
|
60
|
+
@options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def resource_base_through_parent_resource
|
64
|
+
if @options[:singleton]
|
65
|
+
resource_class
|
66
|
+
else
|
67
|
+
parent_resource.send(@options[:through_association] || name.to_s.pluralize)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def resource_base_through
|
72
|
+
if parent_resource
|
73
|
+
resource_base_through_parent_resource
|
74
|
+
elsif @options[:shallow]
|
75
|
+
resource_class
|
76
|
+
else
|
77
|
+
# maybe this should be a record not found error instead?
|
78
|
+
raise AccessDenied.new(nil, authorization_action, resource_class)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# The object that methods (such as "find", "new" or "build") are called on.
|
83
|
+
# If the :through option is passed it will go through an association on that instance.
|
84
|
+
# If the :shallow option is passed it will use the resource_class if there's no parent
|
85
|
+
# If the :singleton option is passed it won't use the association because it needs to be handled later.
|
86
|
+
def resource_base
|
87
|
+
@options[:through] ? resource_base_through : resource_class
|
88
|
+
end
|
89
|
+
|
90
|
+
def parent_authorization_action
|
91
|
+
@options[:parent_action] || :show
|
92
|
+
end
|
93
|
+
|
94
|
+
def authorization_action
|
95
|
+
parent? ? parent_authorization_action : @params[:action].to_sym
|
96
|
+
end
|
97
|
+
|
98
|
+
def load_collection
|
99
|
+
resource_base.accessible_by(current_ability, authorization_action)
|
100
|
+
end
|
101
|
+
|
102
|
+
def load_resource_instance
|
103
|
+
if !parent? && new_actions.include?(@params[:action].to_sym)
|
104
|
+
build_resource
|
105
|
+
elsif id_param || @options[:singleton]
|
106
|
+
find_resource
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def extract_key(value)
|
113
|
+
value.to_s.underscore.tr('/', '_')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ControllerResourceNameFinder
|
3
|
+
protected
|
4
|
+
|
5
|
+
def name_from_controller
|
6
|
+
@params[:controller].split('/').last.singularize
|
7
|
+
end
|
8
|
+
|
9
|
+
def namespaced_name
|
10
|
+
[namespace, name].join('/').singularize.camelize.safe_constantize || name
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@name || name_from_controller
|
15
|
+
end
|
16
|
+
|
17
|
+
def namespace
|
18
|
+
@params[:controller].split('/')[0..-2]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ControllerResourceSanitizer
|
3
|
+
protected
|
4
|
+
|
5
|
+
def sanitize_parameters
|
6
|
+
case params_method
|
7
|
+
when Symbol
|
8
|
+
@controller.send(params_method)
|
9
|
+
when String
|
10
|
+
@controller.instance_eval(params_method)
|
11
|
+
when Proc
|
12
|
+
params_method.call(@controller)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def params_methods
|
17
|
+
methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
|
18
|
+
methods.unshift(@options[:param_method]) if @options[:param_method].present?
|
19
|
+
methods
|
20
|
+
end
|
21
|
+
|
22
|
+
def params_method
|
23
|
+
params_methods.each do |method|
|
24
|
+
return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
|
25
|
+
method.is_a?(String) || method.is_a?(Proc)
|
26
|
+
end
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/cancan/exceptions.rb
CHANGED
@@ -11,6 +11,9 @@ module CanCan
|
|
11
11
|
# Raised when using check_authorization without calling authorized!
|
12
12
|
class AuthorizationNotPerformed < Error; end
|
13
13
|
|
14
|
+
# Raised when using a wrong association name
|
15
|
+
class WrongAssociationName < Error; end
|
16
|
+
|
14
17
|
# This error is raised when a user isn't allowed to access a given controller action.
|
15
18
|
# This usually happens within a call to ControllerAdditions#authorize! but can be
|
16
19
|
# raised manually.
|
@@ -33,14 +36,15 @@ module CanCan
|
|
33
36
|
# See ControllerAdditions#authorized! for more information on rescuing from this exception
|
34
37
|
# and customizing the message using I18n.
|
35
38
|
class AccessDenied < Error
|
36
|
-
attr_reader :action, :subject
|
39
|
+
attr_reader :action, :subject, :conditions
|
37
40
|
attr_writer :default_message
|
38
41
|
|
39
|
-
def initialize(message = nil, action = nil, subject = nil)
|
42
|
+
def initialize(message = nil, action = nil, subject = nil, conditions = nil)
|
40
43
|
@message = message
|
41
44
|
@action = action
|
42
45
|
@subject = subject
|
43
|
-
@
|
46
|
+
@conditions = conditions
|
47
|
+
@default_message = I18n.t(:"unauthorized.default", default: 'You are not authorized to access this page.')
|
44
48
|
end
|
45
49
|
|
46
50
|
def to_s
|
data/lib/cancan/matchers.rb
CHANGED
@@ -9,13 +9,22 @@ end
|
|
9
9
|
|
10
10
|
Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
|
11
11
|
match do |ability|
|
12
|
-
|
12
|
+
actions = args.first
|
13
|
+
if actions.is_a? Array
|
14
|
+
break false if actions.empty?
|
15
|
+
actions.all? { |action| ability.can?(action, *args[1..-1]) }
|
16
|
+
else
|
17
|
+
ability.can?(*args)
|
18
|
+
end
|
13
19
|
end
|
14
20
|
|
15
21
|
# Check that RSpec is < 2.99
|
16
22
|
if !respond_to?(:failure_message) && respond_to?(:failure_message_for_should)
|
17
|
-
|
18
|
-
|
23
|
+
alias_method :failure_message, :failure_message_for_should
|
24
|
+
end
|
25
|
+
|
26
|
+
if !respond_to?(:failure_message_when_negated) && respond_to?(:failure_message_for_should_not)
|
27
|
+
alias_method :failure_message_when_negated, :failure_message_for_should_not
|
19
28
|
end
|
20
29
|
|
21
30
|
failure_message do
|
@@ -11,7 +11,7 @@ module CanCan
|
|
11
11
|
end
|
12
12
|
|
13
13
|
# Used to determine if the given adapter should be used for the passed in class.
|
14
|
-
def self.for_class?(
|
14
|
+
def self.for_class?(_member_class)
|
15
15
|
false # override in subclass
|
16
16
|
end
|
17
17
|
|
@@ -22,24 +22,24 @@ module CanCan
|
|
22
22
|
|
23
23
|
# Used to determine if this model adapter will override the matching behavior for a hash of conditions.
|
24
24
|
# If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
|
25
|
-
def self.override_conditions_hash_matching?(
|
25
|
+
def self.override_conditions_hash_matching?(_subject, _conditions)
|
26
26
|
false
|
27
27
|
end
|
28
28
|
|
29
29
|
# Override if override_conditions_hash_matching? returns true
|
30
|
-
def self.matches_conditions_hash?(
|
31
|
-
raise NotImplemented,
|
30
|
+
def self.matches_conditions_hash?(_subject, _conditions)
|
31
|
+
raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
|
32
32
|
end
|
33
33
|
|
34
34
|
# Used to determine if this model adapter will override the matching behavior for a specific condition.
|
35
35
|
# If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
|
36
|
-
def self.override_condition_matching?(
|
36
|
+
def self.override_condition_matching?(_subject, _name, _value)
|
37
37
|
false
|
38
38
|
end
|
39
39
|
|
40
40
|
# Override if override_condition_matching? returns true
|
41
|
-
def self.matches_condition?(
|
42
|
-
raise NotImplemented,
|
41
|
+
def self.matches_condition?(_subject, _name, _value)
|
42
|
+
raise NotImplemented, 'This model adapter does not support matching on a specific condition.'
|
43
43
|
end
|
44
44
|
|
45
45
|
def initialize(model_class, rules)
|
@@ -49,7 +49,7 @@ module CanCan
|
|
49
49
|
|
50
50
|
def database_records
|
51
51
|
# This should be overridden in a subclass to return records which match @rules
|
52
|
-
raise NotImplemented,
|
52
|
+
raise NotImplemented, 'This model adapter does not support fetching records from the database.'
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
@@ -3,7 +3,26 @@ module CanCan
|
|
3
3
|
class ActiveRecord4Adapter < AbstractAdapter
|
4
4
|
include ActiveRecordAdapter
|
5
5
|
def self.for_class?(model_class)
|
6
|
-
model_class <= ActiveRecord::Base
|
6
|
+
ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO: this should be private
|
10
|
+
def self.override_condition_matching?(subject, name, _value)
|
11
|
+
subject.class.defined_enums.include?(name.to_s)
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: this should be private
|
15
|
+
def self.matches_condition?(subject, name, value)
|
16
|
+
# Get the mapping from enum strings to values.
|
17
|
+
enum = subject.class.send(name.to_s.pluralize)
|
18
|
+
# Get the value of the attribute as an integer.
|
19
|
+
attribute = enum[subject.send(name)]
|
20
|
+
# Check to see if the value matches the condition.
|
21
|
+
if value.is_a?(Enumerable)
|
22
|
+
value.include? attribute
|
23
|
+
else
|
24
|
+
attribute == value
|
25
|
+
end
|
7
26
|
end
|
8
27
|
|
9
28
|
private
|
@@ -20,19 +39,23 @@ module CanCan
|
|
20
39
|
|
21
40
|
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
22
41
|
def sanitize_sql(conditions)
|
23
|
-
if ActiveRecord::VERSION::MINOR >= 2 && Hash
|
24
|
-
|
25
|
-
|
26
|
-
conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
|
27
|
-
conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
|
28
|
-
|
29
|
-
ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map { |b|
|
30
|
-
@model_class.send(:connection).visitor.compile b
|
31
|
-
}.join(' AND ')
|
42
|
+
if ActiveRecord::VERSION::MINOR >= 2 && conditions.is_a?(Hash)
|
43
|
+
sanitize_sql_activerecord4(conditions)
|
32
44
|
else
|
33
45
|
@model_class.send(:sanitize_sql, conditions)
|
34
46
|
end
|
35
47
|
end
|
48
|
+
|
49
|
+
def sanitize_sql_activerecord4(conditions)
|
50
|
+
table = Arel::Table.new(@model_class.send(:table_name))
|
51
|
+
|
52
|
+
conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
|
53
|
+
conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
|
54
|
+
|
55
|
+
ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map do |b|
|
56
|
+
@model_class.send(:connection).visitor.compile b
|
57
|
+
end.join(' AND ')
|
58
|
+
end
|
36
59
|
end
|
37
60
|
end
|
38
61
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class ActiveRecord5Adapter < ActiveRecord4Adapter
|
4
|
+
AbstractAdapter.inherited(self)
|
5
|
+
|
6
|
+
def self.for_class?(model_class)
|
7
|
+
ActiveRecord::VERSION::MAJOR == 5 && model_class <= ActiveRecord::Base
|
8
|
+
end
|
9
|
+
|
10
|
+
# rails 5 is capable of using strings in enum
|
11
|
+
# but often people use symbols in rules
|
12
|
+
def self.matches_condition?(subject, name, value)
|
13
|
+
return super if Array.wrap(value).all? { |x| x.is_a? Integer }
|
14
|
+
|
15
|
+
attribute = subject.send(name)
|
16
|
+
if value.is_a?(Enumerable)
|
17
|
+
value.map(&:to_s).include? attribute
|
18
|
+
else
|
19
|
+
attribute == value.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# As of rails 4, `includes()` no longer causes active record to
|
26
|
+
# look inside the where clause to decide to outer join tables
|
27
|
+
# you're using in the where. Instead, `references()` is required
|
28
|
+
# in addition to `includes()` to force the outer join.
|
29
|
+
def build_relation(*where_conditions)
|
30
|
+
relation = @model_class.where(*where_conditions)
|
31
|
+
relation = relation.includes(joins).references(joins) if joins.present?
|
32
|
+
relation
|
33
|
+
end
|
34
|
+
|
35
|
+
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
36
|
+
def sanitize_sql(conditions)
|
37
|
+
if conditions.is_a?(Hash)
|
38
|
+
sanitize_sql_activerecord5(conditions)
|
39
|
+
else
|
40
|
+
@model_class.send(:sanitize_sql, conditions)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def sanitize_sql_activerecord5(conditions)
|
45
|
+
table = @model_class.send(:arel_table)
|
46
|
+
table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
|
47
|
+
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
|
48
|
+
|
49
|
+
conditions = predicate_builder.resolve_column_aliases(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 ')
|
56
|
+
end
|
57
|
+
|
58
|
+
def visit_nodes(b)
|
59
|
+
# Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
|
60
|
+
if ActiveRecord::VERSION::MINOR >= 2
|
61
|
+
connection = @model_class.send(:connection)
|
62
|
+
collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
|
63
|
+
connection.visitor.accept(b, collector).value
|
64
|
+
else
|
65
|
+
@model_class.send(:connection).visitor.compile(b)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -1,6 +1,11 @@
|
|
1
|
+
require_relative 'can_can/model_adapters/active_record_adapter/joins.rb'
|
2
|
+
require_relative 'conditions_extractor.rb'
|
3
|
+
require 'cancan/rules_compressor'
|
1
4
|
module CanCan
|
2
5
|
module ModelAdapters
|
3
6
|
module ActiveRecordAdapter
|
7
|
+
include CanCan::ModelAdapters::ActiveRecordAdapter::Joins
|
8
|
+
|
4
9
|
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
5
10
|
# method directly, but instead go through ModelAdditions#accessible_by.
|
6
11
|
#
|
@@ -17,94 +22,69 @@ module CanCan
|
|
17
22
|
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
18
23
|
#
|
19
24
|
def conditions
|
20
|
-
|
25
|
+
compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
|
26
|
+
conditions_extractor = ConditionsExtractor.new(@model_class)
|
27
|
+
if compressed_rules.size == 1 && compressed_rules.first.base_behavior
|
21
28
|
# Return the conditions directly if there's just one definition
|
22
|
-
|
29
|
+
conditions_extractor.tableize_conditions(compressed_rules.first.conditions).dup
|
23
30
|
else
|
24
|
-
|
25
|
-
merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
|
26
|
-
end
|
31
|
+
extract_multiple_conditions(conditions_extractor, compressed_rules)
|
27
32
|
end
|
28
33
|
end
|
29
34
|
|
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
|
35
|
+
def extract_multiple_conditions(conditions_extractor, rules)
|
36
|
+
rules.reverse.inject(false_sql) do |sql, rule|
|
37
|
+
merge_conditions(sql, conditions_extractor.tableize_conditions(rule.conditions).dup, rule.base_behavior)
|
50
38
|
end
|
51
39
|
end
|
52
40
|
|
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
41
|
def database_records
|
64
42
|
if override_scope
|
65
43
|
@model_class.where(nil).merge(override_scope)
|
66
44
|
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
|
45
|
+
mergeable_conditions? ? build_relation(conditions) : build_relation(*@rules.map(&:conditions))
|
72
46
|
else
|
73
|
-
@model_class.all(:
|
47
|
+
@model_class.all(conditions: conditions, joins: joins)
|
74
48
|
end
|
75
49
|
end
|
76
50
|
|
77
51
|
private
|
78
52
|
|
79
53
|
def mergeable_conditions?
|
80
|
-
@rules.find
|
54
|
+
@rules.find(&:unmergeable?).blank?
|
81
55
|
end
|
82
56
|
|
83
57
|
def override_scope
|
84
58
|
conditions = @rules.map(&:conditions).compact
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
59
|
+
return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
|
60
|
+
return conditions.first if conditions.size == 1
|
61
|
+
raise_override_scope_error
|
62
|
+
end
|
63
|
+
|
64
|
+
def raise_override_scope_error
|
65
|
+
rule_found = @rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
|
66
|
+
raise Error,
|
67
|
+
'Unable to merge an Active Record scope with other conditions. '\
|
68
|
+
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
93
69
|
end
|
94
70
|
|
95
71
|
def merge_conditions(sql, conditions_hash, behavior)
|
96
72
|
if conditions_hash.blank?
|
97
73
|
behavior ? true_sql : false_sql
|
98
74
|
else
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
75
|
+
merge_non_empty_conditions(behavior, conditions_hash, sql)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def merge_non_empty_conditions(behavior, conditions_hash, sql)
|
80
|
+
conditions = sanitize_sql(conditions_hash)
|
81
|
+
case sql
|
82
|
+
when true_sql
|
83
|
+
behavior ? true_sql : "not (#{conditions})"
|
84
|
+
when false_sql
|
85
|
+
behavior ? conditions : false_sql
|
86
|
+
else
|
87
|
+
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
|
108
88
|
end
|
109
89
|
end
|
110
90
|
|
@@ -119,30 +99,10 @@ module CanCan
|
|
119
99
|
def sanitize_sql(conditions)
|
120
100
|
@model_class.send(:sanitize_sql, conditions)
|
121
101
|
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
102
|
end
|
143
103
|
end
|
144
104
|
end
|
145
105
|
|
146
|
-
|
147
|
-
include CanCan::ModelAdditions
|
106
|
+
ActiveSupport.on_load(:active_record) do
|
107
|
+
send :include, CanCan::ModelAdditions
|
148
108
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
module ActiveRecordAdapter
|
4
|
+
module Joins
|
5
|
+
# Returns the associations used in conditions for the :joins option of a search.
|
6
|
+
# See ModelAdditions#accessible_by
|
7
|
+
def joins
|
8
|
+
joins_hash = {}
|
9
|
+
@rules.reverse.each do |rule|
|
10
|
+
merge_joins(joins_hash, rule.associations_hash)
|
11
|
+
end
|
12
|
+
clean_joins(joins_hash) unless joins_hash.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Removes empty hashes and moves everything into arrays.
|
18
|
+
def clean_joins(joins_hash)
|
19
|
+
joins = []
|
20
|
+
joins_hash.each do |name, nested|
|
21
|
+
joins << (nested.empty? ? name : { name => clean_joins(nested) })
|
22
|
+
end
|
23
|
+
joins
|
24
|
+
end
|
25
|
+
|
26
|
+
# Takes two hashes and does a deep merge.
|
27
|
+
def merge_joins(base, add)
|
28
|
+
add.each do |name, nested|
|
29
|
+
if base[name].is_a?(Hash)
|
30
|
+
merge_joins(base[name], nested) unless nested.empty?
|
31
|
+
else
|
32
|
+
base[name] = nested
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|