cancancan 1.10.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 (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