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.
Files changed (73) 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 +93 -194
  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 +38 -41
  11. data/lib/cancan/controller_resource.rb +52 -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 -13
  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 -105
  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 -56
  37. data/.gitignore +0 -15
  38. data/.rspec +0 -1
  39. data/.travis.yml +0 -28
  40. data/Appraisals +0 -81
  41. data/CHANGELOG.rdoc +0 -518
  42. data/CONTRIBUTING.md +0 -23
  43. data/Gemfile +0 -3
  44. data/LICENSE +0 -22
  45. data/README.md +0 -214
  46. data/Rakefile +0 -9
  47. data/gemfiles/activerecord_3.2.gemfile +0 -16
  48. data/gemfiles/activerecord_4.0.gemfile +0 -17
  49. data/gemfiles/activerecord_4.1.gemfile +0 -17
  50. data/gemfiles/activerecord_4.2.gemfile +0 -18
  51. data/gemfiles/mongoid_2.x.gemfile +0 -16
  52. data/gemfiles/sequel_3.x.gemfile +0 -16
  53. data/lib/cancan/inherited_resource.rb +0 -20
  54. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -16
  55. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -54
  56. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  57. data/spec/README.rdoc +0 -27
  58. data/spec/cancan/ability_spec.rb +0 -521
  59. data/spec/cancan/controller_additions_spec.rb +0 -141
  60. data/spec/cancan/controller_resource_spec.rb +0 -632
  61. data/spec/cancan/exceptions_spec.rb +0 -58
  62. data/spec/cancan/inherited_resource_spec.rb +0 -71
  63. data/spec/cancan/matchers_spec.rb +0 -29
  64. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -85
  65. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -384
  66. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  67. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -227
  68. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
  69. data/spec/cancan/rule_spec.rb +0 -52
  70. data/spec/matchers.rb +0 -13
  71. data/spec/spec.opts +0 -2
  72. data/spec/spec_helper.rb +0 -27
  73. 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,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
- 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
- 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 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 Enumerable then value.include?(attribute)
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 hash_condition_match?(attribute, value)
149
- if attribute.kind_of?(Array) || (defined?(ActiveRecord) && attribute.kind_of?(ActiveRecord::Relation))
150
- 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]
151
122
  else
152
- !attribute.nil? && matches_conditions_hash?(attribute, value)
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