cancancan 2.3.0 → 3.2.2

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 (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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f63f1d6ab266068caab6e7baf2b628ae72a7e03a134116eda91473861b9df359
4
- data.tar.gz: fbf6337f058ece9a2f01c990a55398489e0ea56888f0a5ce3b9f8d5e203c31ae
3
+ metadata.gz: 64c041d800f42e86e488e7c0f46bad09c21ea0d075142d0693deed634feb1c37
4
+ data.tar.gz: 78fbf14fd5a661c92c76bd9204e7996c096cec7f82e1b2fd1f73d33acb23d99b
5
5
  SHA512:
6
- metadata.gz: 24fcc98ce0592b263add65cf0bc75a7020d75777fea7c6902216f97bbc9c13a36b4d379194a142cd8a5e5a24dc1ae82c82a61d6d99143c30a9afe11133048528
7
- data.tar.gz: 8873b440698a941314f67a748db86c2abe33e89417ae54d9f35769c29f904edc9eff5736d0bf55d53badc494b9bca9e0ac1761c1920067238628d23dc8e0c643
6
+ metadata.gz: 95f9e1b7c6bb47b5d8ed2172752f8c7e7d9c4fb4567e1b0f43a6bf97eae4ed7d4b2557e85a24ce92ddd526e870b538e59d05767a62705a072a2532208d81cd67
7
+ data.tar.gz: 0b24f5078921f372af6fa98d180cc1f9dd79c6d17cd502fac4bdd3e87ce196e2a714070133d63caaf916840ad5bc70c938a435949f1d50889b2dd5a89fde590c
data/cancancan.gemspec CHANGED
@@ -1,6 +1,6 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'cancan/version'
6
6
 
@@ -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
@@ -20,9 +21,9 @@ Gem::Specification.new do |s|
20
21
 
21
22
  s.required_ruby_version = '>= 2.2.0'
22
23
 
23
- s.add_development_dependency 'bundler', '~> 1.3'
24
- s.add_development_dependency 'rubocop', '~> 0.48.1'
24
+ s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
25
+ s.add_development_dependency 'bundler', '~> 2.0'
25
26
  s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
26
27
  s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
27
- s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
28
+ s.add_development_dependency 'rubocop', '~> 0.63.1'
28
29
  end
data/init.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cancan'
data/lib/cancan.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cancan/version'
4
+ require 'cancan/config'
5
+ require 'cancan/parameter_validators'
2
6
  require 'cancan/ability'
3
7
  require 'cancan/rule'
4
8
  require 'cancan/controller_resource'
@@ -12,6 +16,8 @@ require 'cancan/rules_compressor'
12
16
 
13
17
  if defined? ActiveRecord
14
18
  require 'cancan/model_adapters/conditions_extractor'
19
+ require 'cancan/model_adapters/conditions_normalizer'
20
+ require 'cancan/model_adapters/sti_normalizer'
15
21
  require 'cancan/model_adapters/active_record_adapter'
16
22
  require 'cancan/model_adapters/active_record_4_adapter'
17
23
  require 'cancan/model_adapters/active_record_5_adapter'
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'ability/rules.rb'
2
4
  require_relative 'ability/actions.rb'
5
+ require_relative 'unauthorized_message_resolver.rb'
6
+ require_relative 'ability/strong_parameter_support'
7
+
3
8
  module CanCan
4
9
  # This module is designed to be included into an Ability class. This will
5
10
  # provide the "can" methods for defining and checking abilities.
@@ -19,6 +24,8 @@ module CanCan
19
24
  module Ability
20
25
  include CanCan::Ability::Rules
21
26
  include CanCan::Ability::Actions
27
+ include CanCan::UnauthorizedMessageResolver
28
+ include StrongParameterSupport
22
29
 
23
30
  # Check if the user has permission to perform a given action on an object.
24
31
  #
@@ -64,10 +71,10 @@ module CanCan
64
71
  # end
65
72
  #
66
73
  # Also see the RSpec Matchers to aid in testing.
67
- def can?(action, subject, *extra_args)
74
+ def can?(action, subject, attribute = nil, *extra_args)
68
75
  match = extract_subjects(subject).lazy.map do |a_subject|
69
76
  relevant_rules_for_match(action, a_subject).detect do |rule|
70
- rule.matches_conditions?(action, a_subject, extra_args)
77
+ rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
71
78
  end
72
79
  end.reject(&:nil?).first
73
80
  match ? match.base_behavior : false
@@ -134,8 +141,8 @@ module CanCan
134
141
  # # check the database and return true/false
135
142
  # end
136
143
  #
137
- def can(action = nil, subject = nil, conditions = nil, &block)
138
- add_rule(Rule.new(true, action, subject, conditions, block))
144
+ def can(action = nil, subject = nil, *attributes_and_conditions, &block)
145
+ add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
139
146
  end
140
147
 
141
148
  # Defines an ability which cannot be done. Accepts the same arguments as "can".
@@ -150,8 +157,8 @@ module CanCan
150
157
  # product.invisible?
151
158
  # end
152
159
  #
153
- def cannot(action = nil, subject = nil, conditions = nil, &block)
154
- add_rule(Rule.new(false, action, subject, conditions, block))
160
+ def cannot(action = nil, subject = nil, *attributes_and_conditions, &block)
161
+ add_rule(Rule.new(false, action, subject, *attributes_and_conditions, &block))
155
162
  end
156
163
 
157
164
  # User shouldn't specify targets with names of real actions or it will cause Seg fault
@@ -167,10 +174,7 @@ module CanCan
167
174
 
168
175
  # See ControllerAdditions#authorize! for documentation.
169
176
  def authorize!(action, subject, *args)
170
- message = nil
171
- if args.last.is_a?(Hash) && args.last.key?(:message)
172
- message = args.pop[:message]
173
- end
177
+ message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil
174
178
  if cannot?(action, subject, *args)
175
179
  message ||= unauthorized_message(action, subject)
176
180
  raise AccessDenied.new(message, action, subject, args)
@@ -178,14 +182,6 @@ module CanCan
178
182
  subject
179
183
  end
180
184
 
181
- def unauthorized_message(action, subject)
182
- keys = unauthorized_message_keys(action, subject)
183
- variables = { action: action.to_s }
184
- variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase
185
- message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + ['']))
186
- message.blank? ? nil : message
187
- end
188
-
189
185
  def attributes_for(action, subject)
190
186
  attributes = {}
191
187
  relevant_rules(action, subject).map do |rule|
@@ -202,12 +198,13 @@ module CanCan
202
198
  relevant_rules(action, subject).any?(&:only_raw_sql?)
203
199
  end
204
200
 
205
- # Copies all rules of the given +CanCan::Ability+ and adds them to +self+.
201
+ # Copies all rules and aliased actions of the given +CanCan::Ability+ and adds them to +self+.
206
202
  # class ReadAbility
207
203
  # include CanCan::Ability
208
204
  #
209
205
  # def initialize
210
206
  # can :read, User
207
+ # alias_action :show, :index, to: :see
211
208
  # end
212
209
  # end
213
210
  #
@@ -216,6 +213,7 @@ module CanCan
216
213
  #
217
214
  # def initialize
218
215
  # can :edit, User
216
+ # alias_action :create, :update, to: :modify
219
217
  # end
220
218
  # end
221
219
  #
@@ -223,11 +221,35 @@ module CanCan
223
221
  # read_ability.can? :edit, User.new #=> false
224
222
  # read_ability.merge(WritingAbility.new)
225
223
  # read_ability.can? :edit, User.new #=> true
224
+ # read_ability.aliased_actions #=> [:see => [:show, :index], :modify => [:create, :update]]
225
+ #
226
+ # If there are collisions when merging the +aliased_actions+, the actions on +self+ will be
227
+ # overwritten.
226
228
  #
229
+ # class ReadAbility
230
+ # include CanCan::Ability
231
+ #
232
+ # def initialize
233
+ # alias_action :show, :index, to: :see
234
+ # end
235
+ # end
236
+ #
237
+ # class ShowAbility
238
+ # include CanCan::Ability
239
+ #
240
+ # def initialize
241
+ # alias_action :show, to: :see
242
+ # end
243
+ # end
244
+ #
245
+ # read_ability = ReadAbility.new
246
+ # read_ability.merge(ShowAbility)
247
+ # read_ability.aliased_actions #=> [:see => [:show]]
227
248
  def merge(ability)
228
249
  ability.rules.each do |rule|
229
250
  add_rule(rule.dup)
230
251
  end
252
+ @aliased_actions = aliased_actions.merge(ability.aliased_actions)
231
253
  self
232
254
  end
233
255
 
@@ -239,10 +261,13 @@ module CanCan
239
261
  #
240
262
  # Where can_hash and cannot_hash are formatted thusly:
241
263
  # {
242
- # action: array_of_objects
264
+ # action: { subject: [attributes] }
243
265
  # }
244
266
  def permissions
245
- permissions_list = { can: {}, cannot: {} }
267
+ permissions_list = {
268
+ can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } },
269
+ cannot: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }
270
+ }
246
271
  rules.each { |rule| extract_rule_in_permissions(permissions_list, rule) }
247
272
  permissions_list
248
273
  end
@@ -250,8 +275,9 @@ module CanCan
250
275
  def extract_rule_in_permissions(permissions_list, rule)
251
276
  expand_actions(rule.actions).each do |action|
252
277
  container = rule.base_behavior ? :can : :cannot
253
- permissions_list[container][action] ||= []
254
- permissions_list[container][action] += rule.subjects.map(&:to_s)
278
+ rule.subjects.each do |subject|
279
+ permissions_list[container][action][subject.to_s] += rule.attributes
280
+ end
255
281
  end
256
282
  end
257
283
 
@@ -276,7 +302,11 @@ module CanCan
276
302
 
277
303
  def alternative_subjects(subject)
278
304
  subject = subject.class unless subject.is_a?(Module)
279
- [: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
280
310
  end
281
311
  end
282
312
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module Ability
3
5
  module Actions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module Ability
3
5
  module Rules
@@ -31,6 +33,7 @@ module CanCan
31
33
  # This does not take into consideration any hash conditions or block statements
32
34
  def relevant_rules(action, subject)
33
35
  return [] unless @rules
36
+
34
37
  relevant = possible_relevant_rules(subject).select do |rule|
35
38
  rule.expanded_actions = expand_actions(rule.actions)
36
39
  rule.relevant? action, subject
@@ -53,19 +56,23 @@ module CanCan
53
56
  def relevant_rules_for_match(action, subject)
54
57
  relevant_rules(action, subject).each do |rule|
55
58
  next unless rule.only_raw_sql?
59
+
56
60
  raise Error,
57
61
  "The can? and cannot? call cannot be used with a raw sql 'can' definition."\
58
- " The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
62
+ " The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
59
63
  end
60
64
  end
61
65
 
62
66
  def relevant_rules_for_query(action, subject)
63
- relevant_rules(action, subject).each do |rule|
64
- if rule.only_block?
65
- raise Error, "The accessible_by call cannot be used with a block 'can' definition."\
66
- " The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
67
- end
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}"
68
74
  end
75
+ rules
69
76
  end
70
77
 
71
78
  # Optimizes the order of the rules, so that rules with the :all subject are evaluated first.
@@ -75,6 +82,7 @@ module CanCan
75
82
  (first_can_in_group = -1) && next unless rule.base_behavior
76
83
  (first_can_in_group = i) && next if first_can_in_group == -1
77
84
  next unless rule.subjects == [:all]
85
+
78
86
  rules[i] = rules[first_can_in_group]
79
87
  rules[first_can_in_group] = rule
80
88
  first_can_in_group += 1
@@ -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,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
@@ -1,26 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ConditionsMatcher
3
5
  # Matches the block or conditions hash
4
- def matches_conditions?(action, subject, extra_args)
6
+ def matches_conditions?(action, subject, attribute = nil, *extra_args)
5
7
  return call_block_with_all(action, subject, extra_args) if @match_all
6
- return @block.call(subject, *extra_args) if @block && !subject_class?(subject)
7
- matches_non_block_conditions(subject)
8
+ return matches_block_conditions(subject, attribute, *extra_args) if @block
9
+ return matches_non_block_conditions(subject) unless conditions_empty?
10
+
11
+ true
8
12
  end
9
13
 
10
14
  private
11
15
 
12
16
  def subject_class?(subject)
13
17
  klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
14
- klass == Class || klass == Module
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)
15
25
  end
16
26
 
17
27
  def matches_non_block_conditions(subject)
18
- if @conditions.is_a?(Hash)
19
- return nested_subject_matches_conditions?(subject) if subject.class == Hash
20
- return matches_conditions_hash?(subject) unless subject_class?(subject)
21
- end
28
+ return nested_subject_matches_conditions?(subject) if subject.class == Hash
29
+ return matches_conditions_hash?(subject) unless subject_class?(subject)
30
+
22
31
  # Don't stop at "cannot" definitions when there are conditions.
23
- conditions_empty? ? true : @base_behavior
32
+ @base_behavior
24
33
  end
25
34
 
26
35
  def nested_subject_matches_conditions?(subject_hash)
@@ -34,6 +43,7 @@ module CanCan
34
43
  # matches_conditions_hash?(subject, conditions)
35
44
  def matches_conditions_hash?(subject, conditions = @conditions)
36
45
  return true if conditions.empty?
46
+
37
47
  adapter = model_adapter(subject)
38
48
 
39
49
  if adapter.override_conditions_hash_matching?(subject, conditions)
@@ -68,13 +78,13 @@ module CanCan
68
78
 
69
79
  def hash_condition_match?(attribute, value)
70
80
  if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
71
- attribute.any? { |element| matches_conditions_hash?(element, value) }
81
+ attribute.to_a.any? { |element| matches_conditions_hash?(element, value) }
72
82
  else
73
83
  attribute && matches_conditions_hash?(attribute, value)
74
84
  end
75
85
  end
76
86
 
77
- def call_block_with_all(action, subject, extra_args)
87
+ def call_block_with_all(action, subject, *extra_args)
78
88
  if subject.class == Class
79
89
  @block.call(action, subject, nil, *extra_args)
80
90
  else
@@ -87,7 +97,10 @@ module CanCan
87
97
  end
88
98
 
89
99
  def conditions_empty?
90
- @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?
91
104
  end
92
105
  end
93
106
  end