cancancan 1.17.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +10 -11
  3. data/init.rb +2 -0
  4. data/lib/cancan/ability/actions.rb +93 -0
  5. data/lib/cancan/ability/rules.rb +96 -0
  6. data/lib/cancan/ability/strong_parameter_support.rb +41 -0
  7. data/lib/cancan/ability.rb +87 -198
  8. data/lib/cancan/class_matcher.rb +30 -0
  9. data/lib/cancan/conditions_matcher.rb +147 -0
  10. data/lib/cancan/config.rb +101 -0
  11. data/lib/cancan/controller_additions.rb +13 -30
  12. data/lib/cancan/controller_resource.rb +33 -225
  13. data/lib/cancan/controller_resource_builder.rb +26 -0
  14. data/lib/cancan/controller_resource_finder.rb +42 -0
  15. data/lib/cancan/controller_resource_loader.rb +120 -0
  16. data/lib/cancan/controller_resource_name_finder.rb +23 -0
  17. data/lib/cancan/controller_resource_sanitizer.rb +32 -0
  18. data/lib/cancan/exceptions.rb +24 -4
  19. data/lib/cancan/matchers.rb +12 -1
  20. data/lib/cancan/model_adapters/abstract_adapter.rb +22 -1
  21. data/lib/cancan/model_adapters/active_record_4_adapter.rb +25 -44
  22. data/lib/cancan/model_adapters/active_record_5_adapter.rb +61 -0
  23. data/lib/cancan/model_adapters/active_record_adapter.rb +157 -83
  24. data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
  25. data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
  26. data/lib/cancan/model_adapters/default_adapter.rb +2 -0
  27. data/lib/cancan/model_adapters/sti_normalizer.rb +47 -0
  28. data/lib/cancan/model_adapters/strategies/base.rb +40 -0
  29. data/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +93 -0
  30. data/lib/cancan/model_adapters/strategies/joined_alias_exists_subquery.rb +31 -0
  31. data/lib/cancan/model_adapters/strategies/left_join.rb +11 -0
  32. data/lib/cancan/model_adapters/strategies/subquery.rb +18 -0
  33. data/lib/cancan/model_additions.rb +6 -2
  34. data/lib/cancan/parameter_validators.rb +9 -0
  35. data/lib/cancan/relevant.rb +29 -0
  36. data/lib/cancan/rule.rb +67 -90
  37. data/lib/cancan/rules_compressor.rb +23 -0
  38. data/lib/cancan/sti_detector.rb +12 -0
  39. data/lib/cancan/unauthorized_message_resolver.rb +24 -0
  40. data/lib/cancan/version.rb +3 -1
  41. data/lib/cancan.rb +15 -10
  42. data/lib/cancancan.rb +2 -0
  43. data/lib/generators/cancan/ability/ability_generator.rb +3 -1
  44. data/lib/generators/cancan/ability/templates/ability.rb +9 -9
  45. metadata +64 -86
  46. data/.gitignore +0 -15
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -39
  49. data/.rubocop_todo.yml +0 -54
  50. data/.travis.yml +0 -39
  51. data/Appraisals +0 -105
  52. data/CHANGELOG.rdoc +0 -536
  53. data/CONTRIBUTING.md +0 -23
  54. data/Gemfile +0 -3
  55. data/LICENSE +0 -22
  56. data/README.md +0 -234
  57. data/Rakefile +0 -13
  58. data/gemfiles/activerecord_3.2.gemfile +0 -18
  59. data/gemfiles/activerecord_4.0.gemfile +0 -19
  60. data/gemfiles/activerecord_4.1.gemfile +0 -19
  61. data/gemfiles/activerecord_4.2.gemfile +0 -21
  62. data/gemfiles/activerecord_5.0.gemfile +0 -20
  63. data/gemfiles/mongoid_2.x.gemfile +0 -18
  64. data/gemfiles/sequel_3.x.gemfile +0 -18
  65. data/lib/cancan/inherited_resource.rb +0 -20
  66. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -16
  67. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -80
  68. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  69. data/spec/README.rdoc +0 -27
  70. data/spec/cancan/ability_spec.rb +0 -553
  71. data/spec/cancan/controller_additions_spec.rb +0 -164
  72. data/spec/cancan/controller_resource_spec.rb +0 -645
  73. data/spec/cancan/exceptions_spec.rb +0 -58
  74. data/spec/cancan/inherited_resource_spec.rb +0 -71
  75. data/spec/cancan/matchers_spec.rb +0 -29
  76. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -160
  77. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -415
  78. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  79. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -246
  80. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -129
  81. data/spec/cancan/rule_spec.rb +0 -52
  82. data/spec/matchers.rb +0 -13
  83. data/spec/spec.opts +0 -2
  84. data/spec/spec_helper.rb +0 -27
  85. data/spec/support/ability.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 418d49100740136f143204714c01555ca5787a1e
4
- data.tar.gz: 2631fb854865b69ab8720023eb2e50289f680603
2
+ SHA256:
3
+ metadata.gz: bebbba60e68460ec234fc11e8d3cf0414e578a56c0347862c673396eb917dff9
4
+ data.tar.gz: bb07244a17dcf45d1852cf6677864084c2f0db5630ea9b72bdcc0c6055b5c4b6
5
5
  SHA512:
6
- metadata.gz: 2ac85e5511555021a0841efbdb82eb0eccf5d3145d8e53de3df189484f5a7eb34796224912556c553ead44d75aa0d496838fe73c8db25a10ede4d889c0ab115f
7
- data.tar.gz: 34dc9609930ec647cf36f7ce5fe3caa8663720ba4f8d871df3333ad41e42177aac79f383a932f6189ea05c02b2060f6a44724cd1cd27c5ecd574886742d6b303
6
+ metadata.gz: be9f2b03ae43651ea70a451b97a44fd6ec6e0a09ca444ddf625b91ae3815a245e0669bb80b4d3b0687ca327bc0c4fe81028f7736cb6493ef636b43a4140f4f49
7
+ data.tar.gz: db75441929e737d12699f57324d031e894b5d2cdbe1555451857977c42b0ef28148cc63bb718981c32ac08bafd2873623d2151ba8e98c442f217b3f4affecda9
data/cancancan.gemspec CHANGED
@@ -1,6 +1,6 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'cancan/version'
6
6
 
@@ -10,21 +10,20 @@ 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
16
17
  s.license = 'MIT'
17
18
 
18
- s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
- s.test_files = `git ls-files -- Appraisals {spec,features,gemfiles}/*`.split($INPUT_RECORD_SEPARATOR)
20
- s.executables = `git ls-files -- bin/*`.split($INPUT_RECORD_SEPARATOR).map { |f| File.basename(f) }
19
+ s.files = `git ls-files lib init.rb cancancan.gemspec`.split($INPUT_RECORD_SEPARATOR)
21
20
  s.require_paths = ['lib']
22
21
 
23
- s.required_ruby_version = '>= 2.0.0'
22
+ s.required_ruby_version = '>= 2.2.0'
24
23
 
25
- s.add_development_dependency 'bundler', '~> 1.3'
26
- s.add_development_dependency 'rubocop', '~> 0.46'
27
- s.add_development_dependency 'rake', '~> 10.1.1'
28
- s.add_development_dependency 'rspec', '~> 3.2.0'
29
- s.add_development_dependency 'appraisal', '>= 2.0.0'
24
+ s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
25
+ s.add_development_dependency 'bundler', '~> 2.0'
26
+ s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
27
+ s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
28
+ s.add_development_dependency 'rubocop', '~> 1.31.1'
30
29
  end
data/init.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cancan'
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module Actions
6
+ # Alias one or more actions into another one.
7
+ #
8
+ # alias_action :update, :destroy, :to => :modify
9
+ # can :modify, Comment
10
+ #
11
+ # Then :modify permission will apply to both :update and :destroy requests.
12
+ #
13
+ # can? :update, Comment # => true
14
+ # can? :destroy, Comment # => true
15
+ #
16
+ # This only works in one direction. Passing the aliased action into the "can?" call
17
+ # will not work because aliases are meant to generate more generic actions.
18
+ #
19
+ # alias_action :update, :destroy, :to => :modify
20
+ # can :update, Comment
21
+ # can? :modify, Comment # => false
22
+ #
23
+ # Unless that exact alias is used.
24
+ #
25
+ # can :modify, Comment
26
+ # can? :modify, Comment # => true
27
+ #
28
+ # The following aliases are added by default for conveniently mapping common controller actions.
29
+ #
30
+ # alias_action :index, :show, :to => :read
31
+ # alias_action :new, :to => :create
32
+ # alias_action :edit, :to => :update
33
+ #
34
+ # This way one can use params[:action] in the controller to determine the permission.
35
+ def alias_action(*args)
36
+ target = args.pop[:to]
37
+ validate_target(target)
38
+ aliased_actions[target] ||= []
39
+ aliased_actions[target] += args
40
+ end
41
+
42
+ # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
43
+ def aliased_actions
44
+ @aliased_actions ||= default_alias_actions
45
+ end
46
+
47
+ # Removes previously aliased actions including the defaults.
48
+ def clear_aliased_actions
49
+ @aliased_actions = {}
50
+ end
51
+
52
+ private
53
+
54
+ def default_alias_actions
55
+ {
56
+ read: %i[index show],
57
+ create: [:new],
58
+ update: [:edit]
59
+ }
60
+ end
61
+
62
+ # Given an action, it will try to find all of the actions which are aliased to it.
63
+ # This does the opposite kind of lookup as expand_actions.
64
+ def aliases_for_action(action)
65
+ results = [action]
66
+ aliased_actions.each do |aliased_action, actions|
67
+ results += aliases_for_action(aliased_action) if actions.include? action
68
+ end
69
+ results
70
+ end
71
+
72
+ def expanded_actions
73
+ @expanded_actions ||= {}
74
+ end
75
+
76
+ # Accepts an array of actions and returns an array of actions which match.
77
+ # This should be called before "matches?" and other checking methods since they
78
+ # rely on the actions to be expanded.
79
+ def expand_actions(actions)
80
+ expanded_actions[actions] ||= begin
81
+ expanded = []
82
+ actions.each do |action|
83
+ expanded << action
84
+ if (aliases = aliased_actions[action])
85
+ expanded += expand_actions(aliases)
86
+ end
87
+ end
88
+ expanded
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module Rules
6
+ protected
7
+
8
+ # Must be protected as an ability can merge with other abilities.
9
+ # This means that an ability must expose their rules with another ability.
10
+ def rules
11
+ @rules ||= []
12
+ end
13
+
14
+ private
15
+
16
+ def add_rule(rule)
17
+ rules << rule
18
+ add_rule_to_index(rule, rules.size - 1)
19
+ end
20
+
21
+ def add_rule_to_index(rule, position)
22
+ @rules_index ||= {}
23
+
24
+ subjects = rule.subjects.compact
25
+ subjects << :all if subjects.empty?
26
+
27
+ subjects.each do |subject|
28
+ @rules_index[subject] ||= []
29
+ @rules_index[subject] << position
30
+ end
31
+ end
32
+
33
+ # Returns an array of Rule instances which match the action and subject
34
+ # This does not take into consideration any hash conditions or block statements
35
+ def relevant_rules(action, subject)
36
+ return [] unless @rules
37
+
38
+ relevant = possible_relevant_rules(subject).select do |rule|
39
+ rule.expanded_actions = expand_actions(rule.actions)
40
+ rule.relevant? action, subject
41
+ end
42
+ relevant.reverse!.uniq!
43
+ optimize_order! relevant
44
+ relevant
45
+ end
46
+
47
+ def possible_relevant_rules(subject)
48
+ if subject.is_a?(Hash)
49
+ rules
50
+ else
51
+ positions = @rules_index.values_at(subject, *alternative_subjects(subject))
52
+ positions.compact!
53
+ positions.flatten!
54
+ positions.sort!
55
+ positions.map { |i| @rules[i] }
56
+ end
57
+ end
58
+
59
+ def relevant_rules_for_match(action, subject)
60
+ relevant_rules(action, subject).each do |rule|
61
+ next unless rule.only_raw_sql?
62
+
63
+ raise Error,
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}"
66
+ end
67
+ end
68
+
69
+ def relevant_rules_for_query(action, subject)
70
+ rules = relevant_rules(action, subject).reject do |rule|
71
+ # reject 'cannot' rules with attributes when doing queries
72
+ rule.base_behavior == false && rule.attributes.present?
73
+ end
74
+ if rules.any?(&:only_block?)
75
+ raise Error, "The accessible_by call cannot be used with a block 'can' definition." \
76
+ "The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
77
+ end
78
+ rules
79
+ end
80
+
81
+ # Optimizes the order of the rules, so that rules with the :all subject are evaluated first.
82
+ def optimize_order!(rules)
83
+ first_can_in_group = -1
84
+ rules.each_with_index do |rule, i|
85
+ (first_can_in_group = -1) && next unless rule.base_behavior
86
+ (first_can_in_group = i) && next if first_can_in_group == -1
87
+ next unless rule.subjects == [:all]
88
+
89
+ rules[i] = rules[first_can_in_group]
90
+ rules[first_can_in_group] = rule
91
+ first_can_in_group += 1
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Ability
5
+ module StrongParameterSupport
6
+ # Returns an array of attributes suitable for use with strong parameters
7
+ #
8
+ # Note: reversing the relevant rules is important. Normal order means that 'cannot'
9
+ # rules will come before 'can' rules. However, you can't remove attributes before
10
+ # they are added. The 'reverse' is so that attributes will be added before the
11
+ # 'cannot' rules remove them.
12
+ def permitted_attributes(action, subject)
13
+ relevant_rules(action, subject)
14
+ .reverse
15
+ .select { |rule| rule.matches_conditions? action, subject }
16
+ .each_with_object(Set.new) do |rule, set|
17
+ attributes = get_attributes(rule, subject)
18
+ # add attributes for 'can', remove them for 'cannot'
19
+ rule.base_behavior ? set.merge(attributes) : set.subtract(attributes)
20
+ end.to_a
21
+ end
22
+
23
+ private
24
+
25
+ def subject_class?(subject)
26
+ klass = (subject.is_a?(Hash) ? subject.values.first : subject).class
27
+ [Class, Module].include? klass
28
+ end
29
+
30
+ def get_attributes(rule, subject)
31
+ klass = subject_class?(subject) ? subject : subject.class
32
+ # empty attributes is an 'all'
33
+ if rule.attributes.empty? && klass < ActiveRecord::Base
34
+ klass.column_names.map(&:to_sym) - Array(klass.primary_key)
35
+ else
36
+ rule.attributes
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ability/rules.rb'
4
+ require_relative 'ability/actions.rb'
5
+ require_relative 'unauthorized_message_resolver.rb'
6
+ require_relative 'ability/strong_parameter_support'
7
+
1
8
  module CanCan
2
9
  # This module is designed to be included into an Ability class. This will
3
10
  # provide the "can" methods for defining and checking abilities.
@@ -15,6 +22,11 @@ module CanCan
15
22
  # end
16
23
  #
17
24
  module Ability
25
+ include CanCan::Ability::Rules
26
+ include CanCan::Ability::Actions
27
+ include CanCan::UnauthorizedMessageResolver
28
+ include StrongParameterSupport
29
+
18
30
  # Check if the user has permission to perform a given action on an object.
19
31
  #
20
32
  # can? :destroy, @project
@@ -59,10 +71,10 @@ module CanCan
59
71
  # end
60
72
  #
61
73
  # Also see the RSpec Matchers to aid in testing.
62
- def can?(action, subject, *extra_args)
74
+ def can?(action, subject, attribute = nil, *extra_args)
63
75
  match = extract_subjects(subject).lazy.map do |a_subject|
64
76
  relevant_rules_for_match(action, a_subject).detect do |rule|
65
- rule.matches_conditions?(action, a_subject, extra_args)
77
+ rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
66
78
  end
67
79
  end.reject(&:nil?).first
68
80
  match ? match.base_behavior : false
@@ -129,8 +141,8 @@ module CanCan
129
141
  # # check the database and return true/false
130
142
  # end
131
143
  #
132
- def can(action = nil, subject = nil, conditions = nil, &block)
133
- add_rule(Rule.new(true, action, subject, conditions, block))
144
+ def can(action = nil, subject = nil, *attributes_and_conditions, &block)
145
+ add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
134
146
  end
135
147
 
136
148
  # Defines an ability which cannot be done. Accepts the same arguments as "can".
@@ -145,44 +157,8 @@ module CanCan
145
157
  # product.invisible?
146
158
  # end
147
159
  #
148
- def cannot(action = nil, subject = nil, conditions = nil, &block)
149
- add_rule(Rule.new(false, action, subject, conditions, block))
150
- end
151
-
152
- # Alias one or more actions into another one.
153
- #
154
- # alias_action :update, :destroy, :to => :modify
155
- # can :modify, Comment
156
- #
157
- # Then :modify permission will apply to both :update and :destroy requests.
158
- #
159
- # can? :update, Comment # => true
160
- # can? :destroy, Comment # => true
161
- #
162
- # This only works in one direction. Passing the aliased action into the "can?" call
163
- # will not work because aliases are meant to generate more generic actions.
164
- #
165
- # alias_action :update, :destroy, :to => :modify
166
- # can :update, Comment
167
- # can? :modify, Comment # => false
168
- #
169
- # Unless that exact alias is used.
170
- #
171
- # can :modify, Comment
172
- # can? :modify, Comment # => true
173
- #
174
- # The following aliases are added by default for conveniently mapping common controller actions.
175
- #
176
- # alias_action :index, :show, :to => :read
177
- # alias_action :new, :to => :create
178
- # alias_action :edit, :to => :update
179
- #
180
- # This way one can use params[:action] in the controller to determine the permission.
181
- def alias_action(*args)
182
- target = args.pop[:to]
183
- validate_target(target)
184
- aliased_actions[target] ||= []
185
- aliased_actions[target] += args
160
+ def cannot(action = nil, subject = nil, *attributes_and_conditions, &block)
161
+ add_rule(Rule.new(false, action, subject, *attributes_and_conditions, &block))
186
162
  end
187
163
 
188
164
  # User shouldn't specify targets with names of real actions or it will cause Seg fault
@@ -191,16 +167,6 @@ module CanCan
191
167
  raise Error, error_message if aliased_actions.values.flatten.include? target
192
168
  end
193
169
 
194
- # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
195
- def aliased_actions
196
- @aliased_actions ||= default_alias_actions
197
- end
198
-
199
- # Removes previously aliased actions including the defaults.
200
- def clear_aliased_actions
201
- @aliased_actions = {}
202
- end
203
-
204
170
  def model_adapter(model_class, action)
205
171
  adapter_class = ModelAdapters::AbstractAdapter.adapter_class(model_class)
206
172
  adapter_class.new(model_class, relevant_rules_for_query(action, model_class))
@@ -208,25 +174,14 @@ module CanCan
208
174
 
209
175
  # See ControllerAdditions#authorize! for documentation.
210
176
  def authorize!(action, subject, *args)
211
- message = nil
212
- if args.last.is_a?(Hash) && args.last.key?(:message)
213
- message = args.pop[:message]
214
- end
177
+ message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil
215
178
  if cannot?(action, subject, *args)
216
179
  message ||= unauthorized_message(action, subject)
217
- raise AccessDenied.new(message, action, subject)
180
+ raise AccessDenied.new(message, action, subject, args)
218
181
  end
219
182
  subject
220
183
  end
221
184
 
222
- def unauthorized_message(action, subject)
223
- keys = unauthorized_message_keys(action, subject)
224
- variables = { action: action.to_s }
225
- variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase
226
- message = I18n.translate(nil, variables.merge(scope: :unauthorized, default: keys + ['']))
227
- message.blank? ? nil : message
228
- end
229
-
230
185
  def attributes_for(action, subject)
231
186
  attributes = {}
232
187
  relevant_rules(action, subject).map do |rule|
@@ -243,10 +198,58 @@ module CanCan
243
198
  relevant_rules(action, subject).any?(&:only_raw_sql?)
244
199
  end
245
200
 
201
+ # Copies all rules and aliased actions of the given +CanCan::Ability+ and adds them to +self+.
202
+ # class ReadAbility
203
+ # include CanCan::Ability
204
+ #
205
+ # def initialize
206
+ # can :read, User
207
+ # alias_action :show, :index, to: :see
208
+ # end
209
+ # end
210
+ #
211
+ # class WritingAbility
212
+ # include CanCan::Ability
213
+ #
214
+ # def initialize
215
+ # can :edit, User
216
+ # alias_action :create, :update, to: :modify
217
+ # end
218
+ # end
219
+ #
220
+ # read_ability = ReadAbility.new
221
+ # read_ability.can? :edit, User.new #=> false
222
+ # read_ability.merge(WritingAbility.new)
223
+ # read_ability.can? :edit, User.new #=> true
224
+ # read_ability.aliased_actions #=> [:see => [:show, :index], :modify => [:create, :update]]
225
+ #
226
+ # If there are collisions when merging the +aliased_actions+, the actions on +self+ will be
227
+ # overwritten.
228
+ #
229
+ # class ReadAbility
230
+ # include CanCan::Ability
231
+ #
232
+ # def initialize
233
+ # alias_action :show, :index, to: :see
234
+ # end
235
+ # end
236
+ #
237
+ # class ShowAbility
238
+ # include CanCan::Ability
239
+ #
240
+ # def initialize
241
+ # alias_action :show, to: :see
242
+ # end
243
+ # end
244
+ #
245
+ # read_ability = ReadAbility.new
246
+ # read_ability.merge(ShowAbility)
247
+ # read_ability.aliased_actions #=> [:see => [:show]]
246
248
  def merge(ability)
247
249
  ability.rules.each do |rule|
248
250
  add_rule(rule.dup)
249
251
  end
252
+ @aliased_actions = aliased_actions.merge(ability.aliased_actions)
250
253
  self
251
254
  end
252
255
 
@@ -258,66 +261,36 @@ module CanCan
258
261
  #
259
262
  # Where can_hash and cannot_hash are formatted thusly:
260
263
  # {
261
- # action: array_of_objects
264
+ # action: { subject: [attributes] }
262
265
  # }
263
266
  def permissions
264
- permissions_list = { can: {}, cannot: {} }
265
-
266
- rules.each do |rule|
267
- subjects = rule.subjects
268
- expand_actions(rule.actions).each do |action|
269
- if rule.base_behavior
270
- permissions_list[:can][action] ||= []
271
- permissions_list[:can][action] += subjects.map(&:to_s)
272
- else
273
- permissions_list[:cannot][action] ||= []
274
- permissions_list[:cannot][action] += subjects.map(&:to_s)
275
- end
276
- end
277
- end
278
-
267
+ permissions_list = {
268
+ can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } },
269
+ cannot: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }
270
+ }
271
+ rules.each { |rule| extract_rule_in_permissions(permissions_list, rule) }
279
272
  permissions_list
280
273
  end
281
274
 
282
- protected
283
-
284
- # Must be protected as an ability can merge with other abilities.
285
- # This means that an ability must expose their rules with another ability.
286
- def rules
287
- @rules ||= []
275
+ def extract_rule_in_permissions(permissions_list, rule)
276
+ expand_actions(rule.actions).each do |action|
277
+ container = rule.base_behavior ? :can : :cannot
278
+ rule.subjects.each do |subject|
279
+ permissions_list[container][action][subject.to_s] += rule.attributes
280
+ end
281
+ end
288
282
  end
289
283
 
290
284
  private
291
285
 
292
286
  def unauthorized_message_keys(action, subject)
293
287
  subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.is_a? Symbol
294
- [subject, :all].map do |try_subject|
295
- [aliases_for_action(action), :manage].flatten.map do |try_action|
296
- :"#{try_action}.#{try_subject}"
297
- end
298
- end.flatten
299
- end
300
-
301
- # Accepts an array of actions and returns an array of actions which match.
302
- # This should be called before "matches?" and other checking methods since they
303
- # rely on the actions to be expanded.
304
- def expand_actions(actions)
305
- expanded_actions[actions] ||= begin
306
- expanded = []
307
- actions.each do |action|
308
- expanded << action
309
- if (aliases = aliased_actions[action])
310
- expanded += expand_actions(aliases)
311
- end
312
- end
313
- expanded
288
+ aliases = aliases_for_action(action)
289
+ [subject, :all].product([*aliases, :manage]).map do |try_subject, try_action|
290
+ :"#{try_action}.#{try_subject}"
314
291
  end
315
292
  end
316
293
 
317
- def expanded_actions
318
- @expanded_actions ||= {}
319
- end
320
-
321
294
  # It translates to an array the subject or the hash with multiple subjects given to can?.
322
295
  def extract_subjects(subject)
323
296
  if subject.is_a?(Hash) && subject.key?(:any)
@@ -327,97 +300,13 @@ module CanCan
327
300
  end
328
301
  end
329
302
 
330
- # Given an action, it will try to find all of the actions which are aliased to it.
331
- # This does the opposite kind of lookup as expand_actions.
332
- def aliases_for_action(action)
333
- results = [action]
334
- aliased_actions.each do |aliased_action, actions|
335
- results += aliases_for_action(aliased_action) if actions.include? action
336
- end
337
- results
338
- end
339
-
340
- def add_rule(rule)
341
- rules << rule
342
- add_rule_to_index(rule, rules.size - 1)
343
- end
344
-
345
- def add_rule_to_index(rule, position)
346
- @rules_index ||= Hash.new { |h, k| h[k] = [] }
347
-
348
- subjects = rule.subjects.compact
349
- subjects << :all if subjects.empty?
350
-
351
- subjects.each do |subject|
352
- @rules_index[subject] << position
353
- end
354
- end
355
-
356
303
  def alternative_subjects(subject)
357
304
  subject = subject.class unless subject.is_a?(Module)
358
- [:all, *subject.ancestors, subject.class.to_s]
359
- end
360
-
361
- # Returns an array of Rule instances which match the action and subject
362
- # This does not take into consideration any hash conditions or block statements
363
- def relevant_rules(action, subject)
364
- return [] unless @rules
365
- relevant = possible_relevant_rules(subject).select do |rule|
366
- rule.expanded_actions = expand_actions(rule.actions)
367
- rule.relevant? action, subject
368
- end
369
- relevant.reverse!.uniq!
370
- optimize_order! relevant
371
- relevant
372
- end
373
-
374
- # Optimizes the order of the rules, so that rules with the :all subject are evaluated first.
375
- def optimize_order!(rules)
376
- first_can_in_group = -1
377
- rules.each_with_index do |rule, i|
378
- (first_can_in_group = -1) && next unless rule.base_behavior
379
- (first_can_in_group = i) && next if first_can_in_group == -1
380
- next unless rule.subjects == [:all]
381
- rules[i] = rules[first_can_in_group]
382
- rules[first_can_in_group] = rule
383
- first_can_in_group += 1
384
- end
385
- end
386
-
387
- def possible_relevant_rules(subject)
388
- if subject.is_a?(Hash)
389
- rules
305
+ if subject.respond_to?(:subclasses) && defined?(ActiveRecord::Base) && subject < ActiveRecord::Base
306
+ [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s]
390
307
  else
391
- positions = @rules_index.values_at(subject, *alternative_subjects(subject))
392
- positions.flatten!.sort!
393
- positions.map { |i| @rules[i] }
308
+ [:all, *subject.ancestors, subject.class.to_s]
394
309
  end
395
310
  end
396
-
397
- def relevant_rules_for_match(action, subject)
398
- relevant_rules(action, subject).each do |rule|
399
- next unless rule.only_raw_sql?
400
- raise Error,
401
- "The can? and cannot? call cannot be used with a raw sql 'can' definition."\
402
- " The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
403
- end
404
- end
405
-
406
- def relevant_rules_for_query(action, subject)
407
- relevant_rules(action, subject).each do |rule|
408
- if rule.only_block?
409
- raise Error, "The accessible_by call cannot be used with a block 'can' definition."\
410
- " The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
411
- end
412
- end
413
- end
414
-
415
- def default_alias_actions
416
- {
417
- read: [:index, :show],
418
- create: [:new],
419
- update: [:edit]
420
- }
421
- end
422
311
  end
423
312
  end