cancancan 3.0.2 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0fb1ea8821029ab1e070299caac458325635748b3051e1f08999176ec81e29d
4
- data.tar.gz: 20a2d023ab68fdf8392d57cc0d32a95d5d904d2bac67c60850d8339524e48de6
3
+ metadata.gz: c4498ac94e1994faa4da80dc957d8c9564433d991048774f9ac2f051e60de580
4
+ data.tar.gz: 74209123c4c49adcd1d2d81df1de61c5f8cc2f243fdcdb4d01d3e41731e4c266
5
5
  SHA512:
6
- metadata.gz: 983f4b45a5c32972bca59adb1a0a2591352ba571fb3d95a13c91c40b719828507611ab7c3695ff94bcc07ae8599cac95d1bab8e4bcbad9fd9250bc20e22d9b9c
7
- data.tar.gz: 626e5e82e569b68383eb63a605983f92276540f3a20abd35e1fb1a79876880a8b533296382ddef27caa9e7cad3fa28bdd885dd77e87680b32aa70cc38592c256
6
+ metadata.gz: eb7774650d12a7073d09bb713f7eedfd7d376689f6d6bb842620b325a814720172f4fec900705bcc0dd3bd90818a88d2ab7e3904ea3a5d004533e13b4bed1c4c
7
+ data.tar.gz: a7a6fff07fbd7d52816dd960d00ae0baea7d50c5ed41d0abf32ac3a0c2b6bc3c557a4309944717b57aad785ad7dc394870c079b6a820b62798a373a9d9f8c2b0
data/cancancan.gemspec CHANGED
@@ -10,6 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.authors = ['Alessandro Rodi (Renuo AG)', 'Bryan Rite', 'Ryan Bates', 'Richard Wilson']
11
11
  s.email = 'alessandro.rodi@renuo.ch'
12
12
  s.homepage = 'https://github.com/CanCanCommunity/cancancan'
13
+ s.metadata = { 'funding_uri' => 'https://github.com/sponsors/coorasse' }
13
14
  s.summary = 'Simple authorization solution for Rails.'
14
15
  s.description = 'Simple authorization solution for Rails. All permissions are stored in a single location.'
15
16
  s.platform = Gem::Platform::RUBY
data/lib/cancan.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cancan/version'
4
+ require 'cancan/config'
4
5
  require 'cancan/parameter_validators'
5
6
  require 'cancan/ability'
6
7
  require 'cancan/rule'
@@ -16,6 +17,7 @@ require 'cancan/rules_compressor'
16
17
  if defined? ActiveRecord
17
18
  require 'cancan/model_adapters/conditions_extractor'
18
19
  require 'cancan/model_adapters/conditions_normalizer'
20
+ require 'cancan/model_adapters/sti_normalizer'
19
21
  require 'cancan/model_adapters/active_record_adapter'
20
22
  require 'cancan/model_adapters/active_record_4_adapter'
21
23
  require 'cancan/model_adapters/active_record_5_adapter'
@@ -302,7 +302,11 @@ module CanCan
302
302
 
303
303
  def alternative_subjects(subject)
304
304
  subject = subject.class unless subject.is_a?(Module)
305
- [:all, *subject.ancestors, subject.class.to_s]
305
+ if subject.respond_to?(:subclasses) && defined?(ActiveRecord::Base) && subject < ActiveRecord::Base
306
+ [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s]
307
+ else
308
+ [:all, *subject.ancestors, subject.class.to_s]
309
+ end
306
310
  end
307
311
  end
308
312
  end
@@ -19,12 +19,13 @@ module CanCan
19
19
  end
20
20
 
21
21
  def add_rule_to_index(rule, position)
22
- @rules_index ||= Hash.new { |h, k| h[k] = [] }
22
+ @rules_index ||= {}
23
23
 
24
24
  subjects = rule.subjects.compact
25
25
  subjects << :all if subjects.empty?
26
26
 
27
27
  subjects.each do |subject|
28
+ @rules_index[subject] ||= []
28
29
  @rules_index[subject] << position
29
30
  end
30
31
  end
@@ -48,7 +49,9 @@ module CanCan
48
49
  rules
49
50
  else
50
51
  positions = @rules_index.values_at(subject, *alternative_subjects(subject))
51
- positions.flatten!.sort!
52
+ positions.compact!
53
+ positions.flatten!
54
+ positions.sort!
52
55
  positions.map { |i| @rules[i] }
53
56
  end
54
57
  end
@@ -0,0 +1,26 @@
1
+ # This class is responsible for matching classes and their subclasses as well as
2
+ # upmatching classes to their ancestors.
3
+ # This is used to generate sti connections
4
+ class SubjectClassMatcher
5
+ def self.matches_subject_class?(subjects, subject)
6
+ subjects.any? do |sub|
7
+ has_subclasses = subject.respond_to?(:subclasses)
8
+ matching_class_check(subject, sub, has_subclasses)
9
+ end
10
+ end
11
+
12
+ def self.matching_class_check(subject, sub, has_subclasses)
13
+ matches = matches_class_or_is_related(subject, sub)
14
+ if has_subclasses
15
+ matches || subject.subclasses.include?(sub)
16
+ else
17
+ matches
18
+ end
19
+ end
20
+
21
+ def self.matches_class_or_is_related(subject, sub)
22
+ sub.is_a?(Module) && (subject.is_a?(sub) ||
23
+ subject.class.to_s == sub.to_s ||
24
+ (subject.is_a?(Module) && subject.ancestors.include?(sub)))
25
+ end
26
+ end
@@ -78,7 +78,7 @@ module CanCan
78
78
 
79
79
  def hash_condition_match?(attribute, value)
80
80
  if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
81
- attribute.any? { |element| matches_conditions_hash?(element, value) }
81
+ attribute.to_a.any? { |element| matches_conditions_hash?(element, value) }
82
82
  else
83
83
  attribute && matches_conditions_hash?(attribute, value)
84
84
  end
@@ -97,7 +97,10 @@ module CanCan
97
97
  end
98
98
 
99
99
  def conditions_empty?
100
- @conditions == {} || @conditions.nil?
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?
101
104
  end
102
105
  end
103
106
  end
@@ -0,0 +1,74 @@
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
+ return @accessible_by_strategy if @accessible_by_strategy
25
+
26
+ @accessible_by_strategy = default_accessible_by_strategy
27
+ end
28
+
29
+ def self.default_accessible_by_strategy
30
+ if does_not_support_subquery_strategy?
31
+ # see https://github.com/CanCanCommunity/cancancan/pull/655 for where this was added
32
+ # the `subquery` strategy (from https://github.com/CanCanCommunity/cancancan/pull/619
33
+ # only works in Rails 5 and higher
34
+ :left_join
35
+ else
36
+ :subquery
37
+ end
38
+ end
39
+
40
+ def self.accessible_by_strategy=(value)
41
+ validate_accessible_by_strategy!(value)
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.with_accessible_by_strategy(value)
51
+ return yield if value == accessible_by_strategy
52
+
53
+ validate_accessible_by_strategy!(value)
54
+
55
+ begin
56
+ strategy_was = accessible_by_strategy
57
+ @accessible_by_strategy = value
58
+ yield
59
+ ensure
60
+ @accessible_by_strategy = strategy_was
61
+ end
62
+ end
63
+
64
+ def self.validate_accessible_by_strategy!(value)
65
+ return if valid_accessible_by_strategies.include?(value)
66
+
67
+ raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}"
68
+ end
69
+
70
+ def self.does_not_support_subquery_strategy?
71
+ !defined?(CanCan::ModelAdapters::ActiveRecordAdapter) ||
72
+ CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
73
+ end
74
+ end
@@ -58,5 +58,13 @@ module CanCan
58
58
  def to_s
59
59
  @message || @default_message
60
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
61
69
  end
62
70
  end
@@ -5,7 +5,7 @@ module CanCan
5
5
  class AbstractAdapter
6
6
  def self.inherited(subclass)
7
7
  @subclasses ||= []
8
- @subclasses << subclass
8
+ @subclasses.insert(0, subclass)
9
9
  end
10
10
 
11
11
  def self.adapter_class(model_class)
@@ -34,10 +34,8 @@ module CanCan
34
34
  # look inside the where clause to decide to outer join tables
35
35
  # you're using in the where. Instead, `references()` is required
36
36
  # in addition to `includes()` to force the outer join.
37
- def build_relation(*where_conditions)
38
- relation = @model_class.where(*where_conditions)
39
- relation = relation.includes(joins).references(joins) if joins.present?
40
- relation
37
+ def build_joins_relation(relation, *_where_conditions)
38
+ relation.includes(joins).references(joins)
41
39
  end
42
40
 
43
41
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
@@ -21,13 +21,19 @@ module CanCan
21
21
 
22
22
  private
23
23
 
24
- def build_relation(*where_conditions)
25
- relation = @model_class.where(*where_conditions)
26
- relation = relation.left_joins(joins).distinct if joins.present?
27
- 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
28
35
  end
29
36
 
30
- # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
31
37
  def sanitize_sql(conditions)
32
38
  if conditions.is_a?(Hash)
33
39
  sanitize_sql_activerecord5(conditions)
@@ -41,11 +47,7 @@ module CanCan
41
47
  table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
42
48
  predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
43
49
 
44
- conditions = predicate_builder.resolve_column_aliases(conditions)
45
-
46
- conditions.stringify_keys!
47
-
48
- predicate_builder.build_from_hash(conditions).map { |b| visit_nodes(b) }.join(' AND ')
50
+ predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
49
51
  end
50
52
 
51
53
  def visit_nodes(node)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'conditions_extractor.rb'
4
- require 'cancan/rules_compressor'
5
3
  module CanCan
6
4
  module ModelAdapters
7
5
  class ActiveRecordAdapter < AbstractAdapter
@@ -16,6 +14,7 @@ module CanCan
16
14
  def initialize(model_class, rules)
17
15
  super
18
16
  @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
17
+ StiNormalizer.normalize(@compressed_rules)
19
18
  ConditionsNormalizer.normalize(model_class, @compressed_rules)
20
19
  end
21
20
 
@@ -60,6 +59,14 @@ module CanCan
60
59
  end
61
60
  end
62
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
+
63
70
  # Returns the associations used in conditions for the :joins option of a search.
64
71
  # See ModelAdditions#accessible_by
65
72
  def joins
@@ -0,0 +1,39 @@
1
+ # this class is responsible for detecting sti classes and creating new rules for the
2
+ # relevant subclasses, using the inheritance_column as a merger
3
+ module CanCan
4
+ module ModelAdapters
5
+ class StiNormalizer
6
+ class << self
7
+ def normalize(rules)
8
+ rules_cache = []
9
+ return unless defined?(ActiveRecord::Base)
10
+
11
+ rules.delete_if do |rule|
12
+ subjects = rule.subjects.select do |subject|
13
+ update_rule(subject, rule, rules_cache)
14
+ end
15
+ subjects.length == rule.subjects.length
16
+ end
17
+ rules_cache.each { |rule| rules.push(rule) }
18
+ end
19
+
20
+ private
21
+
22
+ def update_rule(subject, rule, rules_cache)
23
+ return false unless subject.respond_to?(:descends_from_active_record?)
24
+ return false if subject == :all || subject.descends_from_active_record?
25
+ return false unless subject < ActiveRecord::Base
26
+
27
+ rules_cache.push(build_rule_for_subclass(rule, subject))
28
+ true
29
+ end
30
+
31
+ # create a new rule for the subclasses that links on the inheritance_column
32
+ def build_rule_for_subclass(rule, subject)
33
+ CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass,
34
+ rule.conditions.merge(subject.inheritance_column => subject.name), rule.block)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -20,8 +20,10 @@ module CanCan
20
20
  # @articles = Article.accessible_by(current_ability, :update)
21
21
  #
22
22
  # Here only the articles which the user can update are returned.
23
- def accessible_by(ability, action = :index)
24
- ability.model_adapter(self, action).database_records
23
+ def accessible_by(ability, action = :index, strategy: CanCan.accessible_by_strategy)
24
+ CanCan.with_accessible_by_strategy(strategy) do
25
+ ability.model_adapter(self, action).database_records
26
+ end
25
27
  end
26
28
  end
27
29
 
@@ -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,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'conditions_matcher.rb'
4
+ require_relative 'class_matcher.rb'
5
+ require_relative 'relevant.rb'
6
+
4
7
  module CanCan
5
8
  # This class is used internally and should only be called through Ability.
6
9
  # it holds the information about a "can" call made on Ability and provides
7
10
  # helpful methods to determine permission checking and conditions hash generation.
8
11
  class Rule # :nodoc:
9
12
  include ConditionsMatcher
13
+ include Relevant
10
14
  include ParameterValidators
11
- attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes
15
+ attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes, :block
12
16
  attr_writer :expanded_actions, :conditions
13
17
 
14
18
  # The first argument when initializing is the base_behavior which is a true/false
@@ -24,9 +28,9 @@ module CanCan
24
28
  raise Error, "Subject is required for #{action}" if action && subject.nil?
25
29
 
26
30
  @base_behavior = base_behavior
27
- @actions = Array(action)
28
- @subjects = Array(subject)
29
- @attributes = Array(attributes)
31
+ @actions = wrap(action)
32
+ @subjects = wrap(subject)
33
+ @attributes = wrap(attributes)
30
34
  @conditions = extra_args || {}
31
35
  @block = block
32
36
  end
@@ -57,12 +61,6 @@ module CanCan
57
61
  (!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions))
58
62
  end
59
63
 
60
- # Matches both the action, subject, and attribute, not necessarily the conditions
61
- def relevant?(action, subject)
62
- subject = subject.values.first if subject.class == Hash
63
- @match_all || (matches_action?(action) && matches_subject?(subject))
64
- end
65
-
66
64
  def only_block?
67
65
  conditions_empty? && @block
68
66
  end
@@ -113,11 +111,7 @@ module CanCan
113
111
  end
114
112
 
115
113
  def matches_subject_class?(subject)
116
- @subjects.any? do |sub|
117
- sub.is_a?(Module) && (subject.is_a?(sub) ||
118
- subject.class.to_s == sub.to_s ||
119
- (subject.is_a?(Module) && subject.ancestors.include?(sub)))
120
- end
114
+ SubjectClassMatcher.matches_subject_class?(@subjects, subject)
121
115
  end
122
116
 
123
117
  def parse_attributes_from_extra_args(args)
@@ -132,5 +126,15 @@ module CanCan
132
126
  raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\
133
127
  "Check \":#{action} #{subject}\" ability."
134
128
  end
129
+
130
+ def wrap(object)
131
+ if object.nil?
132
+ []
133
+ elsif object.respond_to?(:to_ary)
134
+ object.to_ary || [object]
135
+ else
136
+ [object]
137
+ end
138
+ end
135
139
  end
136
140
  end
@@ -3,10 +3,12 @@
3
3
  module CanCan
4
4
  module UnauthorizedMessageResolver
5
5
  def unauthorized_message(action, subject)
6
+ subject = subject.values.last if subject.is_a?(Hash)
6
7
  keys = unauthorized_message_keys(action, subject)
7
- variables = { action: action.to_s }
8
+ variables = {}
9
+ variables[:action] = I18n.translate("actions.#{action}", default: action.to_s)
8
10
  variables[:subject] = translate_subject(subject)
9
- message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + ['']))
11
+ message = I18n.translate(keys.shift, **variables.merge(scope: :unauthorized, default: keys + ['']))
10
12
  message.blank? ? nil : message
11
13
  end
12
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanCan
4
- VERSION = '3.0.2'.freeze
4
+ VERSION = '3.3.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cancancan
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi (Renuo AG)
8
8
  - Bryan Rite
9
9
  - Ryan Bates
10
10
  - Richard Wilson
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2020-01-17 00:00:00.000000000 Z
14
+ date: 2021-06-21 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: appraisal
@@ -115,7 +115,9 @@ files:
115
115
  - lib/cancan/ability/actions.rb
116
116
  - lib/cancan/ability/rules.rb
117
117
  - lib/cancan/ability/strong_parameter_support.rb
118
+ - lib/cancan/class_matcher.rb
118
119
  - lib/cancan/conditions_matcher.rb
120
+ - lib/cancan/config.rb
119
121
  - lib/cancan/controller_additions.rb
120
122
  - lib/cancan/controller_resource.rb
121
123
  - lib/cancan/controller_resource_builder.rb
@@ -132,8 +134,10 @@ files:
132
134
  - lib/cancan/model_adapters/conditions_extractor.rb
133
135
  - lib/cancan/model_adapters/conditions_normalizer.rb
134
136
  - lib/cancan/model_adapters/default_adapter.rb
137
+ - lib/cancan/model_adapters/sti_normalizer.rb
135
138
  - lib/cancan/model_additions.rb
136
139
  - lib/cancan/parameter_validators.rb
140
+ - lib/cancan/relevant.rb
137
141
  - lib/cancan/rule.rb
138
142
  - lib/cancan/rules_compressor.rb
139
143
  - lib/cancan/unauthorized_message_resolver.rb
@@ -145,8 +149,9 @@ files:
145
149
  homepage: https://github.com/CanCanCommunity/cancancan
146
150
  licenses:
147
151
  - MIT
148
- metadata: {}
149
- post_install_message:
152
+ metadata:
153
+ funding_uri: https://github.com/sponsors/coorasse
154
+ post_install_message:
150
155
  rdoc_options: []
151
156
  require_paths:
152
157
  - lib
@@ -162,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
167
  version: '0'
163
168
  requirements: []
164
169
  rubygems_version: 3.0.6
165
- signing_key:
170
+ signing_key:
166
171
  specification_version: 4
167
172
  summary: Simple authorization solution for Rails.
168
173
  test_files: []