cancancan 1.15.0 → 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 (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,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module Actions
6
+ # Alias one or more actions into another one.
7
+ #
8
+ # alias_action :update, :destroy, :to => :modify
9
+ # can :modify, Comment
10
+ #
11
+ # Then :modify permission will apply to both :update and :destroy requests.
12
+ #
13
+ # can? :update, Comment # => true
14
+ # can? :destroy, Comment # => true
15
+ #
16
+ # This only works in one direction. Passing the aliased action into the "can?" call
17
+ # will not work because aliases are meant to generate more generic actions.
18
+ #
19
+ # alias_action :update, :destroy, :to => :modify
20
+ # can :update, Comment
21
+ # can? :modify, Comment # => false
22
+ #
23
+ # Unless that exact alias is used.
24
+ #
25
+ # can :modify, Comment
26
+ # can? :modify, Comment # => true
27
+ #
28
+ # The following aliases are added by default for conveniently mapping common controller actions.
29
+ #
30
+ # alias_action :index, :show, :to => :read
31
+ # alias_action :new, :to => :create
32
+ # alias_action :edit, :to => :update
33
+ #
34
+ # This way one can use params[:action] in the controller to determine the permission.
35
+ def alias_action(*args)
36
+ target = args.pop[:to]
37
+ validate_target(target)
38
+ aliased_actions[target] ||= []
39
+ aliased_actions[target] += args
40
+ end
41
+
42
+ # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
43
+ def aliased_actions
44
+ @aliased_actions ||= default_alias_actions
45
+ end
46
+
47
+ # Removes previously aliased actions including the defaults.
48
+ def clear_aliased_actions
49
+ @aliased_actions = {}
50
+ end
51
+
52
+ private
53
+
54
+ def default_alias_actions
55
+ {
56
+ read: %i[index show],
57
+ create: [:new],
58
+ update: [:edit]
59
+ }
60
+ end
61
+
62
+ # Given an action, it will try to find all of the actions which are aliased to it.
63
+ # This does the opposite kind of lookup as expand_actions.
64
+ def aliases_for_action(action)
65
+ results = [action]
66
+ aliased_actions.each do |aliased_action, actions|
67
+ results += aliases_for_action(aliased_action) if actions.include? action
68
+ end
69
+ results
70
+ end
71
+
72
+ def expanded_actions
73
+ @expanded_actions ||= {}
74
+ end
75
+
76
+ # Accepts an array of actions and returns an array of actions which match.
77
+ # This should be called before "matches?" and other checking methods since they
78
+ # rely on the actions to be expanded.
79
+ def expand_actions(actions)
80
+ expanded_actions[actions] ||= begin
81
+ expanded = []
82
+ actions.each do |action|
83
+ expanded << action
84
+ if (aliases = aliased_actions[action])
85
+ expanded += expand_actions(aliases)
86
+ end
87
+ end
88
+ expanded
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module Rules
6
+ protected
7
+
8
+ # Must be protected as an ability can merge with other abilities.
9
+ # This means that an ability must expose their rules with another ability.
10
+ def rules
11
+ @rules ||= []
12
+ end
13
+
14
+ private
15
+
16
+ def add_rule(rule)
17
+ rules << rule
18
+ add_rule_to_index(rule, rules.size - 1)
19
+ end
20
+
21
+ def add_rule_to_index(rule, position)
22
+ @rules_index ||= Hash.new { |h, k| h[k] = [] }
23
+
24
+ subjects = rule.subjects.compact
25
+ subjects << :all if subjects.empty?
26
+
27
+ subjects.each do |subject|
28
+ @rules_index[subject] << position
29
+ end
30
+ end
31
+
32
+ # Returns an array of Rule instances which match the action and subject
33
+ # This does not take into consideration any hash conditions or block statements
34
+ def relevant_rules(action, subject)
35
+ return [] unless @rules
36
+
37
+ relevant = possible_relevant_rules(subject).select do |rule|
38
+ rule.expanded_actions = expand_actions(rule.actions)
39
+ rule.relevant? action, subject
40
+ end
41
+ relevant.reverse!.uniq!
42
+ optimize_order! relevant
43
+ relevant
44
+ end
45
+
46
+ def possible_relevant_rules(subject)
47
+ if subject.is_a?(Hash)
48
+ rules
49
+ else
50
+ positions = @rules_index.values_at(subject, *alternative_subjects(subject))
51
+ positions.flatten!.sort!
52
+ positions.map { |i| @rules[i] }
53
+ end
54
+ end
55
+
56
+ def relevant_rules_for_match(action, subject)
57
+ relevant_rules(action, subject).each do |rule|
58
+ next unless rule.only_raw_sql?
59
+
60
+ raise Error,
61
+ "The can? and cannot? call cannot be used with a raw sql 'can' definition."\
62
+ " The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
63
+ end
64
+ end
65
+
66
+ def relevant_rules_for_query(action, subject)
67
+ rules = relevant_rules(action, subject).reject do |rule|
68
+ # reject 'cannot' rules with attributes when doing queries
69
+ rule.base_behavior == false && rule.attributes.present?
70
+ end
71
+ if rules.any?(&:only_block?)
72
+ raise Error, "The accessible_by call cannot be used with a block 'can' definition."\
73
+ "The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
74
+ end
75
+ rules
76
+ end
77
+
78
+ # Optimizes the order of the rules, so that rules with the :all subject are evaluated first.
79
+ def optimize_order!(rules)
80
+ first_can_in_group = -1
81
+ rules.each_with_index do |rule, i|
82
+ (first_can_in_group = -1) && next unless rule.base_behavior
83
+ (first_can_in_group = i) && next if first_can_in_group == -1
84
+ next unless rule.subjects == [:all]
85
+
86
+ rules[i] = rules[first_can_in_group]
87
+ rules[first_can_in_group] = rule
88
+ first_can_in_group += 1
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module StrongParameterSupport
6
+ # Returns an array of attributes suitable for use with strong parameters
7
+ #
8
+ # Note: reversing the relevant rules is important. Normal order means that 'cannot'
9
+ # rules will come before 'can' rules. However, you can't remove attributes before
10
+ # they are added. The 'reverse' is so that attributes will be added before the
11
+ # 'cannot' rules remove them.
12
+ def permitted_attributes(action, subject)
13
+ relevant_rules(action, subject)
14
+ .reverse
15
+ .select { |rule| rule.matches_conditions? action, subject }
16
+ .each_with_object(Set.new) do |rule, set|
17
+ attributes = get_attributes(rule, subject)
18
+ # add attributes for 'can', remove them for 'cannot'
19
+ rule.base_behavior ? set.merge(attributes) : set.subtract(attributes)
20
+ end.to_a
21
+ end
22
+
23
+ private
24
+
25
+ def subject_class?(subject)
26
+ klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
27
+ [Class, Module].include? klass
28
+ end
29
+
30
+ def get_attributes(rule, subject)
31
+ klass = subject_class?(subject) ? subject : subject.class
32
+ # empty attributes is an 'all'
33
+ if rule.attributes.empty? && klass < ActiveRecord::Base
34
+ klass.column_names.map(&:to_sym) - Array(klass.primary_key)
35
+ else
36
+ rule.attributes
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ConditionsMatcher
5
+ # Matches the block or conditions hash
6
+ def matches_conditions?(action, subject, attribute = nil, *extra_args)
7
+ return call_block_with_all(action, subject, extra_args) if @match_all
8
+ return matches_block_conditions(subject, attribute, *extra_args) if @block
9
+ return matches_non_block_conditions(subject) unless conditions_empty?
10
+
11
+ true
12
+ end
13
+
14
+ private
15
+
16
+ def subject_class?(subject)
17
+ klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
18
+ [Class, Module].include? klass
19
+ end
20
+
21
+ def matches_block_conditions(subject, *extra_args)
22
+ return @base_behavior if subject_class?(subject)
23
+
24
+ @block.call(subject, *extra_args.compact)
25
+ end
26
+
27
+ def matches_non_block_conditions(subject)
28
+ return nested_subject_matches_conditions?(subject) if subject.class == Hash
29
+ return matches_conditions_hash?(subject) unless subject_class?(subject)
30
+
31
+ # Don't stop at "cannot" definitions when there are conditions.
32
+ @base_behavior
33
+ end
34
+
35
+ def nested_subject_matches_conditions?(subject_hash)
36
+ parent, _child = subject_hash.first
37
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
38
+ end
39
+
40
+ # Checks if the given subject matches the given conditions hash.
41
+ # This behavior can be overriden by a model adapter by defining two class methods:
42
+ # override_matching_for_conditions?(subject, conditions) and
43
+ # matches_conditions_hash?(subject, conditions)
44
+ def matches_conditions_hash?(subject, conditions = @conditions)
45
+ return true if conditions.empty?
46
+
47
+ adapter = model_adapter(subject)
48
+
49
+ if adapter.override_conditions_hash_matching?(subject, conditions)
50
+ return adapter.matches_conditions_hash?(subject, conditions)
51
+ end
52
+
53
+ matches_all_conditions?(adapter, conditions, subject)
54
+ end
55
+
56
+ def matches_all_conditions?(adapter, conditions, subject)
57
+ conditions.all? do |name, value|
58
+ if adapter.override_condition_matching?(subject, name, value)
59
+ adapter.matches_condition?(subject, name, value)
60
+ else
61
+ condition_match?(subject.send(name), value)
62
+ end
63
+ end
64
+ end
65
+
66
+ def condition_match?(attribute, value)
67
+ case value
68
+ when Hash
69
+ hash_condition_match?(attribute, value)
70
+ when Range
71
+ value.cover?(attribute)
72
+ when Enumerable
73
+ value.include?(attribute)
74
+ else
75
+ attribute == value
76
+ end
77
+ end
78
+
79
+ def hash_condition_match?(attribute, value)
80
+ if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
81
+ attribute.any? { |element| matches_conditions_hash?(element, value) }
82
+ else
83
+ attribute && matches_conditions_hash?(attribute, value)
84
+ end
85
+ end
86
+
87
+ def call_block_with_all(action, subject, *extra_args)
88
+ if subject.class == Class
89
+ @block.call(action, subject, nil, *extra_args)
90
+ else
91
+ @block.call(action, subject.class, subject, *extra_args)
92
+ end
93
+ end
94
+
95
+ def model_adapter(subject)
96
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
97
+ end
98
+
99
+ def conditions_empty?
100
+ # @conditions might be an ActiveRecord::Associations::CollectionProxy
101
+ # which it's `==` implementation will fetch all records for comparison
102
+
103
+ (@conditions.is_a?(Hash) && @conditions == {}) || @conditions.nil?
104
+ end
105
+ end
106
+ end
@@ -1,5 +1,6 @@
1
- module CanCan
1
+ # frozen_string_literal: true
2
2
 
3
+ module CanCan
3
4
  # This module is automatically included into all controllers.
4
5
  # It also makes the "can?" and "cannot?" methods available to all views.
5
6
  module ControllerAdditions
@@ -41,7 +42,7 @@ module CanCan
41
42
  # private
42
43
  #
43
44
  # def find_book_by_permalink
44
- # @book = Book.find_by_permalink!(params[:id)
45
+ # @book = Book.find_by_permalink!(params[:id])
45
46
  # end
46
47
  # end
47
48
  #
@@ -72,8 +73,8 @@ module CanCan
72
73
  # Load this resource through another one. This should match the name of the parent instance variable or method.
73
74
  #
74
75
  # [:+through_association+]
75
- # The name of the association to fetch the child records through the parent resource. This is normally not needed
76
- # because it defaults to the pluralized resource name.
76
+ # The name of the association to fetch the child records through the parent resource.
77
+ # This is normally not needed because it defaults to the pluralized resource name.
77
78
  #
78
79
  # [:+shallow+]
79
80
  # Pass +true+ to allow this resource to be loaded directly when parent is +nil+. Defaults to +false+.
@@ -82,8 +83,8 @@ module CanCan
82
83
  # Pass +true+ if this is a singleton resource through a +has_one+ association.
83
84
  #
84
85
  # [:+parent+]
85
- # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
86
- # name is given which does not match the controller.
86
+ # True or false depending on if the resource is considered a parent resource.
87
+ # This defaults to +true+ if a resource name is given which does not match the controller.
87
88
  #
88
89
  # [:+class+]
89
90
  # The class to use for the model (string or constant).
@@ -160,8 +161,8 @@ module CanCan
160
161
  # Pass +true+ if this is a singleton resource through a +has_one+ association.
161
162
  #
162
163
  # [:+parent+]
163
- # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
164
- # name is given which does not match the controller.
164
+ # True or false depending on if the resource is considered a parent resource.
165
+ # This defaults to +true+ if a resource name is given which does not match the controller.
165
166
  #
166
167
  # [:+class+]
167
168
  # The class to use for the model (string or constant). This passed in when the instance variable is not set.
@@ -226,8 +227,9 @@ module CanCan
226
227
  cancan_skipper[:authorize][name] = options
227
228
  end
228
229
 
229
- # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call.
230
- # If neither of these authorization methods are called, a CanCan::AuthorizationNotPerformed exception will be raised.
230
+ # Add this to a controller to ensure it performs authorization through +authorize+! or +authorize_resource+ call.
231
+ # If neither of these authorization methods are called,
232
+ # a CanCan::AuthorizationNotPerformed exception will be raised.
231
233
  # This is normally added to the ApplicationController to ensure all controller actions do authorization.
232
234
  #
233
235
  # class ApplicationController < ActionController::Base
@@ -244,26 +246,29 @@ module CanCan
244
246
  # Does not apply to given actions.
245
247
  #
246
248
  # [:+if+]
247
- # Supply the name of a controller method to be called. The authorization check only takes place if this returns true.
249
+ # Supply the name of a controller method to be called.
250
+ # The authorization check only takes place if this returns true.
248
251
  #
249
252
  # check_authorization :if => :admin_controller?
250
253
  #
251
254
  # [:+unless+]
252
- # Supply the name of a controller method to be called. The authorization check only takes place if this returns false.
255
+ # Supply the name of a controller method to be called.
256
+ # The authorization check only takes place if this returns false.
253
257
  #
254
258
  # check_authorization :unless => :devise_controller?
255
259
  #
256
260
  def check_authorization(options = {})
257
- method_name = ActiveSupport.respond_to?(:version) && ActiveSupport.version >= Gem::Version.new("4") ? :after_action : :after_filter
258
-
259
- block = Proc.new do |controller|
261
+ block = proc do |controller|
260
262
  next if controller.instance_variable_defined?(:@_authorized)
261
263
  next if options[:if] && !controller.send(options[:if])
262
264
  next if options[:unless] && controller.send(options[:unless])
263
- raise AuthorizationNotPerformed, "This action failed the check_authorization because it does not authorize_resource. Add skip_authorization_check to bypass this check."
265
+
266
+ raise AuthorizationNotPerformed,
267
+ 'This action failed the check_authorization because it does not authorize_resource. '\
268
+ 'Add skip_authorization_check to bypass this check.'
264
269
  end
265
270
 
266
- self.send(method_name, options.slice(:only, :except), &block)
271
+ send(:after_action, options.slice(:only, :except), &block)
267
272
  end
268
273
 
269
274
  # Call this in the class of a controller to skip the check_authorization behavior on the actions.
@@ -274,31 +279,23 @@ module CanCan
274
279
  #
275
280
  # Any arguments are passed to the +before_action+ it triggers.
276
281
  def skip_authorization_check(*args)
277
- method_name = ActiveSupport.respond_to?(:version) && ActiveSupport.version >= Gem::Version.new("4") ? :before_action : :before_filter
278
- block = Proc.new{ |controller| controller.instance_variable_set(:@_authorized, true) }
279
- self.send(method_name, *args, &block)
280
- end
281
-
282
- def skip_authorization(*args)
283
- raise ImplementationRemoved, "The CanCan skip_authorization method has been renamed to skip_authorization_check. Please update your code."
282
+ block = proc { |controller| controller.instance_variable_set(:@_authorized, true) }
283
+ send(:before_action, *args, &block)
284
284
  end
285
285
 
286
286
  def cancan_resource_class
287
- if ancestors.map(&:to_s).include? "InheritedResources::Actions"
288
- InheritedResource
289
- else
290
- ControllerResource
291
- end
287
+ ControllerResource
292
288
  end
293
289
 
294
290
  def cancan_skipper
295
- @_cancan_skipper ||= {:authorize => {}, :load => {}}
291
+ self._cancan_skipper ||= { authorize: {}, load: {} }
296
292
  end
297
293
  end
298
294
 
299
295
  def self.included(base)
300
296
  base.extend ClassMethods
301
297
  base.helper_method :can?, :cannot?, :current_ability if base.respond_to? :helper_method
298
+ base.class_attribute :_cancan_skipper
302
299
  end
303
300
 
304
301
  # Raises a CanCan::AccessDenied exception if the current_ability cannot
@@ -342,10 +339,6 @@ module CanCan
342
339
  current_ability.authorize!(*args)
343
340
  end
344
341
 
345
- def unauthorized!(message = nil)
346
- raise ImplementationRemoved, "The unauthorized! method has been removed from CanCan, use authorize! instead."
347
- end
348
-
349
342
  # Creates and returns the current user's ability and caches it. If you
350
343
  # want to override how the Ability is defined then this is the place.
351
344
  # Just define the method in the controller to change behavior.
@@ -394,8 +387,8 @@ module CanCan
394
387
  end
395
388
  end
396
389
 
397
- if defined? ActionController::Base
398
- ActionController::Base.class_eval do
390
+ if defined? ActiveSupport
391
+ ActiveSupport.on_load(:action_controller) do
399
392
  include CanCan::ControllerAdditions
400
393
  end
401
394
  end