cancancan 2.3.0 → 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/cancancan.gemspec +6 -5
  3. data/init.rb +2 -0
  4. data/lib/cancan.rb +6 -0
  5. data/lib/cancan/ability.rb +54 -24
  6. data/lib/cancan/ability/actions.rb +2 -0
  7. data/lib/cancan/ability/rules.rb +14 -6
  8. data/lib/cancan/ability/strong_parameter_support.rb +41 -0
  9. data/lib/cancan/class_matcher.rb +26 -0
  10. data/lib/cancan/conditions_matcher.rb +25 -12
  11. data/lib/cancan/config.rb +54 -0
  12. data/lib/cancan/controller_additions.rb +4 -1
  13. data/lib/cancan/controller_resource.rb +6 -0
  14. data/lib/cancan/controller_resource_builder.rb +2 -0
  15. data/lib/cancan/controller_resource_finder.rb +2 -0
  16. data/lib/cancan/controller_resource_loader.rb +4 -0
  17. data/lib/cancan/controller_resource_name_finder.rb +2 -0
  18. data/lib/cancan/controller_resource_sanitizer.rb +2 -0
  19. data/lib/cancan/exceptions.rb +18 -2
  20. data/lib/cancan/matchers.rb +3 -0
  21. data/lib/cancan/model_adapters/abstract_adapter.rb +3 -1
  22. data/lib/cancan/model_adapters/active_record_4_adapter.rb +26 -25
  23. data/lib/cancan/model_adapters/active_record_5_adapter.rb +21 -26
  24. data/lib/cancan/model_adapters/active_record_adapter.rb +56 -14
  25. data/lib/cancan/model_adapters/conditions_extractor.rb +3 -3
  26. data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
  27. data/lib/cancan/model_adapters/default_adapter.rb +2 -0
  28. data/lib/cancan/model_adapters/sti_normalizer.rb +39 -0
  29. data/lib/cancan/model_additions.rb +2 -0
  30. data/lib/cancan/parameter_validators.rb +9 -0
  31. data/lib/cancan/relevant.rb +29 -0
  32. data/lib/cancan/rule.rb +67 -23
  33. data/lib/cancan/rules_compressor.rb +3 -0
  34. data/lib/cancan/unauthorized_message_resolver.rb +24 -0
  35. data/lib/cancan/version.rb +3 -1
  36. data/lib/cancancan.rb +2 -0
  37. data/lib/generators/cancan/ability/ability_generator.rb +3 -1
  38. data/lib/generators/cancan/ability/templates/ability.rb +2 -0
  39. metadata +37 -30
  40. data/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +0 -39
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ def self.valid_accessible_by_strategies
5
+ strategies = [:left_join]
6
+ strategies << :subquery unless does_not_support_subquery_strategy?
7
+ strategies
8
+ end
9
+
10
+ # Determines how CanCan should build queries when calling accessible_by,
11
+ # if the query will contain a join. The default strategy is `:subquery`.
12
+ #
13
+ # # config/initializers/cancan.rb
14
+ # CanCan.accessible_by_strategy = :subquery
15
+ #
16
+ # Valid strategies are:
17
+ # - :subquery - Creates a nested query with all joins, wrapped by a
18
+ # WHERE IN query.
19
+ # - :left_join - Calls the joins directly using `left_joins`, and
20
+ # ensures records are unique using `distinct`. Note that
21
+ # `distinct` is not reliable in some cases. See
22
+ # https://github.com/CanCanCommunity/cancancan/pull/605
23
+ def self.accessible_by_strategy
24
+ @accessible_by_strategy || default_accessible_by_strategy
25
+ end
26
+
27
+ def self.default_accessible_by_strategy
28
+ if does_not_support_subquery_strategy?
29
+ # see https://github.com/CanCanCommunity/cancancan/pull/655 for where this was added
30
+ # the `subquery` strategy (from https://github.com/CanCanCommunity/cancancan/pull/619
31
+ # only works in Rails 5 and higher
32
+ :left_join
33
+ else
34
+ :subquery
35
+ end
36
+ end
37
+
38
+ def self.accessible_by_strategy=(value)
39
+ unless valid_accessible_by_strategies.include?(value)
40
+ raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}"
41
+ end
42
+
43
+ if value == :subquery && does_not_support_subquery_strategy?
44
+ raise ArgumentError, 'accessible_by_strategy = :subquery requires ActiveRecord 5 or newer'
45
+ end
46
+
47
+ @accessible_by_strategy = value
48
+ end
49
+
50
+ def self.does_not_support_subquery_strategy?
51
+ !defined?(CanCan::ModelAdapters::ActiveRecordAdapter) ||
52
+ CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
53
+ end
54
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  # This module is automatically included into all controllers.
3
5
  # It also makes the "can?" and "cannot?" methods available to all views.
@@ -225,7 +227,7 @@ module CanCan
225
227
  cancan_skipper[:authorize][name] = options
226
228
  end
227
229
 
228
- # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call.
230
+ # Add this to a controller to ensure it performs authorization through +authorize+! or +authorize_resource+ call.
229
231
  # If neither of these authorization methods are called,
230
232
  # a CanCan::AuthorizationNotPerformed exception will be raised.
231
233
  # This is normally added to the ApplicationController to ensure all controller actions do authorization.
@@ -260,6 +262,7 @@ module CanCan
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])
265
+
263
266
  raise AuthorizationNotPerformed,
264
267
  'This action failed the check_authorization because it does not authorize_resource. '\
265
268
  'Add skip_authorization_check to bypass this check.'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'controller_resource_loader.rb'
2
4
  module CanCan
3
5
  # Handle the load and authorization controller logic
@@ -34,6 +36,7 @@ module CanCan
34
36
 
35
37
  def authorize_resource
36
38
  return if skip?(:authorize)
39
+
37
40
  @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
38
41
  end
39
42
 
@@ -43,6 +46,7 @@ module CanCan
43
46
 
44
47
  def skip?(behavior)
45
48
  return false unless (options = @controller.class.cancan_skipper[behavior][@name])
49
+
46
50
  options == {} ||
47
51
  options[:except] && !action_exists_in?(options[:except]) ||
48
52
  action_exists_in?(options[:only])
@@ -90,6 +94,7 @@ module CanCan
90
94
 
91
95
  def resource_instance
92
96
  return unless load_instance? && @controller.instance_variable_defined?("@#{instance_name}")
97
+
93
98
  @controller.instance_variable_get("@#{instance_name}")
94
99
  end
95
100
 
@@ -99,6 +104,7 @@ module CanCan
99
104
 
100
105
  def collection_instance
101
106
  return unless @controller.instance_variable_defined?("@#{instance_name.to_s.pluralize}")
107
+
102
108
  @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
103
109
  end
104
110
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ControllerResourceBuilder
3
5
  protected
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ControllerResourceFinder
3
5
  protected
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'controller_resource_finder.rb'
2
4
  require_relative 'controller_resource_name_finder.rb'
3
5
  require_relative 'controller_resource_builder.rb'
@@ -11,6 +13,7 @@ module CanCan
11
13
 
12
14
  def load_resource
13
15
  return if skip?(:load)
16
+
14
17
  if load_instance?
15
18
  self.resource_instance ||= load_resource_instance
16
19
  elsif load_collection?
@@ -26,6 +29,7 @@ module CanCan
26
29
 
27
30
  def resource_params_by_key(key)
28
31
  return unless @options[key] && @params.key?(extract_key(@options[key]))
32
+
29
33
  @params[extract_key(@options[key])]
30
34
  end
31
35
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ControllerResourceNameFinder
3
5
  protected
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ControllerResourceSanitizer
3
5
  protected
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  # A general CanCan exception
3
5
  class Error < StandardError; end
@@ -8,9 +10,15 @@ module CanCan
8
10
  # Raised when removed code is called, an alternative solution is provided in message.
9
11
  class ImplementationRemoved < Error; end
10
12
 
11
- # Raised when using check_authorization without calling authorized!
13
+ # Raised when using check_authorization without calling authorize!
12
14
  class AuthorizationNotPerformed < Error; end
13
15
 
16
+ # Raised when a rule is created with both a block and a hash of conditions
17
+ class BlockAndConditionsError < Error; end
18
+
19
+ # Raised when an unexpected argument is passed as an attribute
20
+ class AttributeArgumentError < Error; end
21
+
14
22
  # Raised when using a wrong association name
15
23
  class WrongAssociationName < Error; end
16
24
 
@@ -33,7 +41,7 @@ module CanCan
33
41
  # exception.default_message = "Default error message"
34
42
  # exception.message # => "Default error message"
35
43
  #
36
- # See ControllerAdditions#authorized! for more information on rescuing from this exception
44
+ # See ControllerAdditions#authorize! for more information on rescuing from this exception
37
45
  # and customizing the message using I18n.
38
46
  class AccessDenied < Error
39
47
  attr_reader :action, :subject, :conditions
@@ -50,5 +58,13 @@ module CanCan
50
58
  def to_s
51
59
  @message || @default_message
52
60
  end
61
+
62
+ def inspect
63
+ details = %i[action subject conditions message].map do |attribute|
64
+ value = instance_variable_get "@#{attribute}"
65
+ "#{attribute}: #{value.inspect}" if value.present?
66
+ end.compact.join(', ')
67
+ "#<#{self.class.name} #{details}>"
68
+ end
53
69
  end
54
70
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # RSpec 1 compatability
2
4
 
3
5
  if rspec_module == 'RSpec'
@@ -12,6 +14,7 @@ Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
12
14
  actions = args.first
13
15
  if actions.is_a? Array
14
16
  break false if actions.empty?
17
+
15
18
  actions.all? { |action| ability.can?(action, *args[1..-1]) }
16
19
  else
17
20
  ability.can?(*args)
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class AbstractAdapter
4
6
  def self.inherited(subclass)
5
7
  @subclasses ||= []
6
- @subclasses << subclass
8
+ @subclasses.insert(0, subclass)
7
9
  end
8
10
 
9
11
  def self.adapter_class(model_class)
@@ -1,27 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
- class ActiveRecord4Adapter < AbstractAdapter
4
- include ActiveRecordAdapter
5
- def self.for_class?(model_class)
6
- ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base
7
- end
5
+ class ActiveRecord4Adapter < ActiveRecordAdapter
6
+ AbstractAdapter.inherited(self)
8
7
 
9
- # TODO: this should be private
10
- def self.override_condition_matching?(subject, name, _value)
11
- subject.class.defined_enums.include?(name.to_s)
12
- end
8
+ class << self
9
+ def for_class?(model_class)
10
+ version_lower?('5.0.0') && model_class <= ActiveRecord::Base
11
+ end
13
12
 
14
- # TODO: this should be private
15
- def self.matches_condition?(subject, name, value)
16
- # Get the mapping from enum strings to values.
17
- enum = subject.class.send(name.to_s.pluralize)
18
- # Get the value of the attribute as an integer.
19
- attribute = enum[subject.send(name)]
20
- # Check to see if the value matches the condition.
21
- if value.is_a?(Enumerable)
22
- value.include? attribute
23
- else
24
- attribute == value
13
+ def override_condition_matching?(subject, name, _value)
14
+ subject.class.defined_enums.include?(name.to_s)
15
+ end
16
+
17
+ def matches_condition?(subject, name, value)
18
+ # Get the mapping from enum strings to values.
19
+ enum = subject.class.send(name.to_s.pluralize)
20
+ # Get the value of the attribute as an integer.
21
+ attribute = enum[subject.send(name)]
22
+ # Check to see if the value matches the condition.
23
+ if value.is_a?(Enumerable)
24
+ value.include? attribute
25
+ else
26
+ attribute == value
27
+ end
25
28
  end
26
29
  end
27
30
 
@@ -31,15 +34,13 @@ module CanCan
31
34
  # look inside the where clause to decide to outer join tables
32
35
  # you're using in the where. Instead, `references()` is required
33
36
  # in addition to `includes()` to force the outer join.
34
- def build_relation(*where_conditions)
35
- relation = @model_class.where(*where_conditions)
36
- relation = relation.includes(joins).references(joins) if joins.present?
37
- relation
37
+ def build_joins_relation(relation, *_where_conditions)
38
+ relation.includes(joins).references(joins)
38
39
  end
39
40
 
40
41
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
41
42
  def sanitize_sql(conditions)
42
- if ActiveRecord::VERSION::MINOR >= 2 && conditions.is_a?(Hash)
43
+ if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash)
43
44
  sanitize_sql_activerecord4(conditions)
44
45
  else
45
46
  @model_class.send(:sanitize_sql, conditions)
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class ActiveRecord5Adapter < ActiveRecord4Adapter
4
6
  AbstractAdapter.inherited(self)
5
7
 
6
8
  def self.for_class?(model_class)
7
- ActiveRecord::VERSION::MAJOR == 5 && model_class <= ActiveRecord::Base
9
+ version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base
8
10
  end
9
11
 
10
12
  # rails 5 is capable of using strings in enum
@@ -13,26 +15,25 @@ module CanCan
13
15
  return super if Array.wrap(value).all? { |x| x.is_a? Integer }
14
16
 
15
17
  attribute = subject.send(name)
16
- if value.is_a?(Enumerable)
17
- value.map(&:to_s).include? attribute
18
- else
19
- attribute == value.to_s
20
- end
18
+ raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
19
+ !(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
21
20
  end
22
21
 
23
22
  private
24
23
 
25
- # As of rails 4, `includes()` no longer causes active record to
26
- # look inside the where clause to decide to outer join tables
27
- # you're using in the where. Instead, `references()` is required
28
- # in addition to `includes()` to force the outer join.
29
- def build_relation(*where_conditions)
30
- relation = @model_class.where(*where_conditions)
31
- relation = relation.includes(joins).references(joins) if joins.present?
32
- relation
24
+ def build_joins_relation(relation, *where_conditions)
25
+ case CanCan.accessible_by_strategy
26
+ when :subquery
27
+ inner = @model_class.unscoped do
28
+ @model_class.left_joins(joins).where(*where_conditions)
29
+ end
30
+ @model_class.where(@model_class.primary_key => inner)
31
+
32
+ when :left_join
33
+ relation.left_joins(joins).distinct
34
+ end
33
35
  end
34
36
 
35
- # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
36
37
  def sanitize_sql(conditions)
37
38
  if conditions.is_a?(Hash)
38
39
  sanitize_sql_activerecord5(conditions)
@@ -46,23 +47,17 @@ module CanCan
46
47
  table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
47
48
  predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
48
49
 
49
- conditions = predicate_builder.resolve_column_aliases(conditions)
50
-
51
- conditions.stringify_keys!
52
-
53
- predicate_builder.build_from_hash(conditions).map do |b|
54
- visit_nodes(b)
55
- end.join(' AND ')
50
+ predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
56
51
  end
57
52
 
58
- def visit_nodes(b)
53
+ def visit_nodes(node)
59
54
  # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
60
- if ActiveRecord::VERSION::MINOR >= 2
55
+ if self.class.version_greater_or_equal?('5.2.0')
61
56
  connection = @model_class.send(:connection)
62
57
  collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
63
- connection.visitor.accept(b, collector).value
58
+ connection.visitor.accept(node, collector).value
64
59
  else
65
- @model_class.send(:connection).visitor.compile(b)
60
+ @model_class.send(:connection).visitor.compile(node)
66
61
  end
67
62
  end
68
63
  end
@@ -1,10 +1,22 @@
1
- require_relative 'can_can/model_adapters/active_record_adapter/joins.rb'
2
- require_relative 'conditions_extractor.rb'
3
- require 'cancan/rules_compressor'
1
+ # frozen_string_literal: true
2
+
4
3
  module CanCan
5
4
  module ModelAdapters
6
- module ActiveRecordAdapter
7
- include CanCan::ModelAdapters::ActiveRecordAdapter::Joins
5
+ class ActiveRecordAdapter < AbstractAdapter
6
+ def self.version_greater_or_equal?(version)
7
+ Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version)
8
+ end
9
+
10
+ def self.version_lower?(version)
11
+ Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version)
12
+ end
13
+
14
+ def initialize(model_class, rules)
15
+ super
16
+ @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
17
+ StiNormalizer.normalize(@compressed_rules)
18
+ ConditionsNormalizer.normalize(model_class, @compressed_rules)
19
+ end
8
20
 
9
21
  # Returns conditions intended to be used inside a database query. Normally you will not call this
10
22
  # method directly, but instead go through ModelAdditions#accessible_by.
@@ -22,13 +34,12 @@ module CanCan
22
34
  # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
23
35
  #
24
36
  def conditions
25
- compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
26
37
  conditions_extractor = ConditionsExtractor.new(@model_class)
27
- if compressed_rules.size == 1 && compressed_rules.first.base_behavior
38
+ if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
28
39
  # Return the conditions directly if there's just one definition
29
- conditions_extractor.tableize_conditions(compressed_rules.first.conditions).dup
40
+ conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
30
41
  else
31
- extract_multiple_conditions(conditions_extractor, compressed_rules)
42
+ extract_multiple_conditions(conditions_extractor, @compressed_rules)
32
43
  end
33
44
  end
34
45
 
@@ -42,27 +53,58 @@ module CanCan
42
53
  if override_scope
43
54
  @model_class.where(nil).merge(override_scope)
44
55
  elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
45
- mergeable_conditions? ? build_relation(conditions) : build_relation(*@rules.map(&:conditions))
56
+ build_relation(conditions)
46
57
  else
47
58
  @model_class.all(conditions: conditions, joins: joins)
48
59
  end
49
60
  end
50
61
 
62
+ def build_relation(*where_conditions)
63
+ relation = @model_class.where(*where_conditions)
64
+ return relation unless joins.present?
65
+
66
+ # subclasses must implement `build_joins_relation`
67
+ build_joins_relation(relation, *where_conditions)
68
+ end
69
+
70
+ # Returns the associations used in conditions for the :joins option of a search.
71
+ # See ModelAdditions#accessible_by
72
+ def joins
73
+ joins_hash = {}
74
+ @compressed_rules.reverse_each do |rule|
75
+ deep_merge(joins_hash, rule.associations_hash)
76
+ end
77
+ deep_clean(joins_hash) unless joins_hash.empty?
78
+ end
79
+
51
80
  private
52
81
 
53
- def mergeable_conditions?
54
- @rules.find(&:unmergeable?).blank?
82
+ # Removes empty hashes and moves everything into arrays.
83
+ def deep_clean(joins_hash)
84
+ joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
85
+ end
86
+
87
+ # Takes two hashes and does a deep merge.
88
+ def deep_merge(base_hash, added_hash)
89
+ added_hash.each do |key, value|
90
+ if base_hash[key].is_a?(Hash)
91
+ deep_merge(base_hash[key], value) unless value.empty?
92
+ else
93
+ base_hash[key] = value
94
+ end
95
+ end
55
96
  end
56
97
 
57
98
  def override_scope
58
- conditions = @rules.map(&:conditions).compact
99
+ conditions = @compressed_rules.map(&:conditions).compact
59
100
  return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
60
101
  return conditions.first if conditions.size == 1
102
+
61
103
  raise_override_scope_error
62
104
  end
63
105
 
64
106
  def raise_override_scope_error
65
- rule_found = @rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
107
+ rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
66
108
  raise Error,
67
109
  'Unable to merge an Active Record scope with other conditions. '\
68
110
  "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."