cancancan 1.10.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +19 -21
  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 +114 -146
  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 +38 -41
  12. data/lib/cancan/controller_resource.rb +59 -215
  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 +25 -5
  19. data/lib/cancan/matchers.rb +17 -3
  20. data/lib/cancan/model_adapters/abstract_adapter.rb +30 -9
  21. data/lib/cancan/model_adapters/active_record_4_adapter.rb +43 -15
  22. data/lib/cancan/model_adapters/active_record_5_adapter.rb +61 -0
  23. data/lib/cancan/model_adapters/active_record_adapter.rb +157 -82
  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 -3
  34. data/lib/cancan/parameter_validators.rb +9 -0
  35. data/lib/cancan/relevant.rb +29 -0
  36. data/lib/cancan/rule.rb +79 -91
  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 +16 -12
  42. data/lib/cancancan.rb +2 -0
  43. data/lib/generators/cancan/ability/ability_generator.rb +4 -2
  44. data/lib/generators/cancan/ability/templates/ability.rb +9 -9
  45. metadata +82 -93
  46. data/.gitignore +0 -15
  47. data/.rspec +0 -1
  48. data/.travis.yml +0 -48
  49. data/Appraisals +0 -135
  50. data/CHANGELOG.rdoc +0 -495
  51. data/CONTRIBUTING.md +0 -23
  52. data/Gemfile +0 -3
  53. data/LICENSE +0 -22
  54. data/README.md +0 -197
  55. data/Rakefile +0 -9
  56. data/gemfiles/activerecord_3.0.gemfile +0 -18
  57. data/gemfiles/activerecord_3.1.gemfile +0 -20
  58. data/gemfiles/activerecord_3.2.gemfile +0 -20
  59. data/gemfiles/activerecord_4.0.gemfile +0 -17
  60. data/gemfiles/activerecord_4.1.gemfile +0 -17
  61. data/gemfiles/activerecord_4.2.gemfile +0 -17
  62. data/gemfiles/datamapper_1.x.gemfile +0 -14
  63. data/gemfiles/mongoid_2.x.gemfile +0 -20
  64. data/gemfiles/sequel_3.x.gemfile +0 -20
  65. data/lib/cancan/inherited_resource.rb +0 -20
  66. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -47
  67. data/lib/cancan/model_adapters/data_mapper_adapter.rb +0 -34
  68. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -54
  69. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  70. data/spec/README.rdoc +0 -27
  71. data/spec/cancan/ability_spec.rb +0 -487
  72. data/spec/cancan/controller_additions_spec.rb +0 -141
  73. data/spec/cancan/controller_resource_spec.rb +0 -648
  74. data/spec/cancan/exceptions_spec.rb +0 -58
  75. data/spec/cancan/inherited_resource_spec.rb +0 -71
  76. data/spec/cancan/matchers_spec.rb +0 -29
  77. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -40
  78. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -446
  79. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +0 -119
  80. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  81. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -227
  82. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
  83. data/spec/cancan/rule_spec.rb +0 -52
  84. data/spec/matchers.rb +0 -13
  85. data/spec/spec.opts +0 -2
  86. data/spec/spec_helper.rb +0 -27
  87. data/spec/support/ability.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2e578e1005885c6f192bb9796d1beaa98878b01e
4
- data.tar.gz: 8f4f962696dedc14dd038489dc46054c523c55a2
2
+ SHA256:
3
+ metadata.gz: bebbba60e68460ec234fc11e8d3cf0414e578a56c0347862c673396eb917dff9
4
+ data.tar.gz: bb07244a17dcf45d1852cf6677864084c2f0db5630ea9b72bdcc0c6055b5c4b6
5
5
  SHA512:
6
- metadata.gz: 5737784c038345e4ff19c5dc519c828cb36268f77bafef9fa9d40ba3ae396fe030b2c3fc60f3e755903c9d398a59b450690cc0564b22aef1a1f46826c753aa35
7
- data.tar.gz: b4faa7beedaba71e8303aafb358a4ecd02b8d4be7abb2730c2f7b8bc9e13c46650acfa77e0ab4ecd81da381e166ddd9ab9fb098c79e7a9d3fa95dbaf5e24c0e4
6
+ metadata.gz: be9f2b03ae43651ea70a451b97a44fd6ec6e0a09ca444ddf625b91ae3815a245e0669bb80b4d3b0687ca327bc0c4fe81028f7736cb6493ef636b43a4140f4f49
7
+ data.tar.gz: db75441929e737d12699f57324d031e894b5d2cdbe1555451857977c42b0ef28148cc63bb718981c32ac08bafd2873623d2151ba8e98c442f217b3f4affecda9
data/cancancan.gemspec CHANGED
@@ -1,31 +1,29 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'cancan/version'
5
6
 
6
7
  Gem::Specification.new do |s|
7
- s.name = "cancancan"
8
+ s.name = 'cancancan'
8
9
  s.version = CanCan::VERSION
9
- s.authors = ["Bryan Rite", "Ryan Bates"]
10
- s.email = "bryan@bryanrite.com"
11
- s.homepage = "https://github.com/CanCanCommunity/cancancan"
12
- s.summary = "Simple authorization solution for Rails."
13
- s.description = "Continuation of the simple authorization solution for Rails which is decoupled from user roles. All permissions are stored in a single location."
10
+ s.authors = ['Alessandro Rodi (Renuo AG)', 'Bryan Rite', 'Ryan Bates', 'Richard Wilson']
11
+ s.email = 'alessandro.rodi@renuo.ch'
12
+ s.homepage = 'https://github.com/CanCanCommunity/cancancan'
13
+ s.metadata = { 'funding_uri' => 'https://github.com/sponsors/coorasse' }
14
+ s.summary = 'Simple authorization solution for Rails.'
15
+ s.description = 'Simple authorization solution for Rails. All permissions are stored in a single location.'
14
16
  s.platform = Gem::Platform::RUBY
15
- s.license = "MIT"
16
-
17
- s.files = `git ls-files`.split($/)
18
- s.test_files = `git ls-files -- Appraisals {spec,features,gemfiles}/*`.split($/)
19
- s.executables = `git ls-files -- bin/*`.split($/).map{ |f| File.basename(f) }
20
- s.require_paths = ["lib"]
17
+ s.license = 'MIT'
21
18
 
22
- s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
23
- s.required_rubygems_version = ">= 1.3.4"
19
+ s.files = `git ls-files lib init.rb cancancan.gemspec`.split($INPUT_RECORD_SEPARATOR)
20
+ s.require_paths = ['lib']
24
21
 
25
- s.add_development_dependency 'bundler', '~> 1.3'
26
- s.add_development_dependency 'rake', '~> 10.1.1'
27
- s.add_development_dependency 'rspec', '~> 3.0.0'
28
- s.add_development_dependency 'appraisal', '>= 1.0.0'
22
+ s.required_ruby_version = '>= 2.2.0'
29
23
 
30
- s.rubyforge_project = s.name
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'
31
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,5 +1,11 @@
1
- module CanCan
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'
2
7
 
8
+ module CanCan
3
9
  # This module is designed to be included into an Ability class. This will
4
10
  # provide the "can" methods for defining and checking abilities.
5
11
  #
@@ -16,6 +22,11 @@ module CanCan
16
22
  # end
17
23
  #
18
24
  module Ability
25
+ include CanCan::Ability::Rules
26
+ include CanCan::Ability::Actions
27
+ include CanCan::UnauthorizedMessageResolver
28
+ include StrongParameterSupport
29
+
19
30
  # Check if the user has permission to perform a given action on an object.
20
31
  #
21
32
  # can? :destroy, @project
@@ -60,17 +71,15 @@ module CanCan
60
71
  # end
61
72
  #
62
73
  # Also see the RSpec Matchers to aid in testing.
63
- def can?(action, subject, *extra_args)
64
- subject = extract_subjects(subject)
65
-
66
- match = subject.map do |subject|
67
- relevant_rules_for_match(action, subject).detect do |rule|
68
- rule.matches_conditions?(action, subject, extra_args)
74
+ def can?(action, subject, attribute = nil, *extra_args)
75
+ match = extract_subjects(subject).lazy.map do |a_subject|
76
+ relevant_rules_for_match(action, a_subject).detect do |rule|
77
+ rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
69
78
  end
70
- end.compact.first
71
-
79
+ end.reject(&:nil?).first
72
80
  match ? match.base_behavior : false
73
81
  end
82
+
74
83
  # Convenience method which works the same as "can?" but returns the opposite value.
75
84
  #
76
85
  # cannot? :destroy, @project
@@ -119,7 +128,7 @@ module CanCan
119
128
  # can :read, :stats
120
129
  # can? :read, :stats # => true
121
130
  #
122
- # IMPORTANT: Neither a hash of conditions or a block will be used when checking permission on a class.
131
+ # IMPORTANT: Neither a hash of conditions nor a block will be used when checking permission on a class.
123
132
  #
124
133
  # can :update, Project, :priority => 3
125
134
  # can? :update, Project # => true
@@ -132,8 +141,8 @@ module CanCan
132
141
  # # check the database and return true/false
133
142
  # end
134
143
  #
135
- def can(action = nil, subject = nil, conditions = nil, &block)
136
- rules << 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))
137
146
  end
138
147
 
139
148
  # Defines an ability which cannot be done. Accepts the same arguments as "can".
@@ -148,59 +157,14 @@ module CanCan
148
157
  # product.invisible?
149
158
  # end
150
159
  #
151
- def cannot(action = nil, subject = nil, conditions = nil, &block)
152
- rules << Rule.new(false, action, subject, conditions, block)
153
- end
154
-
155
- # Alias one or more actions into another one.
156
- #
157
- # alias_action :update, :destroy, :to => :modify
158
- # can :modify, Comment
159
- #
160
- # Then :modify permission will apply to both :update and :destroy requests.
161
- #
162
- # can? :update, Comment # => true
163
- # can? :destroy, Comment # => true
164
- #
165
- # This only works in one direction. Passing the aliased action into the "can?" call
166
- # will not work because aliases are meant to generate more generic actions.
167
- #
168
- # alias_action :update, :destroy, :to => :modify
169
- # can :update, Comment
170
- # can? :modify, Comment # => false
171
- #
172
- # Unless that exact alias is used.
173
- #
174
- # can :modify, Comment
175
- # can? :modify, Comment # => true
176
- #
177
- # The following aliases are added by default for conveniently mapping common controller actions.
178
- #
179
- # alias_action :index, :show, :to => :read
180
- # alias_action :new, :to => :create
181
- # alias_action :edit, :to => :update
182
- #
183
- # This way one can use params[:action] in the controller to determine the permission.
184
- def alias_action(*args)
185
- target = args.pop[:to]
186
- validate_target(target)
187
- aliased_actions[target] ||= []
188
- 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))
189
162
  end
190
163
 
191
164
  # User shouldn't specify targets with names of real actions or it will cause Seg fault
192
165
  def validate_target(target)
193
- raise Error, "You can't specify target (#{target}) as alias because it is real action name" if aliased_actions.values.flatten.include? target
194
- end
195
-
196
- # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
197
- def aliased_actions
198
- @aliased_actions ||= default_alias_actions
199
- end
200
-
201
- # Removes previously aliased actions including the defaults.
202
- def clear_aliased_actions
203
- @aliased_actions = {}
166
+ error_message = "You can't specify target (#{target}) as alias because it is real action name"
167
+ raise Error, error_message if aliased_actions.values.flatten.include? target
204
168
  end
205
169
 
206
170
  def model_adapter(model_class, action)
@@ -210,25 +174,14 @@ module CanCan
210
174
 
211
175
  # See ControllerAdditions#authorize! for documentation.
212
176
  def authorize!(action, subject, *args)
213
- message = nil
214
- if args.last.kind_of?(Hash) && args.last.has_key?(:message)
215
- message = args.pop[:message]
216
- end
177
+ message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil
217
178
  if cannot?(action, subject, *args)
218
179
  message ||= unauthorized_message(action, subject)
219
- raise AccessDenied.new(message, action, subject)
180
+ raise AccessDenied.new(message, action, subject, args)
220
181
  end
221
182
  subject
222
183
  end
223
184
 
224
- def unauthorized_message(action, subject)
225
- keys = unauthorized_message_keys(action, subject)
226
- variables = {:action => action.to_s}
227
- variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase
228
- message = I18n.translate(nil, variables.merge(:scope => :unauthorized, :default => keys + [""]))
229
- message.blank? ? nil : message
230
- end
231
-
232
185
  def attributes_for(action, subject)
233
186
  attributes = {}
234
187
  relevant_rules(action, subject).map do |rule|
@@ -245,100 +198,115 @@ module CanCan
245
198
  relevant_rules(action, subject).any?(&:only_raw_sql?)
246
199
  end
247
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]]
248
248
  def merge(ability)
249
- ability.send(:rules).each do |rule|
250
- rules << rule.dup
249
+ ability.rules.each do |rule|
250
+ add_rule(rule.dup)
251
251
  end
252
+ @aliased_actions = aliased_actions.merge(ability.aliased_actions)
252
253
  self
253
254
  end
254
255
 
255
- private
256
-
257
- def unauthorized_message_keys(action, subject)
258
- subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol
259
- [subject, :all].map do |try_subject|
260
- [aliases_for_action(action), :manage].flatten.map do |try_action|
261
- :"#{try_action}.#{try_subject}"
262
- end
263
- end.flatten
256
+ # Return a hash of permissions for the user in the format of:
257
+ # {
258
+ # can: can_hash,
259
+ # cannot: cannot_hash
260
+ # }
261
+ #
262
+ # Where can_hash and cannot_hash are formatted thusly:
263
+ # {
264
+ # action: { subject: [attributes] }
265
+ # }
266
+ def permissions
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) }
272
+ permissions_list
264
273
  end
265
274
 
266
- # Accepts an array of actions and returns an array of actions which match.
267
- # This should be called before "matches?" and other checking methods since they
268
- # rely on the actions to be expanded.
269
- def expand_actions(actions)
270
- expanded_actions[actions] ||= begin
271
- expanded = []
272
- actions.each do |action|
273
- expanded << action
274
- if aliases = aliased_actions[action]
275
- expanded += expand_actions(aliases)
276
- end
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
277
280
  end
278
- expanded
279
281
  end
280
282
  end
281
283
 
282
- def expanded_actions
283
- @expanded_actions ||= {}
284
+ private
285
+
286
+ def unauthorized_message_keys(action, subject)
287
+ subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.is_a? Symbol
288
+ aliases = aliases_for_action(action)
289
+ [subject, :all].product([*aliases, :manage]).map do |try_subject, try_action|
290
+ :"#{try_action}.#{try_subject}"
291
+ end
284
292
  end
285
293
 
286
294
  # It translates to an array the subject or the hash with multiple subjects given to can?.
287
295
  def extract_subjects(subject)
288
- subject = if subject.kind_of?(Hash) && subject.key?(:any)
296
+ if subject.is_a?(Hash) && subject.key?(:any)
289
297
  subject[:any]
290
298
  else
291
299
  [subject]
292
300
  end
293
301
  end
294
302
 
295
- # Given an action, it will try to find all of the actions which are aliased to it.
296
- # This does the opposite kind of lookup as expand_actions.
297
- def aliases_for_action(action)
298
- results = [action]
299
- aliased_actions.each do |aliased_action, actions|
300
- results += aliases_for_action(aliased_action) if actions.include? action
301
- end
302
- results
303
- end
304
-
305
- def rules
306
- @rules ||= []
307
- end
308
-
309
- # Returns an array of Rule instances which match the action and subject
310
- # This does not take into consideration any hash conditions or block statements
311
- def relevant_rules(action, subject)
312
- relevant = rules.select do |rule|
313
- rule.expanded_actions = expand_actions(rule.actions)
314
- rule.relevant? action, subject
315
- end
316
- relevant.reverse!
317
- relevant
318
- end
319
-
320
- def relevant_rules_for_match(action, subject)
321
- relevant_rules(action, subject).each do |rule|
322
- if rule.only_raw_sql?
323
- raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
324
- end
325
- end
326
- end
327
-
328
- def relevant_rules_for_query(action, subject)
329
- relevant_rules(action, subject).each do |rule|
330
- if rule.only_block?
331
- raise Error, "The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
332
- end
303
+ def alternative_subjects(subject)
304
+ subject = subject.class unless subject.is_a?(Module)
305
+ if subject.respond_to?(:subclasses) && defined?(ActiveRecord::Base) && subject < ActiveRecord::Base
306
+ [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s]
307
+ else
308
+ [:all, *subject.ancestors, subject.class.to_s]
333
309
  end
334
310
  end
335
-
336
- def default_alias_actions
337
- {
338
- :read => [:index, :show],
339
- :create => [:new],
340
- :update => [:edit],
341
- }
342
- end
343
311
  end
344
312
  end
@@ -0,0 +1,30 @@
1
+ require_relative 'sti_detector'
2
+
3
+ # This class is responsible for matching classes and their subclasses as well as
4
+ # upmatching classes to their ancestors.
5
+ # This is used to generate sti connections
6
+ class SubjectClassMatcher
7
+ def self.matches_subject_class?(subjects, subject)
8
+ subjects.any? do |sub|
9
+ has_subclasses = subject.respond_to?(:subclasses)
10
+ matching_class_check(subject, sub, has_subclasses)
11
+ end
12
+ end
13
+
14
+ def self.matching_class_check(subject, sub, has_subclasses)
15
+ matches = matches_class_or_is_related(subject, sub)
16
+ if has_subclasses
17
+ return matches unless StiDetector.sti_class?(sub)
18
+
19
+ matches || subject.subclasses.include?(sub)
20
+ else
21
+ matches
22
+ end
23
+ end
24
+
25
+ def self.matches_class_or_is_related(subject, sub)
26
+ sub.is_a?(Module) && (subject.is_a?(sub) ||
27
+ subject.class.to_s == sub.to_s ||
28
+ (subject.is_a?(Module) && subject.ancestors.include?(sub)))
29
+ end
30
+ end