cancancan 3.0.1 → 3.5.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 +2 -1
- data/lib/cancan/ability/rules.rb +8 -5
- data/lib/cancan/ability.rb +5 -1
- data/lib/cancan/class_matcher.rb +30 -0
- data/lib/cancan/conditions_matcher.rb +54 -10
- data/lib/cancan/config.rb +101 -0
- data/lib/cancan/controller_additions.rb +1 -1
- data/lib/cancan/controller_resource.rb +1 -1
- data/lib/cancan/exceptions.rb +8 -0
- data/lib/cancan/matchers.rb +5 -3
- data/lib/cancan/model_adapters/abstract_adapter.rb +20 -1
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +2 -4
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +8 -10
- data/lib/cancan/model_adapters/active_record_adapter.rb +84 -4
- data/lib/cancan/model_adapters/conditions_extractor.rb +4 -4
- data/lib/cancan/model_adapters/conditions_normalizer.rb +6 -2
- data/lib/cancan/model_adapters/sti_normalizer.rb +47 -0
- data/lib/cancan/model_adapters/strategies/base.rb +40 -0
- data/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +93 -0
- data/lib/cancan/model_adapters/strategies/joined_alias_exists_subquery.rb +31 -0
- data/lib/cancan/model_adapters/strategies/left_join.rb +11 -0
- data/lib/cancan/model_adapters/strategies/subquery.rb +18 -0
- data/lib/cancan/model_additions.rb +4 -2
- data/lib/cancan/relevant.rb +29 -0
- data/lib/cancan/rule.rb +28 -22
- data/lib/cancan/sti_detector.rb +12 -0
- data/lib/cancan/unauthorized_message_resolver.rb +4 -2
- data/lib/cancan/version.rb +1 -1
- data/lib/cancan.rb +7 -0
- data/lib/generators/cancan/ability/templates/ability.rb +7 -9
- metadata +20 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bebbba60e68460ec234fc11e8d3cf0414e578a56c0347862c673396eb917dff9
|
4
|
+
data.tar.gz: bb07244a17dcf45d1852cf6677864084c2f0db5630ea9b72bdcc0c6055b5c4b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be9f2b03ae43651ea70a451b97a44fd6ec6e0a09ca444ddf625b91ae3815a245e0669bb80b4d3b0687ca327bc0c4fe81028f7736cb6493ef636b43a4140f4f49
|
7
|
+
data.tar.gz: db75441929e737d12699f57324d031e894b5d2cdbe1555451857977c42b0ef28148cc63bb718981c32ac08bafd2873623d2151ba8e98c442f217b3f4affecda9
|
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
|
@@ -24,5 +25,5 @@ Gem::Specification.new do |s|
|
|
24
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 'rubocop', '~>
|
28
|
+
s.add_development_dependency 'rubocop', '~> 1.31.1'
|
28
29
|
end
|
data/lib/cancan/ability/rules.rb
CHANGED
@@ -19,12 +19,13 @@ module CanCan
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def add_rule_to_index(rule, position)
|
22
|
-
@rules_index ||=
|
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.
|
52
|
+
positions.compact!
|
53
|
+
positions.flatten!
|
54
|
+
positions.sort!
|
52
55
|
positions.map { |i| @rules[i] }
|
53
56
|
end
|
54
57
|
end
|
@@ -58,8 +61,8 @@ module CanCan
|
|
58
61
|
next unless rule.only_raw_sql?
|
59
62
|
|
60
63
|
raise Error,
|
61
|
-
"The can? and cannot? call cannot be used with a raw sql 'can' definition."\
|
62
|
-
"
|
64
|
+
"The can? and cannot? call cannot be used with a raw sql 'can' definition. " \
|
65
|
+
"The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
|
63
66
|
end
|
64
67
|
end
|
65
68
|
|
@@ -69,7 +72,7 @@ module CanCan
|
|
69
72
|
rule.base_behavior == false && rule.attributes.present?
|
70
73
|
end
|
71
74
|
if rules.any?(&:only_block?)
|
72
|
-
raise Error, "The accessible_by call cannot be used with a block 'can' definition."\
|
75
|
+
raise Error, "The accessible_by call cannot be used with a block 'can' definition." \
|
73
76
|
"The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
|
74
77
|
end
|
75
78
|
rules
|
data/lib/cancan/ability.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'sti_detector'
|
2
|
+
|
3
|
+
# This class is responsible for matching classes and their subclasses as well as
|
4
|
+
# upmatching classes to their ancestors.
|
5
|
+
# This is used to generate sti connections
|
6
|
+
class SubjectClassMatcher
|
7
|
+
def self.matches_subject_class?(subjects, subject)
|
8
|
+
subjects.any? do |sub|
|
9
|
+
has_subclasses = subject.respond_to?(:subclasses)
|
10
|
+
matching_class_check(subject, sub, has_subclasses)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.matching_class_check(subject, sub, has_subclasses)
|
15
|
+
matches = matches_class_or_is_related(subject, sub)
|
16
|
+
if has_subclasses
|
17
|
+
return matches unless StiDetector.sti_class?(sub)
|
18
|
+
|
19
|
+
matches || subject.subclasses.include?(sub)
|
20
|
+
else
|
21
|
+
matches
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.matches_class_or_is_related(subject, sub)
|
26
|
+
sub.is_a?(Module) && (subject.is_a?(sub) ||
|
27
|
+
subject.class.to_s == sub.to_s ||
|
28
|
+
(subject.is_a?(Module) && subject.ancestors.include?(sub)))
|
29
|
+
end
|
30
|
+
end
|
@@ -18,10 +18,14 @@ module CanCan
|
|
18
18
|
[Class, Module].include? klass
|
19
19
|
end
|
20
20
|
|
21
|
-
def matches_block_conditions(subject, *extra_args)
|
21
|
+
def matches_block_conditions(subject, attribute, *extra_args)
|
22
22
|
return @base_behavior if subject_class?(subject)
|
23
23
|
|
24
|
-
|
24
|
+
if attribute
|
25
|
+
@block.call(subject, attribute, *extra_args)
|
26
|
+
else
|
27
|
+
@block.call(subject, *extra_args)
|
28
|
+
end
|
25
29
|
end
|
26
30
|
|
27
31
|
def matches_non_block_conditions(subject)
|
@@ -33,16 +37,26 @@ module CanCan
|
|
33
37
|
end
|
34
38
|
|
35
39
|
def nested_subject_matches_conditions?(subject_hash)
|
36
|
-
parent,
|
37
|
-
|
40
|
+
parent, child = subject_hash.first
|
41
|
+
|
42
|
+
adapter = model_adapter(parent)
|
43
|
+
|
44
|
+
parent_condition_name = adapter.parent_condition_name(parent, child)
|
45
|
+
|
46
|
+
matches_base_parent_conditions = matches_conditions_hash?(parent,
|
47
|
+
@conditions[parent_condition_name] || {})
|
48
|
+
|
49
|
+
matches_base_parent_conditions &&
|
50
|
+
(!adapter.override_nested_subject_conditions_matching?(parent, child, @conditions) ||
|
51
|
+
adapter.nested_subject_matches_conditions?(parent, child, @conditions))
|
38
52
|
end
|
39
53
|
|
40
54
|
# Checks if the given subject matches the given conditions hash.
|
41
|
-
# This behavior can be
|
55
|
+
# This behavior can be overridden by a model adapter by defining two class methods:
|
42
56
|
# override_matching_for_conditions?(subject, conditions) and
|
43
57
|
# matches_conditions_hash?(subject, conditions)
|
44
58
|
def matches_conditions_hash?(subject, conditions = @conditions)
|
45
|
-
return true if conditions.empty?
|
59
|
+
return true if conditions.is_a?(Hash) && conditions.empty?
|
46
60
|
|
47
61
|
adapter = model_adapter(subject)
|
48
62
|
|
@@ -50,10 +64,20 @@ module CanCan
|
|
50
64
|
return adapter.matches_conditions_hash?(subject, conditions)
|
51
65
|
end
|
52
66
|
|
53
|
-
matches_all_conditions?(adapter,
|
67
|
+
matches_all_conditions?(adapter, subject, conditions)
|
54
68
|
end
|
55
69
|
|
56
|
-
def matches_all_conditions?(adapter,
|
70
|
+
def matches_all_conditions?(adapter, subject, conditions)
|
71
|
+
if conditions.is_a?(Hash)
|
72
|
+
matches_hash_conditions?(adapter, subject, conditions)
|
73
|
+
elsif conditions.respond_to?(:include?)
|
74
|
+
conditions.include?(subject)
|
75
|
+
else
|
76
|
+
subject == conditions
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def matches_hash_conditions?(adapter, subject, conditions)
|
57
81
|
conditions.all? do |name, value|
|
58
82
|
if adapter.override_condition_matching?(subject, name, value)
|
59
83
|
adapter.matches_condition?(subject, name, value)
|
@@ -78,12 +102,29 @@ module CanCan
|
|
78
102
|
|
79
103
|
def hash_condition_match?(attribute, value)
|
80
104
|
if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
|
81
|
-
|
105
|
+
array_like_matches_condition_hash?(attribute, value)
|
82
106
|
else
|
83
107
|
attribute && matches_conditions_hash?(attribute, value)
|
84
108
|
end
|
85
109
|
end
|
86
110
|
|
111
|
+
def array_like_matches_condition_hash?(attribute, value)
|
112
|
+
if attribute.any?
|
113
|
+
attribute.any? { |element| matches_conditions_hash?(element, value) }
|
114
|
+
else
|
115
|
+
# you can use `nil`s in your ability definition to tell cancancan to find
|
116
|
+
# objects that *don't* have any children in a has_many relationship.
|
117
|
+
#
|
118
|
+
# for example, given ability:
|
119
|
+
# => can :read, Article, comments: { id: nil }
|
120
|
+
# cancancan will return articles where `article.comments == []`
|
121
|
+
#
|
122
|
+
# this is implemented here. `attribute` is `article.comments`, and it's an empty array.
|
123
|
+
# the expression below returns true if this was expected.
|
124
|
+
!value.values.empty? && value.values.all?(&:nil?)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
87
128
|
def call_block_with_all(action, subject, *extra_args)
|
88
129
|
if subject.class == Class
|
89
130
|
@block.call(action, subject, nil, *extra_args)
|
@@ -97,7 +138,10 @@ module CanCan
|
|
97
138
|
end
|
98
139
|
|
99
140
|
def conditions_empty?
|
100
|
-
@conditions
|
141
|
+
# @conditions might be an ActiveRecord::Associations::CollectionProxy
|
142
|
+
# which it's `==` implementation will fetch all records for comparison
|
143
|
+
|
144
|
+
(@conditions.is_a?(Hash) && @conditions == {}) || @conditions.nil?
|
101
145
|
end
|
102
146
|
end
|
103
147
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CanCan
|
4
|
+
def self.valid_accessible_by_strategies
|
5
|
+
strategies = [:left_join]
|
6
|
+
|
7
|
+
unless does_not_support_subquery_strategy?
|
8
|
+
strategies.push(:joined_alias_exists_subquery, :joined_alias_each_rule_as_exists_subquery, :subquery)
|
9
|
+
end
|
10
|
+
|
11
|
+
strategies
|
12
|
+
end
|
13
|
+
|
14
|
+
# You can disable the rules compressor if it's causing unexpected issues.
|
15
|
+
def self.rules_compressor_enabled
|
16
|
+
return @rules_compressor_enabled if defined?(@rules_compressor_enabled)
|
17
|
+
|
18
|
+
@rules_compressor_enabled = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.rules_compressor_enabled=(value)
|
22
|
+
@rules_compressor_enabled = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.with_rules_compressor_enabled(value)
|
26
|
+
return yield if value == rules_compressor_enabled
|
27
|
+
|
28
|
+
begin
|
29
|
+
rules_compressor_enabled_was = rules_compressor_enabled
|
30
|
+
@rules_compressor_enabled = value
|
31
|
+
yield
|
32
|
+
ensure
|
33
|
+
@rules_compressor_enabled = rules_compressor_enabled_was
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Determines how CanCan should build queries when calling accessible_by,
|
38
|
+
# if the query will contain a join. The default strategy is `:subquery`.
|
39
|
+
#
|
40
|
+
# # config/initializers/cancan.rb
|
41
|
+
# CanCan.accessible_by_strategy = :subquery
|
42
|
+
#
|
43
|
+
# Valid strategies are:
|
44
|
+
# - :subquery - Creates a nested query with all joins, wrapped by a
|
45
|
+
# WHERE IN query.
|
46
|
+
# - :left_join - Calls the joins directly using `left_joins`, and
|
47
|
+
# ensures records are unique using `distinct`. Note that
|
48
|
+
# `distinct` is not reliable in some cases. See
|
49
|
+
# https://github.com/CanCanCommunity/cancancan/pull/605
|
50
|
+
def self.accessible_by_strategy
|
51
|
+
return @accessible_by_strategy if @accessible_by_strategy
|
52
|
+
|
53
|
+
@accessible_by_strategy = default_accessible_by_strategy
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.default_accessible_by_strategy
|
57
|
+
if does_not_support_subquery_strategy?
|
58
|
+
# see https://github.com/CanCanCommunity/cancancan/pull/655 for where this was added
|
59
|
+
# the `subquery` strategy (from https://github.com/CanCanCommunity/cancancan/pull/619
|
60
|
+
# only works in Rails 5 and higher
|
61
|
+
:left_join
|
62
|
+
else
|
63
|
+
:subquery
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.accessible_by_strategy=(value)
|
68
|
+
validate_accessible_by_strategy!(value)
|
69
|
+
|
70
|
+
if value == :subquery && does_not_support_subquery_strategy?
|
71
|
+
raise ArgumentError, 'accessible_by_strategy = :subquery requires ActiveRecord 5 or newer'
|
72
|
+
end
|
73
|
+
|
74
|
+
@accessible_by_strategy = value
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.with_accessible_by_strategy(value)
|
78
|
+
return yield if value == accessible_by_strategy
|
79
|
+
|
80
|
+
validate_accessible_by_strategy!(value)
|
81
|
+
|
82
|
+
begin
|
83
|
+
strategy_was = accessible_by_strategy
|
84
|
+
@accessible_by_strategy = value
|
85
|
+
yield
|
86
|
+
ensure
|
87
|
+
@accessible_by_strategy = strategy_was
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.validate_accessible_by_strategy!(value)
|
92
|
+
return if valid_accessible_by_strategies.include?(value)
|
93
|
+
|
94
|
+
raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.does_not_support_subquery_strategy?
|
98
|
+
!defined?(CanCan::ModelAdapters::ActiveRecordAdapter) ||
|
99
|
+
CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
|
100
|
+
end
|
101
|
+
end
|
@@ -264,7 +264,7 @@ module CanCan
|
|
264
264
|
next if options[:unless] && controller.send(options[:unless])
|
265
265
|
|
266
266
|
raise AuthorizationNotPerformed,
|
267
|
-
'This action failed the check_authorization because it does not authorize_resource. '\
|
267
|
+
'This action failed the check_authorization because it does not authorize_resource. ' \
|
268
268
|
'Add skip_authorization_check to bypass this check.'
|
269
269
|
end
|
270
270
|
|
@@ -54,7 +54,7 @@ module CanCan
|
|
54
54
|
|
55
55
|
protected
|
56
56
|
|
57
|
-
# Returns the class used for this resource. This can be
|
57
|
+
# Returns the class used for this resource. This can be overridden by the :class option.
|
58
58
|
# If +false+ is passed in it will use the resource name as a symbol in which case it should
|
59
59
|
# only be used for authorization, not loading since there's no class to load through.
|
60
60
|
def resource_class
|
data/lib/cancan/exceptions.rb
CHANGED
@@ -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
|
data/lib/cancan/matchers.rb
CHANGED
@@ -13,9 +13,11 @@ Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
|
|
13
13
|
match do |ability|
|
14
14
|
actions = args.first
|
15
15
|
if actions.is_a? Array
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
if actions.empty?
|
17
|
+
false
|
18
|
+
else
|
19
|
+
actions.all? { |action| ability.can?(action, *args[1..-1]) }
|
20
|
+
end
|
19
21
|
else
|
20
22
|
ability.can?(*args)
|
21
23
|
end
|
@@ -3,9 +3,11 @@
|
|
3
3
|
module CanCan
|
4
4
|
module ModelAdapters
|
5
5
|
class AbstractAdapter
|
6
|
+
attr_reader :model_class
|
7
|
+
|
6
8
|
def self.inherited(subclass)
|
7
9
|
@subclasses ||= []
|
8
|
-
@subclasses
|
10
|
+
@subclasses.insert(0, subclass)
|
9
11
|
end
|
10
12
|
|
11
13
|
def self.adapter_class(model_class)
|
@@ -33,6 +35,23 @@ module CanCan
|
|
33
35
|
raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
|
34
36
|
end
|
35
37
|
|
38
|
+
# Override if parent condition could be under a different key in conditions
|
39
|
+
def self.parent_condition_name(parent, _child)
|
40
|
+
parent.class.name.downcase.to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
# Used above override_conditions_hash_matching to determine if this model adapter will override the
|
44
|
+
# matching behavior for nested subject.
|
45
|
+
# If this returns true then nested_subject_matches_conditions? will be called.
|
46
|
+
def self.override_nested_subject_conditions_matching?(_parent, _child, _all_conditions)
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
# Override if override_nested_subject_conditions_matching? returns true
|
51
|
+
def self.nested_subject_matches_conditions?(_parent, _child, _all_conditions)
|
52
|
+
raise NotImplemented, 'This model adapter does not support matching on a nested subject.'
|
53
|
+
end
|
54
|
+
|
36
55
|
# Used to determine if this model adapter will override the matching behavior for a specific condition.
|
37
56
|
# If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
|
38
57
|
def self.override_condition_matching?(_subject, _name, _value)
|
@@ -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
|
38
|
-
relation
|
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,15 @@ module CanCan
|
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
-
def
|
25
|
-
relation
|
26
|
-
|
27
|
-
|
24
|
+
def build_joins_relation(relation, *where_conditions)
|
25
|
+
strategy_class.new(adapter: self, relation: relation, where_conditions: where_conditions).execute!
|
26
|
+
end
|
27
|
+
|
28
|
+
def strategy_class
|
29
|
+
strategy_class_name = CanCan.accessible_by_strategy.to_s.camelize
|
30
|
+
CanCan::ModelAdapters::Strategies.const_get(strategy_class_name)
|
28
31
|
end
|
29
32
|
|
30
|
-
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
31
33
|
def sanitize_sql(conditions)
|
32
34
|
if conditions.is_a?(Hash)
|
33
35
|
sanitize_sql_activerecord5(conditions)
|
@@ -41,11 +43,7 @@ module CanCan
|
|
41
43
|
table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
|
42
44
|
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
|
43
45
|
|
44
|
-
conditions
|
45
|
-
|
46
|
-
conditions.stringify_keys!
|
47
|
-
|
48
|
-
predicate_builder.build_from_hash(conditions).map { |b| visit_nodes(b) }.join(' AND ')
|
46
|
+
predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
|
49
47
|
end
|
50
48
|
|
51
49
|
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
|
@@ -13,12 +11,86 @@ module CanCan
|
|
13
11
|
Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version)
|
14
12
|
end
|
15
13
|
|
14
|
+
attr_reader :compressed_rules
|
15
|
+
|
16
16
|
def initialize(model_class, rules)
|
17
17
|
super
|
18
|
-
@compressed_rules =
|
18
|
+
@compressed_rules = if CanCan.rules_compressor_enabled
|
19
|
+
RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
|
20
|
+
else
|
21
|
+
@rules
|
22
|
+
end
|
23
|
+
StiNormalizer.normalize(@compressed_rules)
|
19
24
|
ConditionsNormalizer.normalize(model_class, @compressed_rules)
|
20
25
|
end
|
21
26
|
|
27
|
+
class << self
|
28
|
+
# When belongs_to parent_id is a condition for a model,
|
29
|
+
# we want to check the parent when testing ability for a hash {parent => model}
|
30
|
+
def override_nested_subject_conditions_matching?(parent, child, all_conditions)
|
31
|
+
parent_child_conditions(parent, child, all_conditions).present?
|
32
|
+
end
|
33
|
+
|
34
|
+
# parent_id condition can be an array of integer or one integer, we check the parent against this
|
35
|
+
def nested_subject_matches_conditions?(parent, child, all_conditions)
|
36
|
+
id_condition = parent_child_conditions(parent, child, all_conditions)
|
37
|
+
return id_condition.include?(parent.id) if id_condition.is_a? Array
|
38
|
+
return id_condition == parent.id if id_condition.is_a? Integer
|
39
|
+
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent_child_conditions(parent, child, all_conditions)
|
44
|
+
child_class = child.is_a?(Class) ? child : child.class
|
45
|
+
parent_class = parent.is_a?(Class) ? parent : parent.class
|
46
|
+
|
47
|
+
foreign_key = child_class.reflect_on_all_associations(:belongs_to).find do |association|
|
48
|
+
# Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
|
49
|
+
!association.polymorphic? && association.klass == parent.class
|
50
|
+
end&.foreign_key&.to_sym
|
51
|
+
|
52
|
+
# Search again in case of polymorphic associations, this time matching on the :has_many side
|
53
|
+
# via the :as option, as well as klass
|
54
|
+
foreign_key ||= parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
|
55
|
+
!matching_parent_child_polymorphic_association(has_many_assoc, child_class).nil?
|
56
|
+
end&.foreign_key&.to_sym
|
57
|
+
|
58
|
+
foreign_key.nil? ? nil : all_conditions[foreign_key]
|
59
|
+
end
|
60
|
+
|
61
|
+
def matching_parent_child_polymorphic_association(parent_assoc, child_class)
|
62
|
+
return nil unless parent_assoc.klass == child_class
|
63
|
+
return nil if parent_assoc&.options[:as].nil?
|
64
|
+
|
65
|
+
child_class.reflect_on_all_associations(:belongs_to).find do |child_assoc|
|
66
|
+
# Only match this way for polymorphic associations
|
67
|
+
child_assoc.polymorphic? && child_assoc.name == parent_assoc.options[:as]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def child_association_to_parent(parent, child)
|
72
|
+
child_class = child.is_a?(Class) ? child : child.class
|
73
|
+
parent_class = parent.is_a?(Class) ? parent : parent.class
|
74
|
+
|
75
|
+
association = child_class.reflect_on_all_associations(:belongs_to).find do |association|
|
76
|
+
# Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
|
77
|
+
!association.polymorphic? && association.klass == parent.class
|
78
|
+
end
|
79
|
+
|
80
|
+
return association unless association.nil?
|
81
|
+
|
82
|
+
parent_class.reflect_on_all_associations(:has_many).each do |has_many_assoc|
|
83
|
+
association ||= matching_parent_child_polymorphic_association(has_many_assoc, child_class)
|
84
|
+
end
|
85
|
+
|
86
|
+
association
|
87
|
+
end
|
88
|
+
|
89
|
+
def parent_condition_name(parent, child)
|
90
|
+
child_association_to_parent(parent, child)&.name || parent.class.name.downcase.to_sym
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
22
94
|
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
23
95
|
# method directly, but instead go through ModelAdditions#accessible_by.
|
24
96
|
#
|
@@ -60,6 +132,14 @@ module CanCan
|
|
60
132
|
end
|
61
133
|
end
|
62
134
|
|
135
|
+
def build_relation(*where_conditions)
|
136
|
+
relation = @model_class.where(*where_conditions)
|
137
|
+
return relation unless joins.present?
|
138
|
+
|
139
|
+
# subclasses must implement `build_joins_relation`
|
140
|
+
build_joins_relation(relation, *where_conditions)
|
141
|
+
end
|
142
|
+
|
63
143
|
# Returns the associations used in conditions for the :joins option of a search.
|
64
144
|
# See ModelAdditions#accessible_by
|
65
145
|
def joins
|
@@ -99,7 +179,7 @@ module CanCan
|
|
99
179
|
def raise_override_scope_error
|
100
180
|
rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
|
101
181
|
raise Error,
|
102
|
-
'Unable to merge an Active Record scope with other conditions. '\
|
182
|
+
'Unable to merge an Active Record scope with other conditions. ' \
|
103
183
|
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
104
184
|
end
|
105
185
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# this class is responsible of converting the hash of conditions
|
4
4
|
# in "where conditions" to generate the sql query
|
5
5
|
# it consists of a names_cache that helps calculating the next name given to the association
|
6
|
-
# it tries to reflect the
|
6
|
+
# it tries to reflect the behavior of ActiveRecord when generating aliases for tables.
|
7
7
|
module CanCan
|
8
8
|
module ModelAdapters
|
9
9
|
class ConditionsExtractor
|
@@ -50,18 +50,18 @@ module CanCan
|
|
50
50
|
def generate_table_alias(model_class, relation_name, path_to_key)
|
51
51
|
table_alias = model_class.reflect_on_association(relation_name).table_name.to_sym
|
52
52
|
|
53
|
-
if
|
53
|
+
if already_used?(table_alias, relation_name, path_to_key)
|
54
54
|
table_alias = "#{relation_name.to_s.pluralize}_#{model_class.table_name}".to_sym
|
55
55
|
|
56
56
|
index = 1
|
57
|
-
while
|
57
|
+
while already_used?(table_alias, relation_name, path_to_key)
|
58
58
|
table_alias = "#{table_alias}_#{index += 1}".to_sym
|
59
59
|
end
|
60
60
|
end
|
61
61
|
add_to_cache(table_alias, relation_name, path_to_key)
|
62
62
|
end
|
63
63
|
|
64
|
-
def
|
64
|
+
def already_used?(table_alias, relation_name, path_to_key)
|
65
65
|
@names_cache[table_alias].try(:exclude?, "#{path_to_key}_#{relation_name}")
|
66
66
|
end
|
67
67
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# this class is responsible of normalizing the hash of conditions
|
2
2
|
# by exploding has_many through associations
|
3
|
-
# when a condition is defined with an has_many
|
3
|
+
# when a condition is defined with an has_many through association this is exploded in all its parts
|
4
4
|
# TODO: it could identify STI and normalize it
|
5
5
|
module CanCan
|
6
6
|
module ModelAdapters
|
@@ -31,7 +31,7 @@ module CanCan
|
|
31
31
|
raise WrongAssociationName, "Association '#{key}' not defined in model '#{model_class.name}'"
|
32
32
|
end
|
33
33
|
|
34
|
-
if reflection
|
34
|
+
if normalizable_association? reflection
|
35
35
|
key = reflection.options[:through]
|
36
36
|
value = { reflection.source_reflection_name => value }
|
37
37
|
reflection = model_class.reflect_on_association(key)
|
@@ -39,6 +39,10 @@ module CanCan
|
|
39
39
|
|
40
40
|
{ key => normalize_conditions(reflection.klass.name.constantize, value) }
|
41
41
|
end
|
42
|
+
|
43
|
+
def normalizable_association?(reflection)
|
44
|
+
reflection.options[:through].present? && !reflection.options[:source_type].present?
|
45
|
+
end
|
42
46
|
end
|
43
47
|
end
|
44
48
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative '../sti_detector'
|
2
|
+
|
3
|
+
# this class is responsible for detecting sti classes and creating new rules for the
|
4
|
+
# relevant subclasses, using the inheritance_column as a merger
|
5
|
+
module CanCan
|
6
|
+
module ModelAdapters
|
7
|
+
class StiNormalizer
|
8
|
+
class << self
|
9
|
+
def normalize(rules)
|
10
|
+
rules_cache = []
|
11
|
+
return unless defined?(ActiveRecord::Base)
|
12
|
+
|
13
|
+
rules.delete_if do |rule|
|
14
|
+
subjects = rule.subjects.select do |subject|
|
15
|
+
update_rule(subject, rule, rules_cache)
|
16
|
+
end
|
17
|
+
subjects.length == rule.subjects.length
|
18
|
+
end
|
19
|
+
rules_cache.each { |rule| rules.push(rule) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def update_rule(subject, rule, rules_cache)
|
25
|
+
return false unless StiDetector.sti_class?(subject)
|
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
|
+
sti_conditions = { subject.inheritance_column => subject.sti_name }
|
34
|
+
new_rule_conditions =
|
35
|
+
if rule.with_scope?
|
36
|
+
rule.conditions.where(sti_conditions)
|
37
|
+
else
|
38
|
+
rule.conditions.merge(sti_conditions)
|
39
|
+
end
|
40
|
+
|
41
|
+
CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass,
|
42
|
+
new_rule_conditions, rule.block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class Strategies
|
4
|
+
class Base
|
5
|
+
attr_reader :adapter, :relation, :where_conditions
|
6
|
+
|
7
|
+
delegate(
|
8
|
+
:compressed_rules,
|
9
|
+
:extract_multiple_conditions,
|
10
|
+
:joins,
|
11
|
+
:model_class,
|
12
|
+
:quoted_primary_key,
|
13
|
+
:quoted_aliased_table_name,
|
14
|
+
:quoted_table_name,
|
15
|
+
to: :adapter
|
16
|
+
)
|
17
|
+
delegate :connection, :quoted_primary_key, to: :model_class
|
18
|
+
delegate :quote_table_name, to: :connection
|
19
|
+
|
20
|
+
def initialize(adapter:, relation:, where_conditions:)
|
21
|
+
@adapter = adapter
|
22
|
+
@relation = relation
|
23
|
+
@where_conditions = where_conditions
|
24
|
+
end
|
25
|
+
|
26
|
+
def aliased_table_name
|
27
|
+
@aliased_table_name ||= "#{model_class.table_name}_alias"
|
28
|
+
end
|
29
|
+
|
30
|
+
def quoted_aliased_table_name
|
31
|
+
@quoted_aliased_table_name ||= quote_table_name(aliased_table_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def quoted_table_name
|
35
|
+
@quoted_table_name ||= quote_table_name(model_class.table_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module CanCan
|
4
|
+
module ModelAdapters
|
5
|
+
class Strategies
|
6
|
+
class JoinedAliasEachRuleAsExistsSubquery < Base
|
7
|
+
def execute!
|
8
|
+
model_class
|
9
|
+
.joins(
|
10
|
+
"JOIN #{quoted_table_name} AS #{quoted_aliased_table_name} ON " \
|
11
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key} = #{quoted_table_name}.#{quoted_primary_key}"
|
12
|
+
)
|
13
|
+
.where(double_exists_sql)
|
14
|
+
end
|
15
|
+
|
16
|
+
def double_exists_sql
|
17
|
+
double_exists_sql = ''
|
18
|
+
|
19
|
+
compressed_rules.each_with_index do |rule, index|
|
20
|
+
double_exists_sql << ' OR ' if index.positive?
|
21
|
+
double_exists_sql << "EXISTS (#{sub_query_for_rule(rule).to_sql})"
|
22
|
+
end
|
23
|
+
|
24
|
+
double_exists_sql
|
25
|
+
end
|
26
|
+
|
27
|
+
def sub_query_for_rule(rule)
|
28
|
+
conditions_extractor = ConditionsExtractor.new(model_class)
|
29
|
+
rule_where_conditions = extract_multiple_conditions(conditions_extractor, [rule])
|
30
|
+
joins_hash, left_joins_hash = extract_joins_from_rule(rule)
|
31
|
+
sub_query_for_rules_and_join_hashes(rule_where_conditions, joins_hash, left_joins_hash)
|
32
|
+
end
|
33
|
+
|
34
|
+
def sub_query_for_rules_and_join_hashes(rule_where_conditions, joins_hash, left_joins_hash)
|
35
|
+
model_class
|
36
|
+
.select('1')
|
37
|
+
.joins(joins_hash)
|
38
|
+
.left_joins(left_joins_hash)
|
39
|
+
.where(
|
40
|
+
"#{quoted_table_name}.#{quoted_primary_key} = " \
|
41
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key}"
|
42
|
+
)
|
43
|
+
.where(rule_where_conditions)
|
44
|
+
.limit(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
def extract_joins_from_rule(rule)
|
48
|
+
joins = {}
|
49
|
+
left_joins = {}
|
50
|
+
|
51
|
+
extra_joins_recursive([], rule.conditions, joins, left_joins)
|
52
|
+
[joins, left_joins]
|
53
|
+
end
|
54
|
+
|
55
|
+
def extra_joins_recursive(current_path, conditions, joins, left_joins)
|
56
|
+
conditions.each do |key, value|
|
57
|
+
if value.is_a?(Hash)
|
58
|
+
current_path << key
|
59
|
+
extra_joins_recursive(current_path, value, joins, left_joins)
|
60
|
+
current_path.pop
|
61
|
+
else
|
62
|
+
extra_joins_recursive_merge_joins(current_path, value, joins, left_joins)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def extra_joins_recursive_merge_joins(current_path, value, joins, left_joins)
|
68
|
+
hash_joins = current_path_to_hash(current_path)
|
69
|
+
|
70
|
+
if value.nil?
|
71
|
+
left_joins.deep_merge!(hash_joins)
|
72
|
+
else
|
73
|
+
joins.deep_merge!(hash_joins)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Converts an array like [:child, :grand_child] into a hash like {child: {grand_child: {}}
|
78
|
+
def current_path_to_hash(current_path)
|
79
|
+
hash_joins = {}
|
80
|
+
current_hash_joins = hash_joins
|
81
|
+
|
82
|
+
current_path.each do |path_part|
|
83
|
+
new_hash = {}
|
84
|
+
current_hash_joins[path_part] = new_hash
|
85
|
+
current_hash_joins = new_hash
|
86
|
+
end
|
87
|
+
|
88
|
+
hash_joins
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CanCan
|
4
|
+
module ModelAdapters
|
5
|
+
class Strategies
|
6
|
+
class JoinedAliasExistsSubquery < Base
|
7
|
+
def execute!
|
8
|
+
model_class
|
9
|
+
.joins(
|
10
|
+
"JOIN #{quoted_table_name} AS #{quoted_aliased_table_name} ON " \
|
11
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key} = #{quoted_table_name}.#{quoted_primary_key}"
|
12
|
+
)
|
13
|
+
.where("EXISTS (#{joined_alias_exists_subquery_inner_query.to_sql})")
|
14
|
+
end
|
15
|
+
|
16
|
+
def joined_alias_exists_subquery_inner_query
|
17
|
+
model_class
|
18
|
+
.unscoped
|
19
|
+
.select('1')
|
20
|
+
.left_joins(joins)
|
21
|
+
.where(*where_conditions)
|
22
|
+
.where(
|
23
|
+
"#{quoted_table_name}.#{quoted_primary_key} = " \
|
24
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key}"
|
25
|
+
)
|
26
|
+
.limit(1)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class Strategies
|
4
|
+
class Subquery < Base
|
5
|
+
def execute!
|
6
|
+
build_joins_relation_subquery(where_conditions)
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_joins_relation_subquery(where_conditions)
|
10
|
+
inner = model_class.unscoped do
|
11
|
+
model_class.left_joins(joins).where(*where_conditions)
|
12
|
+
end
|
13
|
+
model_class.where(model_class.primary_key => inner)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
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
|
-
|
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 =
|
28
|
-
@subjects =
|
29
|
-
@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
|
@@ -34,11 +38,13 @@ module CanCan
|
|
34
38
|
def inspect
|
35
39
|
repr = "#<#{self.class.name}"
|
36
40
|
repr += "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}"
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
|
42
|
+
if with_scope?
|
43
|
+
repr += ", #{@conditions.where_values_hash}"
|
44
|
+
elsif [Hash, String].include?(@conditions.class)
|
45
|
+
repr += ", #{@conditions.inspect}"
|
46
|
+
end
|
47
|
+
|
42
48
|
repr + '>'
|
43
49
|
end
|
44
50
|
|
@@ -55,12 +61,6 @@ module CanCan
|
|
55
61
|
(!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions))
|
56
62
|
end
|
57
63
|
|
58
|
-
# Matches both the action, subject, and attribute, not necessarily the conditions
|
59
|
-
def relevant?(action, subject)
|
60
|
-
subject = subject.values.first if subject.class == Hash
|
61
|
-
@match_all || (matches_action?(action) && matches_subject?(subject))
|
62
|
-
end
|
63
|
-
|
64
64
|
def only_block?
|
65
65
|
conditions_empty? && @block
|
66
66
|
end
|
@@ -70,7 +70,7 @@ module CanCan
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def with_scope?
|
73
|
-
@conditions.is_a?(ActiveRecord::Relation)
|
73
|
+
defined?(ActiveRecord) && @conditions.is_a?(ActiveRecord::Relation)
|
74
74
|
end
|
75
75
|
|
76
76
|
def associations_hash(conditions = @conditions)
|
@@ -111,11 +111,7 @@ module CanCan
|
|
111
111
|
end
|
112
112
|
|
113
113
|
def matches_subject_class?(subject)
|
114
|
-
@subjects
|
115
|
-
sub.is_a?(Module) && (subject.is_a?(sub) ||
|
116
|
-
subject.class.to_s == sub.to_s ||
|
117
|
-
(subject.is_a?(Module) && subject.ancestors.include?(sub)))
|
118
|
-
end
|
114
|
+
SubjectClassMatcher.matches_subject_class?(@subjects, subject)
|
119
115
|
end
|
120
116
|
|
121
117
|
def parse_attributes_from_extra_args(args)
|
@@ -127,8 +123,18 @@ module CanCan
|
|
127
123
|
def condition_and_block_check(conditions, block, action, subject)
|
128
124
|
return unless conditions.is_a?(Hash) && block
|
129
125
|
|
130
|
-
raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\
|
126
|
+
raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. ' \
|
131
127
|
"Check \":#{action} #{subject}\" ability."
|
132
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
|
133
139
|
end
|
134
140
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class StiDetector
|
4
|
+
def self.sti_class?(subject)
|
5
|
+
return false unless defined?(ActiveRecord::Base)
|
6
|
+
return false unless subject.respond_to?(:descends_from_active_record?)
|
7
|
+
return false if subject == :all || subject.descends_from_active_record?
|
8
|
+
return false unless subject < ActiveRecord::Base
|
9
|
+
|
10
|
+
true
|
11
|
+
end
|
12
|
+
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 = {
|
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
|
|
data/lib/cancan/version.rb
CHANGED
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,7 +17,13 @@ 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'
|
24
|
+
require 'cancan/model_adapters/strategies/base'
|
25
|
+
require 'cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery'
|
26
|
+
require 'cancan/model_adapters/strategies/joined_alias_exists_subquery'
|
27
|
+
require 'cancan/model_adapters/strategies/left_join'
|
28
|
+
require 'cancan/model_adapters/strategies/subquery'
|
22
29
|
end
|
@@ -4,14 +4,12 @@ class Ability
|
|
4
4
|
include CanCan::Ability
|
5
5
|
|
6
6
|
def initialize(user)
|
7
|
-
# Define abilities for the
|
7
|
+
# Define abilities for the user here. For example:
|
8
8
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# can :read, :all
|
14
|
-
# end
|
9
|
+
# return unless user.present?
|
10
|
+
# can :read, :all
|
11
|
+
# return unless user.admin?
|
12
|
+
# can :manage, :all
|
15
13
|
#
|
16
14
|
# The first argument to `can` is the action you are giving the user
|
17
15
|
# permission to do.
|
@@ -26,9 +24,9 @@ class Ability
|
|
26
24
|
# objects.
|
27
25
|
# For example, here the user can only update published articles.
|
28
26
|
#
|
29
|
-
# can :update, Article, :
|
27
|
+
# can :update, Article, published: true
|
30
28
|
#
|
31
29
|
# See the wiki for details:
|
32
|
-
# https://github.com/CanCanCommunity/cancancan/
|
30
|
+
# https://github.com/CanCanCommunity/cancancan/blob/develop/docs/define_check_abilities.md
|
33
31
|
end
|
34
32
|
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
|
4
|
+
version: 3.5.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:
|
14
|
+
date: 2023-03-05 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: appraisal
|
@@ -93,14 +93,14 @@ dependencies:
|
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 1.31.1
|
97
97
|
type: :development
|
98
98
|
prerelease: false
|
99
99
|
version_requirements: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 1.31.1
|
104
104
|
description: Simple authorization solution for Rails. All permissions are stored in
|
105
105
|
a single location.
|
106
106
|
email: alessandro.rodi@renuo.ch
|
@@ -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,10 +134,18 @@ 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
|
138
|
+
- lib/cancan/model_adapters/strategies/base.rb
|
139
|
+
- lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb
|
140
|
+
- lib/cancan/model_adapters/strategies/joined_alias_exists_subquery.rb
|
141
|
+
- lib/cancan/model_adapters/strategies/left_join.rb
|
142
|
+
- lib/cancan/model_adapters/strategies/subquery.rb
|
135
143
|
- lib/cancan/model_additions.rb
|
136
144
|
- lib/cancan/parameter_validators.rb
|
145
|
+
- lib/cancan/relevant.rb
|
137
146
|
- lib/cancan/rule.rb
|
138
147
|
- lib/cancan/rules_compressor.rb
|
148
|
+
- lib/cancan/sti_detector.rb
|
139
149
|
- lib/cancan/unauthorized_message_resolver.rb
|
140
150
|
- lib/cancan/version.rb
|
141
151
|
- lib/cancancan.rb
|
@@ -145,8 +155,9 @@ files:
|
|
145
155
|
homepage: https://github.com/CanCanCommunity/cancancan
|
146
156
|
licenses:
|
147
157
|
- MIT
|
148
|
-
metadata:
|
149
|
-
|
158
|
+
metadata:
|
159
|
+
funding_uri: https://github.com/sponsors/coorasse
|
160
|
+
post_install_message:
|
150
161
|
rdoc_options: []
|
151
162
|
require_paths:
|
152
163
|
- lib
|
@@ -161,9 +172,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
161
172
|
- !ruby/object:Gem::Version
|
162
173
|
version: '0'
|
163
174
|
requirements: []
|
164
|
-
|
165
|
-
|
166
|
-
signing_key:
|
175
|
+
rubygems_version: 3.3.7
|
176
|
+
signing_key:
|
167
177
|
specification_version: 4
|
168
178
|
summary: Simple authorization solution for Rails.
|
169
179
|
test_files: []
|