cancancan 1.15.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +18 -18
  3. data/init.rb +2 -0
  4. data/lib/cancan.rb +9 -11
  5. data/lib/cancan/ability.rb +90 -203
  6. data/lib/cancan/ability/actions.rb +93 -0
  7. data/lib/cancan/ability/rules.rb +93 -0
  8. data/lib/cancan/ability/strong_parameter_support.rb +41 -0
  9. data/lib/cancan/conditions_matcher.rb +106 -0
  10. data/lib/cancan/controller_additions.rb +29 -36
  11. data/lib/cancan/controller_resource.rb +46 -211
  12. data/lib/cancan/controller_resource_builder.rb +26 -0
  13. data/lib/cancan/controller_resource_finder.rb +42 -0
  14. data/lib/cancan/controller_resource_loader.rb +120 -0
  15. data/lib/cancan/controller_resource_name_finder.rb +23 -0
  16. data/lib/cancan/controller_resource_sanitizer.rb +32 -0
  17. data/lib/cancan/exceptions.rb +17 -5
  18. data/lib/cancan/matchers.rb +12 -3
  19. data/lib/cancan/model_adapters/abstract_adapter.rb +10 -8
  20. data/lib/cancan/model_adapters/active_record_4_adapter.rb +39 -43
  21. data/lib/cancan/model_adapters/active_record_5_adapter.rb +68 -0
  22. data/lib/cancan/model_adapters/active_record_adapter.rb +77 -82
  23. data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
  24. data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
  25. data/lib/cancan/model_adapters/default_adapter.rb +2 -0
  26. data/lib/cancan/model_additions.rb +2 -1
  27. data/lib/cancan/parameter_validators.rb +9 -0
  28. data/lib/cancan/relevant.rb +29 -0
  29. data/lib/cancan/rule.rb +76 -106
  30. data/lib/cancan/rules_compressor.rb +23 -0
  31. data/lib/cancan/unauthorized_message_resolver.rb +24 -0
  32. data/lib/cancan/version.rb +3 -1
  33. data/lib/cancancan.rb +2 -0
  34. data/lib/generators/cancan/ability/ability_generator.rb +4 -2
  35. data/lib/generators/cancan/ability/templates/ability.rb +2 -0
  36. metadata +66 -57
  37. data/.gitignore +0 -15
  38. data/.rspec +0 -1
  39. data/.travis.yml +0 -33
  40. data/Appraisals +0 -104
  41. data/CHANGELOG.rdoc +0 -527
  42. data/CONTRIBUTING.md +0 -23
  43. data/Gemfile +0 -3
  44. data/LICENSE +0 -22
  45. data/README.md +0 -217
  46. data/Rakefile +0 -9
  47. data/gemfiles/activerecord_3.2.gemfile +0 -17
  48. data/gemfiles/activerecord_4.0.gemfile +0 -18
  49. data/gemfiles/activerecord_4.1.gemfile +0 -18
  50. data/gemfiles/activerecord_4.2.gemfile +0 -19
  51. data/gemfiles/activerecord_5.0.gemfile +0 -19
  52. data/gemfiles/mongoid_2.x.gemfile +0 -17
  53. data/gemfiles/sequel_3.x.gemfile +0 -17
  54. data/lib/cancan/inherited_resource.rb +0 -20
  55. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -16
  56. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -75
  57. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  58. data/spec/README.rdoc +0 -27
  59. data/spec/cancan/ability_spec.rb +0 -544
  60. data/spec/cancan/controller_additions_spec.rb +0 -151
  61. data/spec/cancan/controller_resource_spec.rb +0 -643
  62. data/spec/cancan/exceptions_spec.rb +0 -58
  63. data/spec/cancan/inherited_resource_spec.rb +0 -71
  64. data/spec/cancan/matchers_spec.rb +0 -29
  65. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -154
  66. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -405
  67. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  68. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -247
  69. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
  70. data/spec/cancan/rule_spec.rb +0 -52
  71. data/spec/matchers.rb +0 -13
  72. data/spec/spec.opts +0 -2
  73. data/spec/spec_helper.rb +0 -27
  74. 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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class DefaultAdapter < AbstractAdapter
@@ -1,5 +1,6 @@
1
- module CanCan
1
+ # frozen_string_literal: true
2
2
 
3
+ module CanCan
3
4
  # This module adds the accessible_by class method to a model. It is included in the model adapters.
4
5
  module ModelAdditions
5
6
  module ClassMethods
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ParameterValidators
5
+ def valid_attribute_param?(attribute)
6
+ attribute.nil? || attribute.is_a?(Symbol) || (attribute.is_a?(Array) && attribute.all? { |a| a.is_a?(Symbol) })
7
+ end
8
+ end
9
+ 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
@@ -1,156 +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
- attr_reader :base_behavior, :subjects, :actions, :conditions
7
- attr_writer :expanded_actions
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, conditions, block)
14
- raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil?
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 = [action].flatten
18
- @subjects = [subject].flatten
19
- @conditions = conditions || {}
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
- # Matches both the subject and action, not necessarily the conditions
24
- def relevant?(action, subject)
25
- subject = subject.values.first if subject.class == Hash
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
- # Matches the block or conditions hash
30
- def matches_conditions?(action, subject, extra_args)
31
- if @match_all
32
- call_block_with_all(action, subject, extra_args)
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
- def conditions_empty?
54
- @conditions == {} || @conditions.nil?
47
+ repr + '>'
55
48
  end
56
49
 
57
- def unmergeable?
58
- @conditions.respond_to?(:keys) && @conditions.present? &&
59
- (!@conditions.keys.first.kind_of? Symbol)
50
+ def can_rule?
51
+ base_behavior
60
52
  end
61
53
 
62
- def associations_hash(conditions = @conditions)
63
- hash = {}
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 attributes_from_conditions
71
- attributes = {}
72
- @conditions.each do |key, value|
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
- private
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 matches_action?(action)
86
- @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
67
+ def only_raw_sql?
68
+ @block.nil? && !conditions_empty? && !@conditions.is_a?(Hash)
87
69
  end
88
70
 
89
- def matches_subject?(subject)
90
- @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
71
+ def with_scope?
72
+ @conditions.is_a?(ActiveRecord::Relation)
91
73
  end
92
74
 
93
- def matches_subject_class?(subject)
94
- @subjects.any? do |sub|
95
- sub.kind_of?(Module) && (subject.kind_of?(sub) ||
96
- subject.class.to_s == sub.to_s ||
97
- (subject.kind_of?(Module) && subject.ancestors.include?(sub)))
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
- # Checks if the given subject matches the given conditions hash.
102
- # This behavior can be overriden by a model adapter by defining two class methods:
103
- # override_matching_for_conditions?(subject, conditions) and
104
- # matches_conditions_hash?(subject, conditions)
105
- def matches_conditions_hash?(subject, conditions = @conditions)
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
- adapter.matches_condition?(subject, name, value)
116
- else
117
- condition_match?(subject.send(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
118
90
  end
119
91
  end
92
+ attributes
120
93
  end
121
94
 
122
- def nested_subject_matches_conditions?(subject_hash)
123
- parent, _child = subject_hash.first
124
- matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
125
- end
95
+ def matches_attributes?(attribute)
96
+ return true if @attributes.empty?
97
+ return @base_behavior if attribute.nil?
126
98
 
127
- def call_block_with_all(action, subject, extra_args)
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
- def model_adapter(subject)
136
- CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
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 condition_match?(attribute, value)
140
- case value
141
- when Hash then hash_condition_match?(attribute, value)
142
- when String then attribute == value
143
- when Range then value.cover?(attribute)
144
- when Enumerable then value.include?(attribute)
145
- else attribute == value
146
- 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."
147
115
  end
148
116
 
149
- def hash_condition_match?(attribute, value)
150
- if attribute.kind_of?(Array) || (defined?(ActiveRecord) && attribute.kind_of?(ActiveRecord::Relation))
151
- attribute.any? { |element| matches_conditions_hash?(element, value) }
117
+ def wrap(object)
118
+ if object.nil?
119
+ []
120
+ elsif object.respond_to?(:to_ary)
121
+ object.to_ary || [object]
152
122
  else
153
- !attribute.nil? && matches_conditions_hash?(attribute, value)
123
+ [object]
154
124
  end
155
125
  end
156
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