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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class DefaultAdapter < AbstractAdapter
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ParameterValidators
5
+ def valid_attribute_param?(attribute)
6
+ attribute.nil? || attribute.is_a?(Symbol) || (attribute.is_a?(Array) && attribute.all? { |a| a.is_a?(Symbol) })
7
+ end
8
+ end
9
+ end
@@ -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
- attr_reader :base_behavior, :subjects, :actions, :conditions
9
- attr_writer :expanded_actions
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, conditions, block)
16
- both_block_and_hash_error = 'You are not able to supply a block with a hash of conditions in '\
17
- "#{action} #{subject} ability. Use either one."
18
- raise Error, both_block_and_hash_error if conditions.is_a?(Hash) && block
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 = Array(action)
22
- @subjects = Array(subject)
23
- @conditions = conditions || {}
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
- [nil, false, [], {}, '', ' '].include? @conditions
37
- end
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 unmergeable?
54
- @conditions.respond_to?(:keys) && @conditions.present? &&
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.any? do |sub|
90
- sub.is_a?(Module) && (subject.is_a?(sub) ||
91
- subject.class.to_s == sub.to_s ||
92
- (subject.is_a?(Module) && subject.ancestors.include?(sub)))
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 }
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module UnauthorizedMessageResolver
5
+ def unauthorized_message(action, subject)
6
+ subject = subject.values.last if subject.is_a?(Hash)
7
+ keys = unauthorized_message_keys(action, subject)
8
+ variables = {}
9
+ variables[:action] = I18n.translate("actions.#{action}", default: action.to_s)
10
+ variables[:subject] = translate_subject(subject)
11
+ message = I18n.translate(keys.shift, **variables.merge(scope: :unauthorized, default: keys + ['']))
12
+ message.blank? ? nil : message
13
+ end
14
+
15
+ def translate_subject(subject)
16
+ klass = (subject.class == Class ? subject : subject.class)
17
+ if klass.respond_to?(:model_name)
18
+ klass.model_name.human
19
+ else
20
+ klass.to_s.underscore.humanize.downcase
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
- VERSION = '2.3.0'.freeze
4
+ VERSION = '3.2.2'.freeze
3
5
  end
data/lib/cancancan.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cancan'
2
4
 
3
5
  module CanCanCan
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Cancan
2
4
  module Generators
3
5
  class AbilityGenerator < Rails::Generators::Base
4
- source_root File.expand_path('../templates', __FILE__)
6
+ source_root File.expand_path('templates', __dir__)
5
7
 
6
8
  def generate_ability
7
9
  copy_file 'ability.rb', 'app/models/ability.rb'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Ability
2
4
  include CanCan::Ability
3
5
 
metadata CHANGED
@@ -1,46 +1,52 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cancancan
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 3.2.2
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: 2018-09-16 00:00:00.000000000 Z
14
+ date: 2021-05-27 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
- name: bundler
17
+ name: appraisal
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
20
23
  - - "~>"
21
24
  - !ruby/object:Gem::Version
22
- version: '1.3'
25
+ version: '2.0'
23
26
  type: :development
24
27
  prerelease: false
25
28
  version_requirements: !ruby/object:Gem::Requirement
26
29
  requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
27
33
  - - "~>"
28
34
  - !ruby/object:Gem::Version
29
- version: '1.3'
35
+ version: '2.0'
30
36
  - !ruby/object:Gem::Dependency
31
- name: rubocop
37
+ name: bundler
32
38
  requirement: !ruby/object:Gem::Requirement
33
39
  requirements:
34
40
  - - "~>"
35
41
  - !ruby/object:Gem::Version
36
- version: 0.48.1
42
+ version: '2.0'
37
43
  type: :development
38
44
  prerelease: false
39
45
  version_requirements: !ruby/object:Gem::Requirement
40
46
  requirements:
41
47
  - - "~>"
42
48
  - !ruby/object:Gem::Version
43
- version: 0.48.1
49
+ version: '2.0'
44
50
  - !ruby/object:Gem::Dependency
45
51
  name: rake
46
52
  requirement: !ruby/object:Gem::Requirement
@@ -65,42 +71,36 @@ dependencies:
65
71
  name: rspec
66
72
  requirement: !ruby/object:Gem::Requirement
67
73
  requirements:
68
- - - "~>"
69
- - !ruby/object:Gem::Version
70
- version: '3.2'
71
74
  - - ">="
72
75
  - !ruby/object:Gem::Version
73
76
  version: 3.2.0
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '3.2'
74
80
  type: :development
75
81
  prerelease: false
76
82
  version_requirements: !ruby/object:Gem::Requirement
77
83
  requirements:
78
- - - "~>"
79
- - !ruby/object:Gem::Version
80
- version: '3.2'
81
84
  - - ">="
82
85
  - !ruby/object:Gem::Version
83
86
  version: 3.2.0
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.2'
84
90
  - !ruby/object:Gem::Dependency
85
- name: appraisal
91
+ name: rubocop
86
92
  requirement: !ruby/object:Gem::Requirement
87
93
  requirements:
88
94
  - - "~>"
89
95
  - !ruby/object:Gem::Version
90
- version: '2.0'
91
- - - ">="
92
- - !ruby/object:Gem::Version
93
- version: 2.0.0
96
+ version: 0.63.1
94
97
  type: :development
95
98
  prerelease: false
96
99
  version_requirements: !ruby/object:Gem::Requirement
97
100
  requirements:
98
101
  - - "~>"
99
102
  - !ruby/object:Gem::Version
100
- version: '2.0'
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: 2.0.0
103
+ version: 0.63.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
@@ -114,7 +114,10 @@ files:
114
114
  - lib/cancan/ability.rb
115
115
  - lib/cancan/ability/actions.rb
116
116
  - lib/cancan/ability/rules.rb
117
+ - lib/cancan/ability/strong_parameter_support.rb
118
+ - lib/cancan/class_matcher.rb
117
119
  - lib/cancan/conditions_matcher.rb
120
+ - lib/cancan/config.rb
118
121
  - lib/cancan/controller_additions.rb
119
122
  - lib/cancan/controller_resource.rb
120
123
  - lib/cancan/controller_resource_builder.rb
@@ -128,12 +131,16 @@ files:
128
131
  - lib/cancan/model_adapters/active_record_4_adapter.rb
129
132
  - lib/cancan/model_adapters/active_record_5_adapter.rb
130
133
  - lib/cancan/model_adapters/active_record_adapter.rb
131
- - lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb
132
134
  - lib/cancan/model_adapters/conditions_extractor.rb
135
+ - lib/cancan/model_adapters/conditions_normalizer.rb
133
136
  - lib/cancan/model_adapters/default_adapter.rb
137
+ - lib/cancan/model_adapters/sti_normalizer.rb
134
138
  - lib/cancan/model_additions.rb
139
+ - lib/cancan/parameter_validators.rb
140
+ - lib/cancan/relevant.rb
135
141
  - lib/cancan/rule.rb
136
142
  - lib/cancan/rules_compressor.rb
143
+ - lib/cancan/unauthorized_message_resolver.rb
137
144
  - lib/cancan/version.rb
138
145
  - lib/cancancan.rb
139
146
  - lib/generators/cancan/ability/USAGE
@@ -142,8 +149,9 @@ files:
142
149
  homepage: https://github.com/CanCanCommunity/cancancan
143
150
  licenses:
144
151
  - MIT
145
- metadata: {}
146
- post_install_message:
152
+ metadata:
153
+ funding_uri: https://github.com/sponsors/coorasse
154
+ post_install_message:
147
155
  rdoc_options: []
148
156
  require_paths:
149
157
  - lib
@@ -158,9 +166,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
166
  - !ruby/object:Gem::Version
159
167
  version: '0'
160
168
  requirements: []
161
- rubyforge_project:
162
- rubygems_version: 2.7.4
163
- signing_key:
169
+ rubygems_version: 3.0.6
170
+ signing_key:
164
171
  specification_version: 4
165
172
  summary: Simple authorization solution for Rails.
166
173
  test_files: []