cancancan 1.13.1 → 3.1.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 +18 -18
- data/init.rb +2 -0
- data/lib/cancan.rb +9 -11
- data/lib/cancan/ability.rb +93 -194
- data/lib/cancan/ability/actions.rb +93 -0
- data/lib/cancan/ability/rules.rb +93 -0
- data/lib/cancan/ability/strong_parameter_support.rb +41 -0
- data/lib/cancan/conditions_matcher.rb +106 -0
- data/lib/cancan/controller_additions.rb +38 -41
- data/lib/cancan/controller_resource.rb +52 -211
- 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 +17 -5
- data/lib/cancan/matchers.rb +12 -3
- data/lib/cancan/model_adapters/abstract_adapter.rb +10 -8
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +39 -13
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +68 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +77 -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_additions.rb +2 -1
- data/lib/cancan/parameter_validators.rb +9 -0
- data/lib/cancan/relevant.rb +29 -0
- data/lib/cancan/rule.rb +76 -105
- data/lib/cancan/rules_compressor.rb +23 -0
- data/lib/cancan/unauthorized_message_resolver.rb +24 -0
- data/lib/cancan/version.rb +3 -1
- data/lib/cancancan.rb +2 -0
- data/lib/generators/cancan/ability/ability_generator.rb +4 -2
- data/lib/generators/cancan/ability/templates/ability.rb +2 -0
- metadata +66 -56
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.travis.yml +0 -28
- data/Appraisals +0 -81
- data/CHANGELOG.rdoc +0 -518
- data/CONTRIBUTING.md +0 -23
- data/Gemfile +0 -3
- data/LICENSE +0 -22
- data/README.md +0 -214
- data/Rakefile +0 -9
- data/gemfiles/activerecord_3.2.gemfile +0 -16
- 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/mongoid_2.x.gemfile +0 -16
- data/gemfiles/sequel_3.x.gemfile +0 -16
- data/lib/cancan/inherited_resource.rb +0 -20
- data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -16
- 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 -521
- 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 -384
- 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
@@ -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 bahavior 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 alredy_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 alredy_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 alredy_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 thorugh 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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CanCan
|
4
|
+
module Relevant
|
5
|
+
# Matches both the action, subject, and attribute, not necessarily the conditions
|
6
|
+
def relevant?(action, subject)
|
7
|
+
subject = subject.values.first if subject.class == Hash
|
8
|
+
@match_all || (matches_action?(action) && matches_subject?(subject))
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def matches_action?(action)
|
14
|
+
@expanded_actions.include?(:manage) || @expanded_actions.include?(action)
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches_subject?(subject)
|
18
|
+
@subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
|
19
|
+
end
|
20
|
+
|
21
|
+
def matches_subject_class?(subject)
|
22
|
+
@subjects.any? do |sub|
|
23
|
+
sub.is_a?(Module) && (subject.is_a?(sub) ||
|
24
|
+
subject.class.to_s == sub.to_s ||
|
25
|
+
(subject.is_a?(Module) && subject.ancestors.include?(sub)))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/cancan/rule.rb
CHANGED
@@ -1,155 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'conditions_matcher.rb'
|
4
|
+
require_relative 'relevant.rb'
|
5
|
+
|
1
6
|
module CanCan
|
2
7
|
# This class is used internally and should only be called through Ability.
|
3
8
|
# it holds the information about a "can" call made on Ability and provides
|
4
9
|
# helpful methods to determine permission checking and conditions hash generation.
|
5
10
|
class Rule # :nodoc:
|
6
|
-
|
7
|
-
|
11
|
+
include ConditionsMatcher
|
12
|
+
include Relevant
|
13
|
+
include ParameterValidators
|
14
|
+
attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes
|
15
|
+
attr_writer :expanded_actions, :conditions
|
8
16
|
|
9
17
|
# The first argument when initializing is the base_behavior which is a true/false
|
10
18
|
# value. True for "can" and false for "cannot". The next two arguments are the action
|
11
19
|
# and subject respectively (such as :read, @project). The third argument is a hash
|
12
20
|
# of conditions and the last one is the block passed to the "can" call.
|
13
|
-
def initialize(base_behavior, action, subject,
|
14
|
-
|
21
|
+
def initialize(base_behavior, action, subject, *extra_args, &block)
|
22
|
+
# for backwards compatibility, attributes are an optional parameter. Check if
|
23
|
+
# attributes were passed or are actually conditions
|
24
|
+
attributes, extra_args = parse_attributes_from_extra_args(extra_args)
|
25
|
+
condition_and_block_check(extra_args, block, action, subject)
|
15
26
|
@match_all = action.nil? && subject.nil?
|
27
|
+
raise Error, "Subject is required for #{action}" if action && subject.nil?
|
28
|
+
|
16
29
|
@base_behavior = base_behavior
|
17
|
-
@actions =
|
18
|
-
@subjects =
|
19
|
-
@
|
30
|
+
@actions = wrap(action)
|
31
|
+
@subjects = wrap(subject)
|
32
|
+
@attributes = wrap(attributes)
|
33
|
+
@conditions = extra_args || {}
|
20
34
|
@block = block
|
21
35
|
end
|
22
36
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@match_all || (matches_action?(action) && matches_subject?(subject))
|
27
|
-
end
|
37
|
+
def inspect
|
38
|
+
repr = "#<#{self.class.name}"
|
39
|
+
repr += "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}"
|
28
40
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
elsif @block && !subject_class?(subject)
|
34
|
-
@block.call(subject, *extra_args)
|
35
|
-
elsif @conditions.kind_of?(Hash) && subject.class == Hash
|
36
|
-
nested_subject_matches_conditions?(subject)
|
37
|
-
elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
|
38
|
-
matches_conditions_hash?(subject)
|
39
|
-
else
|
40
|
-
# Don't stop at "cannot" definitions when there are conditions.
|
41
|
-
conditions_empty? ? true : @base_behavior
|
41
|
+
if with_scope?
|
42
|
+
repr += ", #{@conditions.where_values_hash}"
|
43
|
+
elsif [Hash, String].include?(@conditions.class)
|
44
|
+
repr += ", #{@conditions.inspect}"
|
42
45
|
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def only_block?
|
46
|
-
conditions_empty? && !@block.nil?
|
47
|
-
end
|
48
|
-
|
49
|
-
def only_raw_sql?
|
50
|
-
@block.nil? && !conditions_empty? && !@conditions.kind_of?(Hash)
|
51
|
-
end
|
52
46
|
|
53
|
-
|
54
|
-
@conditions == {} || @conditions.nil?
|
47
|
+
repr + '>'
|
55
48
|
end
|
56
49
|
|
57
|
-
def
|
58
|
-
|
59
|
-
(!@conditions.keys.first.kind_of? Symbol)
|
50
|
+
def can_rule?
|
51
|
+
base_behavior
|
60
52
|
end
|
61
53
|
|
62
|
-
def
|
63
|
-
|
64
|
-
conditions.map do |name, value|
|
65
|
-
hash[name] = associations_hash(value) if value.kind_of? Hash
|
66
|
-
end if conditions.kind_of? Hash
|
67
|
-
hash
|
54
|
+
def cannot_catch_all?
|
55
|
+
!can_rule? && catch_all?
|
68
56
|
end
|
69
57
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
attributes[key] = value unless [Array, Range, Hash].include? value.class
|
74
|
-
end if @conditions.kind_of? Hash
|
75
|
-
attributes
|
58
|
+
def catch_all?
|
59
|
+
(with_scope? && @conditions.where_values_hash.empty?) ||
|
60
|
+
(!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions))
|
76
61
|
end
|
77
62
|
|
78
|
-
|
79
|
-
|
80
|
-
def subject_class?(subject)
|
81
|
-
klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
|
82
|
-
klass == Class || klass == Module
|
63
|
+
def only_block?
|
64
|
+
conditions_empty? && @block
|
83
65
|
end
|
84
66
|
|
85
|
-
def
|
86
|
-
@
|
67
|
+
def only_raw_sql?
|
68
|
+
@block.nil? && !conditions_empty? && !@conditions.is_a?(Hash)
|
87
69
|
end
|
88
70
|
|
89
|
-
def
|
90
|
-
@
|
71
|
+
def with_scope?
|
72
|
+
@conditions.is_a?(ActiveRecord::Relation)
|
91
73
|
end
|
92
74
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
75
|
+
def associations_hash(conditions = @conditions)
|
76
|
+
hash = {}
|
77
|
+
if conditions.is_a? Hash
|
78
|
+
conditions.map do |name, value|
|
79
|
+
hash[name] = associations_hash(value) if value.is_a? Hash
|
80
|
+
end
|
98
81
|
end
|
82
|
+
hash
|
99
83
|
end
|
100
84
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
return true if conditions.empty?
|
107
|
-
adapter = model_adapter(subject)
|
108
|
-
|
109
|
-
if adapter.override_conditions_hash_matching?(subject, conditions)
|
110
|
-
return adapter.matches_conditions_hash?(subject, conditions)
|
111
|
-
end
|
112
|
-
|
113
|
-
conditions.all? do |name, value|
|
114
|
-
if adapter.override_condition_matching?(subject, name, value)
|
115
|
-
return adapter.matches_condition?(subject, name, value)
|
85
|
+
def attributes_from_conditions
|
86
|
+
attributes = {}
|
87
|
+
if @conditions.is_a? Hash
|
88
|
+
@conditions.each do |key, value|
|
89
|
+
attributes[key] = value unless [Array, Range, Hash].include? value.class
|
116
90
|
end
|
117
|
-
|
118
|
-
condition_match?(subject.send(name), value)
|
119
91
|
end
|
92
|
+
attributes
|
120
93
|
end
|
121
94
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
end
|
95
|
+
def matches_attributes?(attribute)
|
96
|
+
return true if @attributes.empty?
|
97
|
+
return @base_behavior if attribute.nil?
|
126
98
|
|
127
|
-
|
128
|
-
if subject.class == Class
|
129
|
-
@block.call(action, subject, nil, *extra_args)
|
130
|
-
else
|
131
|
-
@block.call(action, subject.class, subject, *extra_args)
|
132
|
-
end
|
99
|
+
@attributes.include?(attribute.to_sym)
|
133
100
|
end
|
134
101
|
|
135
|
-
|
136
|
-
|
102
|
+
private
|
103
|
+
|
104
|
+
def parse_attributes_from_extra_args(args)
|
105
|
+
attributes = args.shift if valid_attribute_param?(args.first)
|
106
|
+
extra_args = args.shift
|
107
|
+
[attributes, extra_args]
|
137
108
|
end
|
138
109
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
else attribute == value
|
145
|
-
end
|
110
|
+
def condition_and_block_check(conditions, block, action, subject)
|
111
|
+
return unless conditions.is_a?(Hash) && block
|
112
|
+
|
113
|
+
raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\
|
114
|
+
"Check \":#{action} #{subject}\" ability."
|
146
115
|
end
|
147
116
|
|
148
|
-
def
|
149
|
-
if
|
150
|
-
|
117
|
+
def wrap(object)
|
118
|
+
if object.nil?
|
119
|
+
[]
|
120
|
+
elsif object.respond_to?(:to_ary)
|
121
|
+
object.to_ary || [object]
|
151
122
|
else
|
152
|
-
|
123
|
+
[object]
|
153
124
|
end
|
154
125
|
end
|
155
126
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'conditions_matcher.rb'
|
4
|
+
module CanCan
|
5
|
+
class RulesCompressor
|
6
|
+
attr_reader :initial_rules, :rules_collapsed
|
7
|
+
|
8
|
+
def initialize(rules)
|
9
|
+
@initial_rules = rules
|
10
|
+
@rules_collapsed = compress(@initial_rules)
|
11
|
+
end
|
12
|
+
|
13
|
+
def compress(array)
|
14
|
+
idx = array.rindex(&:catch_all?)
|
15
|
+
return array unless idx
|
16
|
+
|
17
|
+
value = array[idx]
|
18
|
+
array[idx..-1]
|
19
|
+
.drop_while { |n| n.base_behavior == value.base_behavior }
|
20
|
+
.tap { |a| a.unshift(value) unless value.cannot_catch_all? }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CanCan
|
4
|
+
module UnauthorizedMessageResolver
|
5
|
+
def unauthorized_message(action, subject)
|
6
|
+
subject = subject.values.last if subject.is_a?(Hash)
|
7
|
+
keys = unauthorized_message_keys(action, subject)
|
8
|
+
variables = {}
|
9
|
+
variables[:action] = I18n.translate("actions.#{action}", default: action.to_s)
|
10
|
+
variables[:subject] = translate_subject(subject)
|
11
|
+
message = I18n.translate(keys.shift, **variables.merge(scope: :unauthorized, default: keys + ['']))
|
12
|
+
message.blank? ? nil : message
|
13
|
+
end
|
14
|
+
|
15
|
+
def translate_subject(subject)
|
16
|
+
klass = (subject.class == Class ? subject : subject.class)
|
17
|
+
if klass.respond_to?(:model_name)
|
18
|
+
klass.model_name.human
|
19
|
+
else
|
20
|
+
klass.to_s.underscore.humanize.downcase
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|