cancancan 1.17.0 → 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.
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