cancancan 3.6.2 → 3.7.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 +4 -4
- data/cancancan.gemspec +3 -2
- data/lib/cancan/ability/actions.rb +6 -3
- data/lib/cancan/ability/rules.rb +28 -1
- data/lib/cancan/ability/strong_parameter_support.rb +29 -15
- data/lib/cancan/ability.rb +13 -10
- data/lib/cancan/class_matcher.rb +1 -1
- data/lib/cancan/conditions_matcher.rb +20 -11
- data/lib/cancan/config.rb +21 -7
- data/lib/cancan/controller_additions.rb +3 -2
- data/lib/cancan/controller_resource.rb +247 -17
- data/lib/cancan/model_adapters/abstract_adapter.rb +9 -0
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +28 -11
- data/lib/cancan/model_adapters/active_record_adapter.rb +81 -23
- data/lib/cancan/model_adapters/conditions_extractor.rb +94 -38
- data/lib/cancan/model_adapters/conditions_normalizer.rb +9 -1
- data/lib/cancan/model_adapters/sti_normalizer.rb +6 -13
- data/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +2 -2
- data/lib/cancan/relevant.rb +4 -0
- data/lib/cancan/rule.rb +3 -2
- data/lib/cancan/rules_compressor.rb +3 -2
- data/lib/cancan/sti_detector.rb +1 -1
- data/lib/cancan/unauthorized_message_resolver.rb +1 -0
- data/lib/cancan/version.rb +1 -1
- metadata +4 -14
- data/lib/cancan/controller_resource_builder.rb +0 -26
- data/lib/cancan/controller_resource_finder.rb +0 -42
- data/lib/cancan/controller_resource_loader.rb +0 -120
- data/lib/cancan/controller_resource_name_finder.rb +0 -23
- data/lib/cancan/controller_resource_sanitizer.rb +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a89f93b55a85e54a08313de3a4cb8bad1886b3a1325c39953a8667fd58734be9
|
|
4
|
+
data.tar.gz: fdbf729ba98180067f749415815005bb2362e84f3f04014248b88f7974bf3982
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ae7721feae9b7dfc2e411878bb600f08cf8edfbe42a01989b6c09ad162443714643898129ad15ca24b5f8d1bf30fb916016eac7baf466671a18a0f292f69f5d
|
|
7
|
+
data.tar.gz: '086c7fdf46ab6caa794c425681557469deffb8038f88fc7aba00e1a361fe8dd5b85caaa18884916e5ce908c7e6efec0f02b3b337a1405159b12d785ea2dc6f5d'
|
data/cancancan.gemspec
CHANGED
|
@@ -10,7 +10,8 @@ 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 = { '
|
|
13
|
+
s.metadata = { 'changelog_uri' => 'https://github.com/CanCanCommunity/cancancan/blob/develop/CHANGELOG.md',
|
|
14
|
+
'funding_uri' => 'https://github.com/sponsors/coorasse' }
|
|
14
15
|
s.summary = 'Simple authorization solution for Rails.'
|
|
15
16
|
s.description = 'Simple authorization solution for Rails. All permissions are stored in a single location.'
|
|
16
17
|
s.platform = Gem::Platform::RUBY
|
|
@@ -23,7 +24,7 @@ Gem::Specification.new do |s|
|
|
|
23
24
|
|
|
24
25
|
s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
|
|
25
26
|
s.add_development_dependency 'bundler', '~> 2.0'
|
|
26
|
-
s.add_development_dependency 'rake'
|
|
27
|
+
s.add_development_dependency 'rake'
|
|
27
28
|
s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
|
|
28
29
|
s.add_development_dependency 'rubocop', '~> 1.31.1'
|
|
29
30
|
end
|
|
@@ -33,10 +33,12 @@ module CanCan
|
|
|
33
33
|
#
|
|
34
34
|
# This way one can use params[:action] in the controller to determine the permission.
|
|
35
35
|
def alias_action(*args)
|
|
36
|
-
target = args.pop[:to]
|
|
37
|
-
|
|
36
|
+
target = args.pop[:to]&.to_sym
|
|
37
|
+
actions = args.map(&:to_sym)
|
|
38
|
+
validate_target(target, actions)
|
|
38
39
|
aliased_actions[target] ||= []
|
|
39
|
-
aliased_actions[target] +=
|
|
40
|
+
aliased_actions[target] += actions
|
|
41
|
+
@expanded_actions = nil
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
|
|
@@ -47,6 +49,7 @@ module CanCan
|
|
|
47
49
|
# Removes previously aliased actions including the defaults.
|
|
48
50
|
def clear_aliased_actions
|
|
49
51
|
@aliased_actions = {}
|
|
52
|
+
@expanded_actions = nil
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
private
|
data/lib/cancan/ability/rules.rb
CHANGED
|
@@ -35,6 +35,9 @@ module CanCan
|
|
|
35
35
|
def relevant_rules(action, subject)
|
|
36
36
|
return [] unless @rules
|
|
37
37
|
|
|
38
|
+
# Actions are symbols in the rules, so 'destroy' must also match a `cannot :destroy`.
|
|
39
|
+
action = action.to_sym if action.is_a?(String)
|
|
40
|
+
|
|
38
41
|
relevant = possible_relevant_rules(subject).select do |rule|
|
|
39
42
|
rule.expanded_actions = expand_actions(rule.actions)
|
|
40
43
|
rule.relevant? action, subject
|
|
@@ -56,8 +59,32 @@ module CanCan
|
|
|
56
59
|
end
|
|
57
60
|
end
|
|
58
61
|
|
|
62
|
+
# Returns nil when no rule matches. A class that uses single table inheritance is checked together with
|
|
63
|
+
# its subclasses, each one with the rules of its own ancestors: the check passes when any of them passes,
|
|
64
|
+
# which is also when `accessible_by` can return records.
|
|
65
|
+
def subject_can?(action, subject, attribute, *extra_args)
|
|
66
|
+
results = sti_classes(subject).map do |klass|
|
|
67
|
+
matching_rule(action, klass, attribute, *extra_args)&.base_behavior
|
|
68
|
+
end
|
|
69
|
+
results.compact.empty? ? nil : results.any?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def matching_rule(action, subject, attribute, *extra_args)
|
|
73
|
+
relevant_rules_for_match(action, subject).detect do |rule|
|
|
74
|
+
rule.matches_conditions?(action, subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sti_classes(subject)
|
|
79
|
+
return [subject] unless defined?(ActiveRecord::Base) && subject.is_a?(Class) && subject < ActiveRecord::Base
|
|
80
|
+
|
|
81
|
+
[subject, *subject.descendants.select { |klass| StiDetector.sti_class?(klass) }]
|
|
82
|
+
end
|
|
83
|
+
|
|
59
84
|
def relevant_rules_for_match(action, subject)
|
|
60
|
-
relevant_rules(action, subject)
|
|
85
|
+
rules = relevant_rules(action, subject)
|
|
86
|
+
rules = rules.reject { |rule| rule.only_for_subclasses_of?(subject) } if subject.is_a?(Class)
|
|
87
|
+
rules.each do |rule|
|
|
61
88
|
next unless rule.only_raw_sql?
|
|
62
89
|
|
|
63
90
|
raise Error,
|
|
@@ -4,34 +4,48 @@ module CanCan
|
|
|
4
4
|
module Ability
|
|
5
5
|
module StrongParameterSupport
|
|
6
6
|
# Returns an array of attributes suitable for use with strong parameters
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
def permitted_attributes(action, subject)
|
|
8
|
+
fold_attributes(action, subject) { |rule, set| apply_attributes(rule, subject, set) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# This method returns all attributes which are explicity disallowed
|
|
12
|
+
def disallowed_attributes(action, subject)
|
|
13
|
+
fold_attributes(action, subject) do |rule, set|
|
|
14
|
+
attributes = get_attributes(rule, subject)
|
|
15
|
+
rule.base_behavior ? set.subtract(attributes) : set.merge(attributes)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Reversing the relevant rules is important. Normal order means that 'cannot'
|
|
9
22
|
# rules will come before 'can' rules. However, you can't remove attributes before
|
|
10
23
|
# they are added. The 'reverse' is so that attributes will be added before the
|
|
11
24
|
# 'cannot' rules remove them.
|
|
12
|
-
def
|
|
25
|
+
def fold_attributes(action, subject, &block)
|
|
13
26
|
relevant_rules(action, subject)
|
|
14
27
|
.reverse
|
|
15
28
|
.select { |rule| rule.matches_conditions? action, subject }
|
|
16
|
-
.each_with_object(Set.new
|
|
17
|
-
|
|
18
|
-
# add attributes for 'can', remove them for 'cannot'
|
|
19
|
-
rule.base_behavior ? set.merge(attributes) : set.subtract(attributes)
|
|
20
|
-
end.to_a
|
|
29
|
+
.each_with_object(Set.new, &block)
|
|
30
|
+
.to_a
|
|
21
31
|
end
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
def apply_attributes(rule, subject, set)
|
|
34
|
+
if rule.base_behavior
|
|
35
|
+
set.merge(get_attributes(rule, subject))
|
|
36
|
+
elsif rule.attributes.empty?
|
|
37
|
+
set.clear
|
|
38
|
+
else
|
|
39
|
+
set.subtract(rule.attributes)
|
|
40
|
+
end
|
|
28
41
|
end
|
|
29
42
|
|
|
30
43
|
def get_attributes(rule, subject)
|
|
31
|
-
|
|
44
|
+
subject = subject.values.first if subject.is_a?(Hash)
|
|
45
|
+
klass = subject.is_a?(Module) ? subject : subject.class
|
|
32
46
|
# empty attributes is an 'all'
|
|
33
47
|
if rule.attributes.empty? && klass < ActiveRecord::Base
|
|
34
|
-
klass.attribute_names.map(&:to_sym) - Array(klass.primary_key)
|
|
48
|
+
klass.attribute_names.map(&:to_sym) - Array(klass.primary_key).map(&:to_sym)
|
|
35
49
|
else
|
|
36
50
|
rule.attributes
|
|
37
51
|
end
|
data/lib/cancan/ability.rb
CHANGED
|
@@ -49,8 +49,9 @@ module CanCan
|
|
|
49
49
|
#
|
|
50
50
|
# Any additional arguments will be passed into the "can" block definition. This
|
|
51
51
|
# can be used to pass more information about the user's request for example.
|
|
52
|
+
# The block is not called when a class is passed instead of an instance.
|
|
52
53
|
#
|
|
53
|
-
# can? :create,
|
|
54
|
+
# can? :create, @project, request.remote_ip
|
|
54
55
|
#
|
|
55
56
|
# can :create, Project do |project, remote_ip|
|
|
56
57
|
# # ...
|
|
@@ -72,12 +73,11 @@ module CanCan
|
|
|
72
73
|
#
|
|
73
74
|
# Also see the RSpec Matchers to aid in testing.
|
|
74
75
|
def can?(action, subject, attribute = nil, *extra_args)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
match ? match.base_behavior : false
|
|
76
|
+
action = action.to_sym if action.is_a?(String)
|
|
77
|
+
results = extract_subjects(subject).lazy.map do |a_subject|
|
|
78
|
+
subject_can?(action, a_subject, attribute, *extra_args)
|
|
79
|
+
end
|
|
80
|
+
results.reject(&:nil?).first || false
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# Convenience method which works the same as "can?" but returns the opposite value.
|
|
@@ -162,9 +162,11 @@ module CanCan
|
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
# User shouldn't specify targets with names of real actions or it will cause Seg fault
|
|
165
|
-
def validate_target(target)
|
|
165
|
+
def validate_target(target, actions = [])
|
|
166
166
|
error_message = "You can't specify target (#{target}) as alias because it is real action name"
|
|
167
167
|
raise Error, error_message if aliased_actions.values.flatten.include? target
|
|
168
|
+
raise Error, "You can't specify target (#{target}) as alias because it is a reserved action" if target == :manage
|
|
169
|
+
raise Error, "You can't alias the action (#{target}) to itself" if actions.include?(target)
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
def model_adapter(model_class, action)
|
|
@@ -250,6 +252,7 @@ module CanCan
|
|
|
250
252
|
add_rule(rule.dup)
|
|
251
253
|
end
|
|
252
254
|
@aliased_actions = aliased_actions.merge(ability.aliased_actions)
|
|
255
|
+
@expanded_actions = nil
|
|
253
256
|
self
|
|
254
257
|
end
|
|
255
258
|
|
|
@@ -303,9 +306,9 @@ module CanCan
|
|
|
303
306
|
def alternative_subjects(subject)
|
|
304
307
|
subject = subject.class unless subject.is_a?(Module)
|
|
305
308
|
if subject.respond_to?(:subclasses) && defined?(ActiveRecord::Base) && subject < ActiveRecord::Base
|
|
306
|
-
[:all, *(subject.ancestors + subject.
|
|
309
|
+
[:all, *(subject.ancestors + subject.descendants)]
|
|
307
310
|
else
|
|
308
|
-
[:all, *subject.ancestors
|
|
311
|
+
[:all, *subject.ancestors]
|
|
309
312
|
end
|
|
310
313
|
end
|
|
311
314
|
end
|
data/lib/cancan/class_matcher.rb
CHANGED
|
@@ -41,10 +41,11 @@ module CanCan
|
|
|
41
41
|
|
|
42
42
|
adapter = model_adapter(parent)
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# Like for a class, a "cannot" with conditions on the child does not stop the check.
|
|
45
|
+
return false unless @base_behavior || adapter.conditions_only_on_parent?(parent, child, @conditions)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
parent_conditions = @conditions[adapter.parent_condition_name(parent, child)] || {}
|
|
48
|
+
matches_base_parent_conditions = matches_conditions_hash?(parent, parent_conditions)
|
|
48
49
|
|
|
49
50
|
matches_base_parent_conditions &&
|
|
50
51
|
(!adapter.override_nested_subject_conditions_matching?(parent, child, @conditions) ||
|
|
@@ -88,18 +89,26 @@ module CanCan
|
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
def condition_match?(attribute, value)
|
|
92
|
+
return relation_match?(attribute, value) if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Relation)
|
|
93
|
+
|
|
91
94
|
case value
|
|
92
|
-
when Hash
|
|
93
|
-
|
|
94
|
-
when
|
|
95
|
-
|
|
96
|
-
when Enumerable
|
|
97
|
-
value.include?(attribute)
|
|
98
|
-
else
|
|
99
|
-
attribute == value
|
|
95
|
+
when Hash then hash_condition_match?(attribute, value)
|
|
96
|
+
when Range then value.cover?(attribute)
|
|
97
|
+
when Enumerable then value.any? { |element| value_match?(attribute, element) }
|
|
98
|
+
else value_match?(attribute, value)
|
|
100
99
|
end
|
|
101
100
|
end
|
|
102
101
|
|
|
102
|
+
# Like Active Record in the SQL conditions, a symbol matches the string with the same name.
|
|
103
|
+
def value_match?(attribute, value)
|
|
104
|
+
attribute == value || (value.is_a?(Symbol) && attribute == value.to_s)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Like Active Record with a relation as condition value: the selected column, or the primary key, is matched.
|
|
108
|
+
def relation_match?(attribute, relation)
|
|
109
|
+
relation.where((relation.select_values.first || relation.primary_key) => attribute).exists?
|
|
110
|
+
end
|
|
111
|
+
|
|
103
112
|
def hash_condition_match?(attribute, value)
|
|
104
113
|
if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
|
|
105
114
|
array_like_matches_condition_hash?(attribute, value)
|
data/lib/cancan/config.rb
CHANGED
|
@@ -47,10 +47,14 @@ module CanCan
|
|
|
47
47
|
# ensures records are unique using `distinct`. Note that
|
|
48
48
|
# `distinct` is not reliable in some cases. See
|
|
49
49
|
# https://github.com/CanCanCommunity/cancancan/pull/605
|
|
50
|
+
# - :joined_alias_exists_subquery, :joined_alias_each_rule_as_exists_subquery -
|
|
51
|
+
# Build an EXISTS subquery on an aliased table.
|
|
52
|
+
# - a subclass of CanCan::ModelAdapters::Strategies::Base - Your own
|
|
53
|
+
# strategy. Implement `execute!` and return the relation.
|
|
50
54
|
def self.accessible_by_strategy
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
Thread.current[:cancan_accessible_by_strategy_override] ||
|
|
56
|
+
@accessible_by_strategy ||
|
|
57
|
+
default_accessible_by_strategy
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
def self.default_accessible_by_strategy
|
|
@@ -74,24 +78,34 @@ module CanCan
|
|
|
74
78
|
@accessible_by_strategy = value
|
|
75
79
|
end
|
|
76
80
|
|
|
81
|
+
# Overrides the strategy for the duration of the block. The override is local to the current
|
|
82
|
+
# thread and fiber, so it never leaks into concurrent requests.
|
|
77
83
|
def self.with_accessible_by_strategy(value)
|
|
78
84
|
return yield if value == accessible_by_strategy
|
|
79
85
|
|
|
80
86
|
validate_accessible_by_strategy!(value)
|
|
81
87
|
|
|
82
88
|
begin
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
override_was = Thread.current[:cancan_accessible_by_strategy_override]
|
|
90
|
+
Thread.current[:cancan_accessible_by_strategy_override] = value
|
|
85
91
|
yield
|
|
86
92
|
ensure
|
|
87
|
-
|
|
93
|
+
Thread.current[:cancan_accessible_by_strategy_override] = override_was
|
|
88
94
|
end
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
def self.validate_accessible_by_strategy!(value)
|
|
92
98
|
return if valid_accessible_by_strategies.include?(value)
|
|
99
|
+
return if custom_accessible_by_strategy?(value)
|
|
100
|
+
|
|
101
|
+
raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')} " \
|
|
102
|
+
'or a subclass of CanCan::ModelAdapters::Strategies::Base'
|
|
103
|
+
end
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
def self.custom_accessible_by_strategy?(value)
|
|
106
|
+
value.is_a?(Class) &&
|
|
107
|
+
defined?(CanCan::ModelAdapters::Strategies::Base) &&
|
|
108
|
+
value < CanCan::ModelAdapters::Strategies::Base
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
def self.does_not_support_subquery_strategy?
|
|
@@ -74,7 +74,8 @@ module CanCan
|
|
|
74
74
|
#
|
|
75
75
|
# [:+through_association+]
|
|
76
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
|
+
# This is normally not needed because it defaults to the pluralized resource name,
|
|
78
|
+
# or to the singular resource name when +:singleton+ is used.
|
|
78
79
|
#
|
|
79
80
|
# [:+shallow+]
|
|
80
81
|
# Pass +true+ to allow this resource to be loaded directly when parent is +nil+. Defaults to +false+.
|
|
@@ -302,7 +303,7 @@ module CanCan
|
|
|
302
303
|
# its own. Assign a copy, so a skip in a subclass does not change the superclass or sibling classes.
|
|
303
304
|
def add_cancan_skip(behavior, name, options)
|
|
304
305
|
skipper = cancan_skipper
|
|
305
|
-
self._cancan_skipper = skipper.merge(behavior => skipper[behavior].merge(name => options))
|
|
306
|
+
self._cancan_skipper = skipper.merge(behavior => skipper[behavior].merge(name&.to_sym => options))
|
|
306
307
|
end
|
|
307
308
|
end
|
|
308
309
|
|