cancancan 2.3.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/cancancan.gemspec +6 -5
- data/init.rb +2 -0
- data/lib/cancan/ability/actions.rb +2 -0
- data/lib/cancan/ability/rules.rb +19 -8
- data/lib/cancan/ability/strong_parameter_support.rb +41 -0
- data/lib/cancan/ability.rb +54 -24
- data/lib/cancan/class_matcher.rb +26 -0
- data/lib/cancan/conditions_matcher.rb +25 -12
- data/lib/cancan/config.rb +74 -0
- data/lib/cancan/controller_additions.rb +4 -1
- data/lib/cancan/controller_resource.rb +6 -0
- data/lib/cancan/controller_resource_builder.rb +2 -0
- data/lib/cancan/controller_resource_finder.rb +2 -0
- data/lib/cancan/controller_resource_loader.rb +4 -0
- data/lib/cancan/controller_resource_name_finder.rb +2 -0
- data/lib/cancan/controller_resource_sanitizer.rb +2 -0
- data/lib/cancan/exceptions.rb +18 -2
- data/lib/cancan/matchers.rb +3 -0
- data/lib/cancan/model_adapters/abstract_adapter.rb +3 -1
- data/lib/cancan/model_adapters/active_record_4_adapter.rb +26 -25
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +21 -26
- data/lib/cancan/model_adapters/active_record_adapter.rb +56 -14
- data/lib/cancan/model_adapters/conditions_extractor.rb +3 -3
- data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
- data/lib/cancan/model_adapters/default_adapter.rb +2 -0
- data/lib/cancan/model_adapters/sti_normalizer.rb +39 -0
- data/lib/cancan/model_additions.rb +6 -2
- data/lib/cancan/parameter_validators.rb +9 -0
- data/lib/cancan/relevant.rb +29 -0
- data/lib/cancan/rule.rb +67 -23
- data/lib/cancan/rules_compressor.rb +3 -0
- data/lib/cancan/unauthorized_message_resolver.rb +24 -0
- data/lib/cancan/version.rb +3 -1
- data/lib/cancan.rb +6 -0
- data/lib/cancancan.rb +2 -0
- data/lib/generators/cancan/ability/ability_generator.rb +3 -1
- data/lib/generators/cancan/ability/templates/ability.rb +2 -0
- metadata +37 -30
- data/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +0 -39
data/lib/cancan/exceptions.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
# A general CanCan exception
|
3
5
|
class Error < StandardError; end
|
@@ -8,9 +10,15 @@ module CanCan
|
|
8
10
|
# Raised when removed code is called, an alternative solution is provided in message.
|
9
11
|
class ImplementationRemoved < Error; end
|
10
12
|
|
11
|
-
# Raised when using check_authorization without calling
|
13
|
+
# Raised when using check_authorization without calling authorize!
|
12
14
|
class AuthorizationNotPerformed < Error; end
|
13
15
|
|
16
|
+
# Raised when a rule is created with both a block and a hash of conditions
|
17
|
+
class BlockAndConditionsError < Error; end
|
18
|
+
|
19
|
+
# Raised when an unexpected argument is passed as an attribute
|
20
|
+
class AttributeArgumentError < Error; end
|
21
|
+
|
14
22
|
# Raised when using a wrong association name
|
15
23
|
class WrongAssociationName < Error; end
|
16
24
|
|
@@ -33,7 +41,7 @@ module CanCan
|
|
33
41
|
# exception.default_message = "Default error message"
|
34
42
|
# exception.message # => "Default error message"
|
35
43
|
#
|
36
|
-
# See ControllerAdditions#
|
44
|
+
# See ControllerAdditions#authorize! for more information on rescuing from this exception
|
37
45
|
# and customizing the message using I18n.
|
38
46
|
class AccessDenied < Error
|
39
47
|
attr_reader :action, :subject, :conditions
|
@@ -50,5 +58,13 @@ module CanCan
|
|
50
58
|
def to_s
|
51
59
|
@message || @default_message
|
52
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
|
53
69
|
end
|
54
70
|
end
|
data/lib/cancan/matchers.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # RSpec 1 compatability
|
2
4
|
|
3
5
|
if rspec_module == 'RSpec'
|
@@ -12,6 +14,7 @@ Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
|
|
12
14
|
actions = args.first
|
13
15
|
if actions.is_a? Array
|
14
16
|
break false if actions.empty?
|
17
|
+
|
15
18
|
actions.all? { |action| ability.can?(action, *args[1..-1]) }
|
16
19
|
else
|
17
20
|
ability.can?(*args)
|
@@ -1,27 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
module ModelAdapters
|
3
|
-
class ActiveRecord4Adapter <
|
4
|
-
|
5
|
-
def self.for_class?(model_class)
|
6
|
-
ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base
|
7
|
-
end
|
5
|
+
class ActiveRecord4Adapter < ActiveRecordAdapter
|
6
|
+
AbstractAdapter.inherited(self)
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
class << self
|
9
|
+
def for_class?(model_class)
|
10
|
+
version_lower?('5.0.0') && model_class <= ActiveRecord::Base
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
def override_condition_matching?(subject, name, _value)
|
14
|
+
subject.class.defined_enums.include?(name.to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches_condition?(subject, name, value)
|
18
|
+
# Get the mapping from enum strings to values.
|
19
|
+
enum = subject.class.send(name.to_s.pluralize)
|
20
|
+
# Get the value of the attribute as an integer.
|
21
|
+
attribute = enum[subject.send(name)]
|
22
|
+
# Check to see if the value matches the condition.
|
23
|
+
if value.is_a?(Enumerable)
|
24
|
+
value.include? attribute
|
25
|
+
else
|
26
|
+
attribute == value
|
27
|
+
end
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
@@ -31,15 +34,13 @@ module CanCan
|
|
31
34
|
# look inside the where clause to decide to outer join tables
|
32
35
|
# you're using in the where. Instead, `references()` is required
|
33
36
|
# in addition to `includes()` to force the outer join.
|
34
|
-
def
|
35
|
-
relation
|
36
|
-
relation = relation.includes(joins).references(joins) if joins.present?
|
37
|
-
relation
|
37
|
+
def build_joins_relation(relation, *_where_conditions)
|
38
|
+
relation.includes(joins).references(joins)
|
38
39
|
end
|
39
40
|
|
40
41
|
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
41
42
|
def sanitize_sql(conditions)
|
42
|
-
if
|
43
|
+
if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash)
|
43
44
|
sanitize_sql_activerecord4(conditions)
|
44
45
|
else
|
45
46
|
@model_class.send(:sanitize_sql, conditions)
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
module ModelAdapters
|
3
5
|
class ActiveRecord5Adapter < ActiveRecord4Adapter
|
4
6
|
AbstractAdapter.inherited(self)
|
5
7
|
|
6
8
|
def self.for_class?(model_class)
|
7
|
-
|
9
|
+
version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base
|
8
10
|
end
|
9
11
|
|
10
12
|
# rails 5 is capable of using strings in enum
|
@@ -13,26 +15,25 @@ module CanCan
|
|
13
15
|
return super if Array.wrap(value).all? { |x| x.is_a? Integer }
|
14
16
|
|
15
17
|
attribute = subject.send(name)
|
16
|
-
|
17
|
-
|
18
|
-
else
|
19
|
-
attribute == value.to_s
|
20
|
-
end
|
18
|
+
raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
|
19
|
+
!(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
|
21
20
|
end
|
22
21
|
|
23
22
|
private
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
33
35
|
end
|
34
36
|
|
35
|
-
# Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
|
36
37
|
def sanitize_sql(conditions)
|
37
38
|
if conditions.is_a?(Hash)
|
38
39
|
sanitize_sql_activerecord5(conditions)
|
@@ -46,23 +47,17 @@ module CanCan
|
|
46
47
|
table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
|
47
48
|
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
|
48
49
|
|
49
|
-
conditions
|
50
|
-
|
51
|
-
conditions.stringify_keys!
|
52
|
-
|
53
|
-
predicate_builder.build_from_hash(conditions).map do |b|
|
54
|
-
visit_nodes(b)
|
55
|
-
end.join(' AND ')
|
50
|
+
predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
|
56
51
|
end
|
57
52
|
|
58
|
-
def visit_nodes(
|
53
|
+
def visit_nodes(node)
|
59
54
|
# Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
|
60
|
-
if
|
55
|
+
if self.class.version_greater_or_equal?('5.2.0')
|
61
56
|
connection = @model_class.send(:connection)
|
62
57
|
collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
|
63
|
-
connection.visitor.accept(
|
58
|
+
connection.visitor.accept(node, collector).value
|
64
59
|
else
|
65
|
-
@model_class.send(:connection).visitor.compile(
|
60
|
+
@model_class.send(:connection).visitor.compile(node)
|
66
61
|
end
|
67
62
|
end
|
68
63
|
end
|
@@ -1,10 +1,22 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'cancan/rules_compressor'
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
4
3
|
module CanCan
|
5
4
|
module ModelAdapters
|
6
|
-
|
7
|
-
|
5
|
+
class ActiveRecordAdapter < AbstractAdapter
|
6
|
+
def self.version_greater_or_equal?(version)
|
7
|
+
Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.version_lower?(version)
|
11
|
+
Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(model_class, rules)
|
15
|
+
super
|
16
|
+
@compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
|
17
|
+
StiNormalizer.normalize(@compressed_rules)
|
18
|
+
ConditionsNormalizer.normalize(model_class, @compressed_rules)
|
19
|
+
end
|
8
20
|
|
9
21
|
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
10
22
|
# method directly, but instead go through ModelAdditions#accessible_by.
|
@@ -22,13 +34,12 @@ module CanCan
|
|
22
34
|
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
23
35
|
#
|
24
36
|
def conditions
|
25
|
-
compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
|
26
37
|
conditions_extractor = ConditionsExtractor.new(@model_class)
|
27
|
-
if compressed_rules.size == 1 && compressed_rules.first.base_behavior
|
38
|
+
if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
|
28
39
|
# Return the conditions directly if there's just one definition
|
29
|
-
conditions_extractor.tableize_conditions(compressed_rules.first.conditions).dup
|
40
|
+
conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
|
30
41
|
else
|
31
|
-
extract_multiple_conditions(conditions_extractor, compressed_rules)
|
42
|
+
extract_multiple_conditions(conditions_extractor, @compressed_rules)
|
32
43
|
end
|
33
44
|
end
|
34
45
|
|
@@ -42,27 +53,58 @@ module CanCan
|
|
42
53
|
if override_scope
|
43
54
|
@model_class.where(nil).merge(override_scope)
|
44
55
|
elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
|
45
|
-
|
56
|
+
build_relation(conditions)
|
46
57
|
else
|
47
58
|
@model_class.all(conditions: conditions, joins: joins)
|
48
59
|
end
|
49
60
|
end
|
50
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
|
+
|
70
|
+
# Returns the associations used in conditions for the :joins option of a search.
|
71
|
+
# See ModelAdditions#accessible_by
|
72
|
+
def joins
|
73
|
+
joins_hash = {}
|
74
|
+
@compressed_rules.reverse_each do |rule|
|
75
|
+
deep_merge(joins_hash, rule.associations_hash)
|
76
|
+
end
|
77
|
+
deep_clean(joins_hash) unless joins_hash.empty?
|
78
|
+
end
|
79
|
+
|
51
80
|
private
|
52
81
|
|
53
|
-
|
54
|
-
|
82
|
+
# Removes empty hashes and moves everything into arrays.
|
83
|
+
def deep_clean(joins_hash)
|
84
|
+
joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Takes two hashes and does a deep merge.
|
88
|
+
def deep_merge(base_hash, added_hash)
|
89
|
+
added_hash.each do |key, value|
|
90
|
+
if base_hash[key].is_a?(Hash)
|
91
|
+
deep_merge(base_hash[key], value) unless value.empty?
|
92
|
+
else
|
93
|
+
base_hash[key] = value
|
94
|
+
end
|
95
|
+
end
|
55
96
|
end
|
56
97
|
|
57
98
|
def override_scope
|
58
|
-
conditions = @
|
99
|
+
conditions = @compressed_rules.map(&:conditions).compact
|
59
100
|
return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
|
60
101
|
return conditions.first if conditions.size == 1
|
102
|
+
|
61
103
|
raise_override_scope_error
|
62
104
|
end
|
63
105
|
|
64
106
|
def raise_override_scope_error
|
65
|
-
rule_found = @
|
107
|
+
rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
|
66
108
|
raise Error,
|
67
109
|
'Unable to merge an Active Record scope with other conditions. '\
|
68
110
|
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# this class is responsible of converting the hash of conditions
|
2
4
|
# in "where conditions" to generate the sql query
|
3
5
|
# it consists of a names_cache that helps calculating the next name given to the association
|
@@ -12,6 +14,7 @@ module CanCan
|
|
12
14
|
|
13
15
|
def tableize_conditions(conditions, model_class = @root_model_class, path_to_key = 0)
|
14
16
|
return conditions unless conditions.is_a? Hash
|
17
|
+
|
15
18
|
conditions.each_with_object({}) do |(key, value), result_hash|
|
16
19
|
if value.is_a? Hash
|
17
20
|
result_hash.merge!(calculate_result_hash(key, model_class, path_to_key, result_hash, value))
|
@@ -26,9 +29,6 @@ module CanCan
|
|
26
29
|
|
27
30
|
def calculate_result_hash(key, model_class, path_to_key, result_hash, value)
|
28
31
|
reflection = model_class.reflect_on_association(key)
|
29
|
-
unless reflection
|
30
|
-
raise WrongAssociationName, "association #{key} not defined in model #{model_class.name}"
|
31
|
-
end
|
32
32
|
nested_resulted = calculate_nested(model_class, result_hash, key, value.dup, path_to_key)
|
33
33
|
association_class = reflection.klass.name.constantize
|
34
34
|
tableize_conditions(nested_resulted, association_class, "#{path_to_key}_#{key}")
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# this class is responsible of normalizing the hash of conditions
|
2
|
+
# by exploding has_many through associations
|
3
|
+
# when a condition is defined with an has_many thorugh association this is exploded in all its parts
|
4
|
+
# TODO: it could identify STI and normalize it
|
5
|
+
module CanCan
|
6
|
+
module ModelAdapters
|
7
|
+
class ConditionsNormalizer
|
8
|
+
class << self
|
9
|
+
def normalize(model_class, rules)
|
10
|
+
rules.each { |rule| rule.conditions = normalize_conditions(model_class, rule.conditions) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def normalize_conditions(model_class, conditions)
|
14
|
+
return conditions unless conditions.is_a? Hash
|
15
|
+
|
16
|
+
conditions.each_with_object({}) do |(key, value), result_hash|
|
17
|
+
if value.is_a? Hash
|
18
|
+
result_hash.merge!(calculate_result_hash(model_class, key, value))
|
19
|
+
else
|
20
|
+
result_hash[key] = value
|
21
|
+
end
|
22
|
+
result_hash
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def calculate_result_hash(model_class, key, value)
|
29
|
+
reflection = model_class.reflect_on_association(key)
|
30
|
+
unless reflection
|
31
|
+
raise WrongAssociationName, "Association '#{key}' not defined in model '#{model_class.name}'"
|
32
|
+
end
|
33
|
+
|
34
|
+
if normalizable_association? reflection
|
35
|
+
key = reflection.options[:through]
|
36
|
+
value = { reflection.source_reflection_name => value }
|
37
|
+
reflection = model_class.reflect_on_association(key)
|
38
|
+
end
|
39
|
+
|
40
|
+
{ key => normalize_conditions(reflection.klass.name.constantize, value) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def normalizable_association?(reflection)
|
44
|
+
reflection.options[:through].present? && !reflection.options[:source_type].present?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CanCan
|
2
4
|
# This module adds the accessible_by class method to a model. It is included in the model adapters.
|
3
5
|
module ModelAdditions
|
@@ -18,8 +20,10 @@ module CanCan
|
|
18
20
|
# @articles = Article.accessible_by(current_ability, :update)
|
19
21
|
#
|
20
22
|
# Here only the articles which the user can update are returned.
|
21
|
-
def accessible_by(ability, action = :index)
|
22
|
-
|
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
|
23
27
|
end
|
24
28
|
end
|
25
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,29 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'conditions_matcher.rb'
|
4
|
+
require_relative 'class_matcher.rb'
|
5
|
+
require_relative 'relevant.rb'
|
6
|
+
|
2
7
|
module CanCan
|
3
8
|
# This class is used internally and should only be called through Ability.
|
4
9
|
# it holds the information about a "can" call made on Ability and provides
|
5
10
|
# helpful methods to determine permission checking and conditions hash generation.
|
6
11
|
class Rule # :nodoc:
|
7
12
|
include ConditionsMatcher
|
8
|
-
|
9
|
-
|
13
|
+
include Relevant
|
14
|
+
include ParameterValidators
|
15
|
+
attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes, :block
|
16
|
+
attr_writer :expanded_actions, :conditions
|
10
17
|
|
11
18
|
# The first argument when initializing is the base_behavior which is a true/false
|
12
19
|
# value. True for "can" and false for "cannot". The next two arguments are the action
|
13
20
|
# and subject respectively (such as :read, @project). The third argument is a hash
|
14
21
|
# of conditions and the last one is the block passed to the "can" call.
|
15
|
-
def initialize(base_behavior, action, subject,
|
16
|
-
|
17
|
-
|
18
|
-
|
22
|
+
def initialize(base_behavior, action, subject, *extra_args, &block)
|
23
|
+
# for backwards compatibility, attributes are an optional parameter. Check if
|
24
|
+
# attributes were passed or are actually conditions
|
25
|
+
attributes, extra_args = parse_attributes_from_extra_args(extra_args)
|
26
|
+
condition_and_block_check(extra_args, block, action, subject)
|
19
27
|
@match_all = action.nil? && subject.nil?
|
28
|
+
raise Error, "Subject is required for #{action}" if action && subject.nil?
|
29
|
+
|
20
30
|
@base_behavior = base_behavior
|
21
|
-
@actions =
|
22
|
-
@subjects =
|
23
|
-
@
|
31
|
+
@actions = wrap(action)
|
32
|
+
@subjects = wrap(subject)
|
33
|
+
@attributes = wrap(attributes)
|
34
|
+
@conditions = extra_args || {}
|
24
35
|
@block = block
|
25
36
|
end
|
26
37
|
|
38
|
+
def inspect
|
39
|
+
repr = "#<#{self.class.name}"
|
40
|
+
repr += "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}"
|
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
|
+
|
48
|
+
repr + '>'
|
49
|
+
end
|
50
|
+
|
27
51
|
def can_rule?
|
28
52
|
base_behavior
|
29
53
|
end
|
@@ -33,13 +57,8 @@ module CanCan
|
|
33
57
|
end
|
34
58
|
|
35
59
|
def catch_all?
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# Matches both the subject and action, not necessarily the conditions
|
40
|
-
def relevant?(action, subject)
|
41
|
-
subject = subject.values.first if subject.class == Hash
|
42
|
-
@match_all || (matches_action?(action) && matches_subject?(subject))
|
60
|
+
(with_scope? && @conditions.where_values_hash.empty?) ||
|
61
|
+
(!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions))
|
43
62
|
end
|
44
63
|
|
45
64
|
def only_block?
|
@@ -50,9 +69,8 @@ module CanCan
|
|
50
69
|
@block.nil? && !conditions_empty? && !@conditions.is_a?(Hash)
|
51
70
|
end
|
52
71
|
|
53
|
-
def
|
54
|
-
@conditions.
|
55
|
-
(!@conditions.keys.first.is_a? Symbol)
|
72
|
+
def with_scope?
|
73
|
+
@conditions.is_a?(ActiveRecord::Relation)
|
56
74
|
end
|
57
75
|
|
58
76
|
def associations_hash(conditions = @conditions)
|
@@ -75,6 +93,13 @@ module CanCan
|
|
75
93
|
attributes
|
76
94
|
end
|
77
95
|
|
96
|
+
def matches_attributes?(attribute)
|
97
|
+
return true if @attributes.empty?
|
98
|
+
return @base_behavior if attribute.nil?
|
99
|
+
|
100
|
+
@attributes.include?(attribute.to_sym)
|
101
|
+
end
|
102
|
+
|
78
103
|
private
|
79
104
|
|
80
105
|
def matches_action?(action)
|
@@ -86,10 +111,29 @@ module CanCan
|
|
86
111
|
end
|
87
112
|
|
88
113
|
def matches_subject_class?(subject)
|
89
|
-
@subjects
|
90
|
-
|
91
|
-
|
92
|
-
|
114
|
+
SubjectClassMatcher.matches_subject_class?(@subjects, subject)
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_attributes_from_extra_args(args)
|
118
|
+
attributes = args.shift if valid_attribute_param?(args.first)
|
119
|
+
extra_args = args.shift
|
120
|
+
[attributes, extra_args]
|
121
|
+
end
|
122
|
+
|
123
|
+
def condition_and_block_check(conditions, block, action, subject)
|
124
|
+
return unless conditions.is_a?(Hash) && block
|
125
|
+
|
126
|
+
raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\
|
127
|
+
"Check \":#{action} #{subject}\" ability."
|
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]
|
93
137
|
end
|
94
138
|
end
|
95
139
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'conditions_matcher.rb'
|
2
4
|
module CanCan
|
3
5
|
class RulesCompressor
|
@@ -11,6 +13,7 @@ module CanCan
|
|
11
13
|
def compress(array)
|
12
14
|
idx = array.rindex(&:catch_all?)
|
13
15
|
return array unless idx
|
16
|
+
|
14
17
|
value = array[idx]
|
15
18
|
array[idx..-1]
|
16
19
|
.drop_while { |n| n.base_behavior == value.base_behavior }
|