cancancan 3.0.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []