cancancan 1.15.0 → 1.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +38 -0
- data/.rubocop_todo.yml +48 -0
- data/.travis.yml +8 -2
- data/Appraisals +1 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +1 -1
- data/README.md +58 -41
- data/Rakefile +7 -3
- data/cancancan.gemspec +13 -12
- data/gemfiles/activerecord_4.2.gemfile +1 -0
- data/lib/cancan.rb +2 -2
- data/lib/cancan/ability.rb +26 -24
- data/lib/cancan/controller_additions.rb +33 -23
- data/lib/cancan/controller_resource.rb +83 -56
- data/lib/cancan/exceptions.rb +1 -1
- data/lib/cancan/matchers.rb +2 -2
- data/lib/cancan/model_adapters/abstract_adapter.rb +8 -8
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +48 -35
- data/lib/cancan/model_adapters/active_record_adapter.rb +18 -17
- data/lib/cancan/model_adapters/mongoid_adapter.rb +26 -21
- data/lib/cancan/model_adapters/sequel_adapter.rb +12 -12
- data/lib/cancan/model_additions.rb +0 -1
- data/lib/cancan/rule.rb +23 -17
- data/lib/cancan/version.rb +1 -1
- data/lib/generators/cancan/ability/ability_generator.rb +1 -1
- data/spec/cancan/ability_spec.rb +189 -180
- data/spec/cancan/controller_additions_spec.rb +77 -64
- data/spec/cancan/controller_resource_spec.rb +230 -228
- data/spec/cancan/exceptions_spec.rb +20 -20
- data/spec/cancan/inherited_resource_spec.rb +21 -21
- data/spec/cancan/matchers_spec.rb +12 -12
- data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +38 -32
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +155 -145
- data/spec/cancan/model_adapters/default_adapter_spec.rb +2 -2
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +87 -88
- data/spec/cancan/model_adapters/sequel_adapter_spec.rb +44 -47
- data/spec/cancan/rule_spec.rb +18 -18
- data/spec/spec_helper.rb +2 -2
- data/spec/support/ability.rb +0 -1
- metadata +60 -19
@@ -6,62 +6,75 @@ module CanCan
|
|
6
6
|
model_class <= ActiveRecord::Base
|
7
7
|
end
|
8
8
|
|
9
|
-
private
|
10
|
-
|
11
|
-
# As of rails 4, `includes()` no longer causes active record to
|
12
|
-
# look inside the where clause to decide to outer join tables
|
13
|
-
# you're using in the where. Instead, `references()` is required
|
14
|
-
# 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
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.override_condition_matching?(subject, name, value)
|
9
|
+
# TODO: this should be private
|
10
|
+
def self.override_condition_matching?(subject, name, _value)
|
22
11
|
# ActiveRecord introduced enums in version 4.1.
|
23
12
|
(ActiveRecord::VERSION::MAJOR > 4 || ActiveRecord::VERSION::MINOR >= 1) &&
|
24
13
|
subject.class.defined_enums.include?(name.to_s)
|
25
14
|
end
|
26
15
|
|
16
|
+
# TODO: this should be private
|
27
17
|
def self.matches_condition?(subject, name, value)
|
28
18
|
# Get the mapping from enum strings to values.
|
29
19
|
enum = subject.class.send(name.to_s.pluralize)
|
30
20
|
# Get the value of the attribute as an integer.
|
31
21
|
attribute = enum[subject.send(name)]
|
32
22
|
# Check to see if the value matches the condition.
|
33
|
-
value.is_a?(Enumerable)
|
34
|
-
|
23
|
+
if value.is_a?(Enumerable)
|
24
|
+
value.include? attribute
|
25
|
+
else
|
35
26
|
attribute == value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# As of rails 4, `includes()` no longer causes active record to
|
33
|
+
# look inside the where clause to decide to outer join tables
|
34
|
+
# you're using in the where. Instead, `references()` is required
|
35
|
+
# in addition to `includes()` to force the outer join.
|
36
|
+
def build_relation(*where_conditions)
|
37
|
+
relation = @model_class.where(*where_conditions)
|
38
|
+
relation = relation.includes(joins).references(joins) if joins.present?
|
39
|
+
relation
|
36
40
|
end
|
37
41
|
|
38
42
|
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
39
43
|
def sanitize_sql(conditions)
|
40
|
-
if ActiveRecord::VERSION::MAJOR > 4 && Hash
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
+
if ActiveRecord::VERSION::MAJOR > 4 && conditions.is_a?(Hash)
|
45
|
+
sanitize_sql_activerecord5(conditions)
|
46
|
+
elsif ActiveRecord::VERSION::MINOR >= 2 && conditions.is_a?(Hash)
|
47
|
+
sanitize_sql_activerecord4(conditions)
|
48
|
+
|
49
|
+
else
|
50
|
+
@model_class.send(:sanitize_sql, conditions)
|
51
|
+
end
|
52
|
+
end
|
44
53
|
|
45
|
-
|
46
|
-
|
54
|
+
def sanitize_sql_activerecord4(conditions)
|
55
|
+
table = Arel::Table.new(@model_class.send(:table_name))
|
47
56
|
|
48
|
-
|
57
|
+
conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
|
58
|
+
conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
|
49
59
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
table = Arel::Table.new(@model_class.send(:table_name))
|
60
|
+
ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map do |b|
|
61
|
+
@model_class.send(:connection).visitor.compile b
|
62
|
+
end.join(' AND ')
|
63
|
+
end
|
55
64
|
|
56
|
-
|
57
|
-
|
65
|
+
def sanitize_sql_activerecord5(conditions)
|
66
|
+
table = @model_class.send(:arel_table)
|
67
|
+
table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
|
68
|
+
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
|
58
69
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
70
|
+
conditions = predicate_builder.resolve_column_aliases(conditions)
|
71
|
+
conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
|
72
|
+
|
73
|
+
conditions.stringify_keys!
|
74
|
+
|
75
|
+
predicate_builder.build_from_hash(conditions).map do |b|
|
76
|
+
@model_class.send(:connection).visitor.compile b
|
77
|
+
end.join(' AND ')
|
65
78
|
end
|
66
79
|
end
|
67
80
|
end
|
@@ -28,13 +28,13 @@ module CanCan
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def tableized_conditions(conditions, model_class = @model_class)
|
31
|
-
return conditions unless conditions.
|
32
|
-
conditions.
|
33
|
-
if value.
|
31
|
+
return conditions unless conditions.is_a? Hash
|
32
|
+
conditions.each_with_object({}) do |(name, value), result_hash|
|
33
|
+
if value.is_a? Hash
|
34
34
|
value = value.dup
|
35
35
|
association_class = model_class.reflect_on_association(name).klass.name.constantize
|
36
|
-
|
37
|
-
if v.
|
36
|
+
nested_resulted = value.each_with_object({}) do |(k, v), nested|
|
37
|
+
if v.is_a? Hash
|
38
38
|
value.delete(k)
|
39
39
|
nested[k] = v
|
40
40
|
else
|
@@ -42,7 +42,7 @@ module CanCan
|
|
42
42
|
end
|
43
43
|
nested
|
44
44
|
end
|
45
|
-
result_hash.merge!(tableized_conditions(
|
45
|
+
result_hash.merge!(tableized_conditions(nested_resulted, association_class))
|
46
46
|
else
|
47
47
|
result_hash[name] = value
|
48
48
|
end
|
@@ -67,28 +67,29 @@ module CanCan
|
|
67
67
|
if mergeable_conditions?
|
68
68
|
build_relation(conditions)
|
69
69
|
else
|
70
|
-
build_relation(
|
70
|
+
build_relation(*@rules.map(&:conditions))
|
71
71
|
end
|
72
72
|
else
|
73
|
-
@model_class.all(:
|
73
|
+
@model_class.all(conditions: conditions, joins: joins)
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
77
|
private
|
78
78
|
|
79
79
|
def mergeable_conditions?
|
80
|
-
@rules.find
|
80
|
+
@rules.find(&:unmergeable?).blank?
|
81
81
|
end
|
82
82
|
|
83
83
|
def override_scope
|
84
84
|
conditions = @rules.map(&:conditions).compact
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
return unless defined?(ActiveRecord::Relation) && conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
|
86
|
+
if conditions.size == 1
|
87
|
+
conditions.first
|
88
|
+
else
|
89
|
+
rule_found = @rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
|
90
|
+
raise Error,
|
91
|
+
'Unable to merge an Active Record scope with other conditions. '\
|
92
|
+
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
92
93
|
end
|
93
94
|
end
|
94
95
|
|
@@ -135,7 +136,7 @@ module CanCan
|
|
135
136
|
def clean_joins(joins_hash)
|
136
137
|
joins = []
|
137
138
|
joins_hash.each do |name, nested|
|
138
|
-
joins << (nested.empty? ? name : {name => clean_joins(nested)})
|
139
|
+
joins << (nested.empty? ? name : { name => clean_joins(nested) })
|
139
140
|
end
|
140
141
|
joins
|
141
142
|
end
|
@@ -6,8 +6,8 @@ module CanCan
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def self.override_conditions_hash_matching?(subject, conditions)
|
9
|
-
conditions.any? do |k,
|
10
|
-
key_is_not_symbol =
|
9
|
+
conditions.any? do |k, _v|
|
10
|
+
key_is_not_symbol = -> { !k.is_a?(Symbol) }
|
11
11
|
subject_value_is_array = lambda do
|
12
12
|
subject.respond_to?(k) && subject.send(k).is_a?(Array)
|
13
13
|
end
|
@@ -19,50 +19,55 @@ module CanCan
|
|
19
19
|
def self.matches_conditions_hash?(subject, conditions)
|
20
20
|
# To avoid hitting the db, retrieve the raw Mongo selector from
|
21
21
|
# the Mongoid Criteria and use Mongoid::Matchers#matches?
|
22
|
-
subject.matches?(
|
22
|
+
subject.matches?(subject.class.where(conditions).selector)
|
23
23
|
end
|
24
24
|
|
25
25
|
def database_records
|
26
|
-
if @rules.
|
27
|
-
@model_class.where(:
|
26
|
+
if @rules.empty?
|
27
|
+
@model_class.where(_id: { '$exists' => false, '$type' => 7 }) # return no records in Mongoid
|
28
28
|
elsif @rules.size == 1 && @rules[0].conditions.is_a?(Mongoid::Criteria)
|
29
29
|
@rules[0].conditions
|
30
30
|
else
|
31
31
|
# we only need to process can rules if
|
32
32
|
# there are no rules with empty conditions
|
33
|
-
|
34
|
-
|
33
|
+
database_records_from_multiple_rules
|
34
|
+
end
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
37
|
+
def database_records_from_multiple_rules
|
38
|
+
rules = @rules.reject { |rule| rule.conditions.empty? && rule.base_behavior }
|
39
|
+
process_can_rules = @rules.count == rules.count
|
40
|
+
|
41
|
+
rules.inject(@model_class.all) do |records, rule|
|
42
|
+
if process_can_rules && rule.base_behavior
|
43
|
+
records.or simplify_relations(@model_class, rule.conditions)
|
44
|
+
elsif !rule.base_behavior
|
45
|
+
records.excludes simplify_relations(@model_class, rule.conditions)
|
46
|
+
else
|
47
|
+
records
|
44
48
|
end
|
45
49
|
end
|
46
50
|
end
|
47
51
|
|
48
52
|
private
|
53
|
+
|
49
54
|
# Look for criteria on relations and replace with simple id queries
|
50
55
|
# eg.
|
51
56
|
# {user: {:tags.all => []}} becomes {"user_id" => {"$in" => [__, ..]}}
|
52
57
|
# {user: {:session => {:tags.all => []}}} becomes {"user_id" => {"session_id" => {"$in" => [__, ..]} }}
|
53
|
-
def simplify_relations
|
58
|
+
def simplify_relations(model_class, conditions)
|
54
59
|
model_relations = model_class.relations.with_indifferent_access
|
55
60
|
Hash[
|
56
|
-
conditions.map
|
57
|
-
if relation = model_relations[k]
|
61
|
+
conditions.map do |k, v|
|
62
|
+
if (relation = model_relations[k])
|
58
63
|
relation_class_name = relation[:class_name].blank? ? k.to_s.classify : relation[:class_name]
|
59
64
|
v = simplify_relations(relation_class_name.constantize, v)
|
60
65
|
relation_ids = relation_class_name.constantize.where(v).only(:id).map(&:id)
|
61
66
|
k = "#{k}_id"
|
62
|
-
v = {
|
67
|
+
v = { '$in' => relation_ids }
|
63
68
|
end
|
64
|
-
[k,v]
|
65
|
-
|
69
|
+
[k, v]
|
70
|
+
end
|
66
71
|
]
|
67
72
|
end
|
68
73
|
end
|
@@ -9,8 +9,8 @@ module CanCan
|
|
9
9
|
model_class[id]
|
10
10
|
end
|
11
11
|
|
12
|
-
def self.override_condition_matching?(
|
13
|
-
value.
|
12
|
+
def self.override_condition_matching?(_subject, _name, value)
|
13
|
+
value.is_a?(Hash)
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.matches_condition?(subject, name, value)
|
@@ -19,8 +19,8 @@ module CanCan
|
|
19
19
|
false
|
20
20
|
else
|
21
21
|
value.each do |k, v|
|
22
|
-
if v.
|
23
|
-
return false unless
|
22
|
+
if v.is_a?(Hash)
|
23
|
+
return false unless matches_condition?(obj, k, v)
|
24
24
|
elsif obj.send(k) != v
|
25
25
|
return false
|
26
26
|
end
|
@@ -29,12 +29,12 @@ module CanCan
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def database_records
|
32
|
-
if @rules.
|
32
|
+
if @rules.empty?
|
33
33
|
@model_class.where('1=0')
|
34
34
|
else
|
35
35
|
# only need to process can rules if there are no can rule with empty conditions
|
36
36
|
rules = @rules.reject { |rule| rule.base_behavior && rule.conditions.empty? }
|
37
|
-
rules.reject!
|
37
|
+
rules.reject!(&:base_behavior) if rules.count < @rules.count
|
38
38
|
|
39
39
|
can_condition_added = false
|
40
40
|
rules.reverse.inject(@model_class.dataset) do |records, rule|
|
@@ -56,13 +56,13 @@ module CanCan
|
|
56
56
|
private
|
57
57
|
|
58
58
|
def normalize_conditions(conditions, model_class = @model_class)
|
59
|
-
return conditions unless conditions.
|
60
|
-
conditions.
|
61
|
-
if value.
|
59
|
+
return conditions unless conditions.is_a? Hash
|
60
|
+
conditions.each_with_object({}) do |(name, value), result_hash|
|
61
|
+
if value.is_a? Hash
|
62
62
|
value = value.dup
|
63
63
|
association_class = model_class.association_reflection(name).associated_class
|
64
|
-
|
65
|
-
if v.
|
64
|
+
nested_resulted = value.each_with_object({}) do |(k, v), nested|
|
65
|
+
if v.is_a?(Hash)
|
66
66
|
value.delete(k)
|
67
67
|
nested_class = association_class.association_reflection(k).associated_class
|
68
68
|
nested[k] = nested_class.where(normalize_conditions(v, association_class))
|
@@ -71,7 +71,7 @@ module CanCan
|
|
71
71
|
end
|
72
72
|
nested
|
73
73
|
end
|
74
|
-
result_hash[name] = association_class.where(
|
74
|
+
result_hash[name] = association_class.where(nested_resulted)
|
75
75
|
else
|
76
76
|
result_hash[name] = value
|
77
77
|
end
|
data/lib/cancan/rule.rb
CHANGED
@@ -11,7 +11,9 @@ module CanCan
|
|
11
11
|
# and subject respectively (such as :read, @project). The third argument is a hash
|
12
12
|
# of conditions and the last one is the block passed to the "can" call.
|
13
13
|
def initialize(base_behavior, action, subject, conditions, block)
|
14
|
-
|
14
|
+
both_block_and_hash_error = 'You are not able to supply a block with a hash of conditions in '\
|
15
|
+
"#{action} #{subject} ability. Use either one."
|
16
|
+
raise Error, both_block_and_hash_error if conditions.is_a?(Hash) && block
|
15
17
|
@match_all = action.nil? && subject.nil?
|
16
18
|
@base_behavior = base_behavior
|
17
19
|
@actions = [action].flatten
|
@@ -32,9 +34,9 @@ module CanCan
|
|
32
34
|
call_block_with_all(action, subject, extra_args)
|
33
35
|
elsif @block && !subject_class?(subject)
|
34
36
|
@block.call(subject, *extra_args)
|
35
|
-
elsif @conditions.
|
37
|
+
elsif @conditions.is_a?(Hash) && subject.class == Hash
|
36
38
|
nested_subject_matches_conditions?(subject)
|
37
|
-
elsif @conditions.
|
39
|
+
elsif @conditions.is_a?(Hash) && !subject_class?(subject)
|
38
40
|
matches_conditions_hash?(subject)
|
39
41
|
else
|
40
42
|
# Don't stop at "cannot" definitions when there are conditions.
|
@@ -43,11 +45,11 @@ module CanCan
|
|
43
45
|
end
|
44
46
|
|
45
47
|
def only_block?
|
46
|
-
conditions_empty? &&
|
48
|
+
conditions_empty? && @block
|
47
49
|
end
|
48
50
|
|
49
51
|
def only_raw_sql?
|
50
|
-
@block.nil? && !conditions_empty? && !@conditions.
|
52
|
+
@block.nil? && !conditions_empty? && !@conditions.is_a?(Hash)
|
51
53
|
end
|
52
54
|
|
53
55
|
def conditions_empty?
|
@@ -56,29 +58,33 @@ module CanCan
|
|
56
58
|
|
57
59
|
def unmergeable?
|
58
60
|
@conditions.respond_to?(:keys) && @conditions.present? &&
|
59
|
-
(!@conditions.keys.first.
|
61
|
+
(!@conditions.keys.first.is_a? Symbol)
|
60
62
|
end
|
61
63
|
|
62
64
|
def associations_hash(conditions = @conditions)
|
63
65
|
hash = {}
|
64
|
-
conditions.
|
65
|
-
|
66
|
-
|
66
|
+
if conditions.is_a? Hash
|
67
|
+
conditions.map do |name, value|
|
68
|
+
hash[name] = associations_hash(value) if value.is_a? Hash
|
69
|
+
end
|
70
|
+
end
|
67
71
|
hash
|
68
72
|
end
|
69
73
|
|
70
74
|
def attributes_from_conditions
|
71
75
|
attributes = {}
|
72
|
-
@conditions.
|
73
|
-
|
74
|
-
|
76
|
+
if @conditions.is_a? Hash
|
77
|
+
@conditions.each do |key, value|
|
78
|
+
attributes[key] = value unless [Array, Range, Hash].include? value.class
|
79
|
+
end
|
80
|
+
end
|
75
81
|
attributes
|
76
82
|
end
|
77
83
|
|
78
84
|
private
|
79
85
|
|
80
86
|
def subject_class?(subject)
|
81
|
-
klass = (subject.
|
87
|
+
klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
|
82
88
|
klass == Class || klass == Module
|
83
89
|
end
|
84
90
|
|
@@ -92,9 +98,9 @@ module CanCan
|
|
92
98
|
|
93
99
|
def matches_subject_class?(subject)
|
94
100
|
@subjects.any? do |sub|
|
95
|
-
sub.
|
101
|
+
sub.is_a?(Module) && (subject.is_a?(sub) ||
|
96
102
|
subject.class.to_s == sub.to_s ||
|
97
|
-
(subject.
|
103
|
+
(subject.is_a?(Module) && subject.ancestors.include?(sub)))
|
98
104
|
end
|
99
105
|
end
|
100
106
|
|
@@ -147,10 +153,10 @@ module CanCan
|
|
147
153
|
end
|
148
154
|
|
149
155
|
def hash_condition_match?(attribute, value)
|
150
|
-
if attribute.
|
156
|
+
if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
|
151
157
|
attribute.any? { |element| matches_conditions_hash?(element, value) }
|
152
158
|
else
|
153
|
-
|
159
|
+
attribute && matches_conditions_hash?(attribute, value)
|
154
160
|
end
|
155
161
|
end
|
156
162
|
end
|
data/lib/cancan/version.rb
CHANGED