action_policy 0.3.4 → 0.4.4

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +4 -1
  3. data/.github/bug_report_template.rb +175 -0
  4. data/.travis.yml +4 -4
  5. data/CHANGELOG.md +54 -0
  6. data/LICENSE.txt +1 -1
  7. data/README.md +7 -10
  8. data/benchmarks/namespaced_lookup_cache.rb +8 -5
  9. data/benchmarks/pre_checks.rb +73 -0
  10. data/docs/README.md +2 -0
  11. data/docs/caching.md +22 -4
  12. data/docs/instrumentation.md +11 -0
  13. data/docs/lookup_chain.md +6 -1
  14. data/docs/namespaces.md +2 -2
  15. data/docs/quick_start.md +5 -3
  16. data/docs/rails.md +7 -0
  17. data/docs/testing.md +63 -0
  18. data/docs/writing_policies.md +1 -1
  19. data/gemfiles/rails42.gemfile +1 -0
  20. data/lib/action_policy.rb +1 -1
  21. data/lib/action_policy/behaviour.rb +4 -0
  22. data/lib/action_policy/behaviours/memoized.rb +3 -7
  23. data/lib/action_policy/behaviours/policy_for.rb +13 -4
  24. data/lib/action_policy/behaviours/scoping.rb +2 -0
  25. data/lib/action_policy/behaviours/thread_memoized.rb +3 -7
  26. data/lib/action_policy/ext/policy_cache_key.rb +8 -6
  27. data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
  28. data/lib/action_policy/lookup_chain.rb +19 -13
  29. data/lib/action_policy/policy/authorization.rb +7 -11
  30. data/lib/action_policy/policy/cache.rb +26 -4
  31. data/lib/action_policy/policy/core.rb +2 -2
  32. data/lib/action_policy/policy/pre_check.rb +2 -2
  33. data/lib/action_policy/policy/reasons.rb +2 -2
  34. data/lib/action_policy/rails/ext/active_record.rb +7 -0
  35. data/lib/action_policy/rails/policy/instrumentation.rb +9 -2
  36. data/lib/action_policy/rspec/dsl.rb +6 -6
  37. data/lib/action_policy/rspec/have_authorized_scope.rb +2 -2
  38. data/lib/action_policy/testing.rb +3 -3
  39. data/lib/action_policy/version.rb +1 -1
  40. data/lib/generators/action_policy/install/templates/application_policy.rb +1 -1
  41. metadata +5 -4
  42. data/.github/FUNDING.yml +0 -1
@@ -65,6 +65,8 @@ Learn more about the motivation behind the Action Policy and its features by wat
65
65
 
66
66
  ## Resources
67
67
 
68
+ - RubyRussia, 2019 "Welcome, or access denied?" talk ([video](https://www.youtube.com/watch?v=y15a2g7v8i0) [RU], [slides](https://speakerdeck.com/palkan/rubyrussia-2019-welcome-or-access-denied))
69
+
68
70
  - Seattle.rb, 2019 "A Denial!" talk [[slides](https://speakerdeck.com/palkan/seattle-dot-rb-2019-a-denial)]
69
71
 
70
72
  - RailsConf, 2018 "Access Denied" talk [[video](https://www.youtube.com/watch?v=NVwx0DARDis), [slides](https://speakerdeck.com/palkan/railsconf-2018-access-denied-the-missing-guide-to-authorization-in-rails)]
@@ -162,11 +162,11 @@ Rails.application.configure do |config|
162
162
  end
163
163
  ```
164
164
 
165
- Cache store must provide at least a `#read(key)` and `write(key, value, **options)` methods.
165
+ Cache store must provide at least a `#read(key)` and `#write(key, value, **options)` methods.
166
166
 
167
167
  **NOTE:** cache store also should take care of serialiation/deserialization since the `value` is `ExecutionResult` instance (which contains also some additional information, e.g. failure reasons). Rails cache store supports serialization/deserialization out-of-the-box.
168
168
 
169
- By default, Action Policy builds a cache key using the following scheme:
169
+ By default, Action Policy builds a cache key using the following scheme (defined in `#rule_cache_key(rule)` method):
170
170
 
171
171
  ```ruby
172
172
  "#{cache_namespace}/#{context_cache_key}" \
@@ -175,11 +175,29 @@ By default, Action Policy builds a cache key using the following scheme:
175
175
 
176
176
  Where `cache_namespace` is equal to `"acp:#{MAJOR_GEM_VERSION}.#{MINOR_GEM_VERSION}"`, and `context_cache_key` is a concatenation of all authorization contexts cache keys (in the same order as they are defined in the policy class).
177
177
 
178
- If any object does not respond to `#policy_cache_key`, we fallback to `#cache_key`. If `#cache_key` is not defined, an `ArgumentError` is raised.
178
+ If any object does not respond to `#policy_cache_key`, we fallback to `#cache_key` (or `#cache_key_with_version` for modern Rails versions). If `#cache_key` is not defined, an `ArgumentError` is raised.
179
179
 
180
180
  **NOTE:** if your `#cache_key` method is performance-heavy (e.g. like the `ActiveRecord::Relation`'s one), we recommend to explicitly define the `#policy_cache_key` method on the corresponding class to avoid unnecessary load. See also [action_policy#55](https://github.com/palkan/action_policy/issues/55).
181
181
 
182
- You can define your own `cache_key` / `cache_namespace` / `context_cache_key` methods for policy class to override this logic.
182
+ You can define your own `rule_cache_key` / `cache_namespace` / `context_cache_key` methods for policy class to override this logic.
183
+
184
+ You can also use the `#cache` instance method to cache arbitrary values in you policies:
185
+
186
+ ```ruby
187
+ class ApplicationPolicy < ActionPolicy::Base
188
+ # Suppose that a user has many roles each having an array of permissions
189
+ def permissions
190
+ cache(user) { user.roles.pluck(:permissions).flatten.uniq }
191
+ end
192
+
193
+ # You can pass multiple cache key "parts"
194
+ def account_permissions(account)
195
+ cache(user, account) { user.account_roles.where(account: account).pluck(:permissions).flatten.uniq }
196
+ end
197
+ end
198
+ ```
199
+
200
+ **NOTE:** `#cache` method uses the same cache key generation logic as rules caching (described above).
183
201
 
184
202
  #### Invalidation
185
203
 
@@ -47,6 +47,17 @@ permitted to do that).
47
47
 
48
48
  The `action_policy.apply_rule` might have a large number of failures, 'cause it also tracks the usage of non-raising applications (i.e. `allowed_to?`).
49
49
 
50
+ ### `action_policy.init`
51
+
52
+ This event is triggered every time a new policy object is initialized.
53
+
54
+ The event contains the following information:
55
+
56
+ - `:policy` – policy class name.
57
+
58
+ This event is useful if you want to track the number of initialized policies per _action_ (for example, when you want to ensure that
59
+ the [memoization](caching.md) works as expected).
60
+
50
61
  ## Turn off instrumentation
51
62
 
52
63
  Instrumentation is enabled by default. To turn it off add to your configuration:
@@ -2,7 +2,12 @@
2
2
 
3
3
  Action Policy tries to automatically infer policy class from the target using the following _probes_:
4
4
 
5
- 1. If the target is a `Symbol`, then use `"#{target.to_s.classify}Policy"` as a `policy_name` (see below);
5
+ 1. If the target is a `Symbol`:
6
+
7
+ a) Try `"#{target.to_s.camelize}Policy"` as a `policy_name` (see below);
8
+
9
+ b) If `String#classify` is available, e.g. when using Rails' ActiveSupport, try `"#{target.to_s.classify}Policy"`;
10
+
6
11
  2. If the target responds to `policy_class`, then use it;
7
12
  3. If the target's class responds to `policy_class`, then use it;
8
13
  4. If the target or the target's class responds to `policy_name`, then use it (the `policy_name` should end with `Policy` as it's not appended automatically);
@@ -6,7 +6,7 @@ Consider an example:
6
6
 
7
7
  ```ruby
8
8
  module Admin
9
- class UsersController < ApplictionController
9
+ class UsersController < ApplicationController
10
10
  def index
11
11
  # uses Admin::UserPolicy if any, otherwise fallbacks to UserPolicy
12
12
  authorize!
@@ -20,7 +20,7 @@ Module nesting is also supported:
20
20
  ```ruby
21
21
  module Admin
22
22
  module Client
23
- class UsersController < ApplictionController
23
+ class UsersController < ApplicationController
24
24
  def index
25
25
  # lookup for Admin::Client::UserPolicy -> Admin::UserPolicy -> UserPolicy
26
26
  authorize!
@@ -34,9 +34,11 @@ class ApplicationPolicy < ActionPolicy::Base
34
34
  end
35
35
  ```
36
36
 
37
- You could use the following command to generate it.
37
+ You could use the following command to generate it when using Rails:
38
38
 
39
- $ rails generate action_policy:install
39
+ ```sh
40
+ rails generate action_policy:install
41
+ ```
40
42
 
41
43
  **NOTE:** it is not necessary to inherit from `ActionPolicy::Base`; instead, you can [construct basic policy](custom_policy.md) choosing only the components you need.
42
44
 
@@ -96,7 +98,7 @@ There is also an `allowed_to?` method which returns `true` or `false` and could
96
98
  <% @posts.each do |post| %>
97
99
  <li><%= post.title %>
98
100
  <% if allowed_to?(:edit?, post) %>
99
- = link_to "Edit", post
101
+ <%= link_to "Edit", post %>
100
102
  <% end %>
101
103
  </li>
102
104
  <% end %>
@@ -6,6 +6,13 @@ In most cases, you do not have to do anything except writing policy files and ad
6
6
 
7
7
  **NOTE:** both controllers and channels extensions are built on top of the Action Policy [behaviour](./behaviour.md) mixin.
8
8
 
9
+ ## Generators
10
+
11
+ Action Policy provides a couple of useful Rails generators:
12
+
13
+ - `rails g action_policy:install` — adds `app/policies/application_policy.rb` file
14
+ - `rails g action_policy:policy MODEL_NAME` — adds a policy file and a policy test file for a given model (also creates an `application_policy.rb` if it's missing)
15
+
9
16
  ## Controllers integration
10
17
 
11
18
  Action Policy assumes that you have a `current_user` method which specifies the current authenticated subject (`user`).
@@ -112,6 +112,8 @@ OR
112
112
 
113
113
  ### Testing scopes
114
114
 
115
+ #### Active Record relation example
116
+
115
117
  There is no single rule on how to test scopes, 'cause it dependes on the _nature_ of the scope.
116
118
 
117
119
  Here's an example of RSpec tests for Active Record scoping rules:
@@ -156,6 +158,35 @@ describe PostPolicy do
156
158
  end
157
159
  ```
158
160
 
161
+ #### Action Controller params example
162
+
163
+ Here's an example of RSpec tests for Action Controller parameters scoping rules:
164
+
165
+ ```ruby
166
+ describe PostPolicy do
167
+ describe "params scope" do
168
+ let(:user) { build_stubbed :user }
169
+ let(:context) { {user: user} }
170
+
171
+ let(:params) { {name: "a", password: "b"} }
172
+ let(:target) { ActionController::Parameters.new(params) }
173
+
174
+ # it's easier to asses the hash representation, not the AC::Params object
175
+ subject { policy.apply_scope(target, type: :action_controller_params).to_h }
176
+
177
+ context "as user" do
178
+ it { is_expected.to eq({name: "a"}) }
179
+ end
180
+
181
+ context "as manager" do
182
+ before { user.update!(role: :manager) }
183
+
184
+ it { is_expected.to eq({name: "a", password: "b"}) }
185
+ end
186
+ end
187
+ end
188
+ ```
189
+
159
190
  ## Testing authorization
160
191
 
161
192
  To test the act of authorization you have to make sure that the `authorize!` method is called with the appropriate arguments.
@@ -230,6 +261,12 @@ end
230
261
 
231
262
  If you omit `.with(PostPolicy)` then the inferred policy for the target (`post`) would be used.
232
263
 
264
+ RSpec composed matchers are available as target:
265
+
266
+ ```ruby
267
+ expect { subject }.to be_authorized_to(:show?, an_instance_of(Post))
268
+ ```
269
+
233
270
  ## Testing scoping
234
271
 
235
272
  Action Policy provides a way to test that a correct scoping has been applied during the code execution.
@@ -325,3 +362,29 @@ expect { subject }.to have_authorized_scope(:scope)
325
362
  expect(target).to eq(User.all)
326
363
  }
327
364
  ```
365
+
366
+
367
+ ## Testing views
368
+
369
+ When you test views that call policies methods as `allowed_to?`, your may have `Missing policy authorization context: user` error.
370
+ You may need to stub `current_user` to resolve the issue.
371
+
372
+ Consider an RSpec example:
373
+
374
+ ```ruby
375
+ describe "users/index.html.slim" do
376
+ let(:user) { build_stubbed :user }
377
+ let(:users) { create_list(:user, 2) }
378
+
379
+ before do
380
+ allow(controller).to receive(:current_user).and_return(user)
381
+
382
+ assign :users, users
383
+ render
384
+ end
385
+
386
+ describe "displays user#index correctly" do
387
+ it { expect(rendered).to have_link(users.first.email, href: edit_user_path(users.first)) }
388
+ end
389
+ end
390
+ ```
@@ -56,7 +56,7 @@ You can also specify all the usual options (such as `with`).
56
56
 
57
57
  There is also a `check?` method which is just an "alias"\* for `allowed_to?` added for better readability:
58
58
 
59
- ```
59
+ ```ruby
60
60
  class PostPolicy < ApplicationPolicy
61
61
  def show?
62
62
  user.admin? || check?(:publicly_visible?)
@@ -2,6 +2,7 @@ source "https://rubygems.org"
2
2
 
3
3
  gem "sqlite3", "~> 1.3.0"
4
4
  gem "rails", "~> 4.2"
5
+ gem "thor", "< 1.0"
5
6
  gem "method_source"
6
7
  gem "unparser"
7
8
 
@@ -31,7 +31,7 @@ module ActionPolicy
31
31
 
32
32
  # Find a policy class for a target
33
33
  def lookup(target, allow_nil: false, **options)
34
- LookupChain.call(target, options) ||
34
+ LookupChain.call(target, **options) ||
35
35
  (allow_nil ? nil : raise(NotFound, target))
36
36
  end
37
37
  end
@@ -38,6 +38,8 @@ module ActionPolicy
38
38
  record = implicit_authorization_target! if record == :__undef__
39
39
  raise ArgumentError, "Record must be specified" if record.nil?
40
40
 
41
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
42
+
41
43
  policy = policy_for(record: record, **options)
42
44
 
43
45
  Authorizer.call(policy, authorization_rule_for(policy, to))
@@ -50,6 +52,8 @@ module ActionPolicy
50
52
  record = implicit_authorization_target! if record == :__undef__
51
53
  raise ArgumentError, "Record must be specified" if record.nil?
52
54
 
55
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
56
+
53
57
  policy = policy_for(record: record, **options)
54
58
 
55
59
  policy.apply(authorization_rule_for(policy, rule))
@@ -19,9 +19,6 @@ module ActionPolicy
19
19
  #
20
20
  # policy.equal?(policy_for(record, with: CustomPolicy)) #=> false
21
21
  module Memoized
22
- require "action_policy/ext/policy_cache_key"
23
- using ActionPolicy::Ext::PolicyCacheKey
24
-
25
22
  class << self
26
23
  def prepended(base)
27
24
  base.prepend InstanceMethods
@@ -32,13 +29,12 @@ module ActionPolicy
32
29
 
33
30
  module InstanceMethods # :nodoc:
34
31
  def policy_for(record:, **opts)
35
- __policy_memoize__(record, opts) { super(record: record, **opts) }
32
+ __policy_memoize__(record, **opts) { super(record: record, **opts) }
36
33
  end
37
34
  end
38
35
 
39
- def __policy_memoize__(record, with: nil, namespace: nil, **_opts)
40
- record_key = record._policy_cache_key(use_object_id: true)
41
- cache_key = "#{namespace}/#{with}/#{record_key}"
36
+ def __policy_memoize__(record, **options)
37
+ cache_key = policy_for_cache_key(record: record, **options)
42
38
 
43
39
  return __policies_cache__[cache_key] if
44
40
  __policies_cache__.key?(cache_key)
@@ -4,11 +4,13 @@ module ActionPolicy
4
4
  module Behaviours
5
5
  # Adds `policy_for` method
6
6
  module PolicyFor
7
+ require "action_policy/ext/policy_cache_key"
8
+ using ActionPolicy::Ext::PolicyCacheKey
9
+
7
10
  # Returns policy instance for the record.
8
- def policy_for(record:, with: nil, namespace: nil, context: nil, **options)
9
- namespace ||= authorization_namespace
10
- policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace, **options)
11
- policy_class&.new(record, authorization_context.tap { |ctx| ctx.merge!(context) if context })
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false)
12
+ policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace, context: context, allow_nil: allow_nil)
13
+ policy_class&.new(record, **context)
12
14
  end
13
15
 
14
16
  def authorization_context
@@ -41,6 +43,13 @@ module ActionPolicy
41
43
  ]
42
44
  )
43
45
  end
46
+
47
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
48
+ record_key = record._policy_cache_key(use_object_id: true)
49
+ context_key = context.values.map { |v| v._policy_cache_key(use_object_id: true) }.join(".")
50
+
51
+ "#{namespace}/#{with}/#{context_key}/#{record_key}"
52
+ end
44
53
  end
45
54
  end
46
55
  end
@@ -11,6 +11,8 @@ module ActionPolicy
11
11
  # - secondly, try to infer policy class from `target` (non-raising lookup)
12
12
  # - use `implicit_authorization_target` if none of the above works.
13
13
  def authorized_scope(target, type: nil, as: :default, scope_options: nil, **options)
14
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
15
+
14
16
  policy = policy_for(record: target, allow_nil: true, **options)
15
17
  policy ||= policy_for(record: implicit_authorization_target!, **options)
16
18
 
@@ -37,9 +37,6 @@ module ActionPolicy
37
37
  #
38
38
  # NOTE: don't forget to clear thread cache with ActionPolicy::PerThreadCache.clear_all
39
39
  module ThreadMemoized
40
- require "action_policy/ext/policy_cache_key"
41
- using ActionPolicy::Ext::PolicyCacheKey
42
-
43
40
  class << self
44
41
  def prepended(base)
45
42
  base.prepend InstanceMethods
@@ -50,13 +47,12 @@ module ActionPolicy
50
47
 
51
48
  module InstanceMethods # :nodoc:
52
49
  def policy_for(record:, **opts)
53
- __policy_thread_memoize__(record, opts) { super(record: record, **opts) }
50
+ __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
54
51
  end
55
52
  end
56
53
 
57
- def __policy_thread_memoize__(record, with: nil, namespace: nil, **_opts)
58
- record_key = record._policy_cache_key(use_object_id: true)
59
- cache_key = "#{namespace}/#{with}/#{record_key}"
54
+ def __policy_thread_memoize__(record, **options)
55
+ cache_key = policy_for_cache_key(record: record, **options)
60
56
 
61
57
  ActionPolicy::PerThreadCache.fetch(cache_key) { yield }
62
58
  end
@@ -13,19 +13,15 @@ module ActionPolicy
13
13
  module ObjectExt
14
14
  def _policy_cache_key(use_object_id: false)
15
15
  return policy_cache_key if respond_to?(:policy_cache_key)
16
+ return cache_key_with_version if respond_to?(:cache_key_with_version)
16
17
  return cache_key if respond_to?(:cache_key)
17
18
 
18
- return object_id if use_object_id == true
19
+ return object_id.to_s if use_object_id == true
19
20
 
20
21
  raise ArgumentError, "object is not cacheable"
21
22
  end
22
23
  end
23
24
 
24
- # JRuby doesn't support _global_ modules refinements (see https://github.com/jruby/jruby/issues/5220)
25
- # Fallback to monkey-patching.
26
- # TODO: remove after 9.2.7.0 (See https://github.com/jruby/jruby/pull/5627)
27
- ::Object.include(ObjectExt) if RUBY_PLATFORM =~ /java/i
28
-
29
25
  refine Object do
30
26
  include ObjectExt
31
27
  end
@@ -85,6 +81,12 @@ module ActionPolicy
85
81
  to_s
86
82
  end
87
83
  end
84
+
85
+ refine Module do
86
+ def _policy_cache_key(*)
87
+ name
88
+ end
89
+ end
88
90
  end
89
91
  end
90
92
  end
@@ -2,15 +2,15 @@
2
2
 
3
3
  module ActionPolicy
4
4
  module Ext
5
- # Add `classify` to Symbol
6
- module SymbolClassify
5
+ # Add `camelize` to Symbol
6
+ module SymbolCamelize
7
7
  refine Symbol do
8
- if "".respond_to?(:classify)
9
- def classify
10
- to_s.classify
8
+ if "".respond_to?(:camelize)
9
+ def camelize
10
+ to_s.camelize
11
11
  end
12
12
  else
13
- def classify
13
+ def camelize
14
14
  word = to_s.capitalize
15
15
  word.gsub!(/(?:_)([a-z\d]*)/) { $1.capitalize }
16
16
  word
@@ -12,8 +12,8 @@ module ActionPolicy
12
12
  using ActionPolicy::Ext::StringConstantize
13
13
  end
14
14
 
15
- require "action_policy/ext/symbol_classify"
16
- using ActionPolicy::Ext::SymbolClassify
15
+ require "action_policy/ext/symbol_camelize"
16
+ using ActionPolicy::Ext::SymbolCamelize
17
17
 
18
18
  require "action_policy/ext/module_namespace"
19
19
  using ActionPolicy::Ext::ModuleNamespace
@@ -45,7 +45,7 @@ module ActionPolicy
45
45
 
46
46
  def call(record, **opts)
47
47
  chain.each do |probe|
48
- val = probe.call(record, opts)
48
+ val = probe.call(record, **opts)
49
49
  return val unless val.nil?
50
50
  end
51
51
  nil
@@ -54,6 +54,7 @@ module ActionPolicy
54
54
  private
55
55
 
56
56
  def lookup_within_namespace(policy_name, namespace)
57
+ return unless namespace
57
58
  NamespaceCache.fetch(namespace.name, policy_name) do
58
59
  mod = namespace
59
60
 
@@ -88,12 +89,12 @@ module ActionPolicy
88
89
  !ENV["RACK_ENV"].nil? ? ENV["RACK_ENV"] == "production" : true
89
90
 
90
91
  # By self `policy_class` method
91
- INSTANCE_POLICY_CLASS = ->(record, _) {
92
+ INSTANCE_POLICY_CLASS = ->(record, **) {
92
93
  record.policy_class if record.respond_to?(:policy_class)
93
94
  }
94
95
 
95
96
  # By record's class `policy_class` method
96
- CLASS_POLICY_CLASS = ->(record, _) {
97
+ CLASS_POLICY_CLASS = ->(record, **) {
97
98
  record.class.policy_class if record.class.respond_to?(:policy_class)
98
99
  }
99
100
 
@@ -106,7 +107,7 @@ module ActionPolicy
106
107
  }
107
108
 
108
109
  # Infer from class name
109
- INFER_FROM_CLASS = ->(record, _) {
110
+ INFER_FROM_CLASS = ->(record, **) {
110
111
  policy_class_name_for(record).safe_constantize
111
112
  }
112
113
 
@@ -114,20 +115,25 @@ module ActionPolicy
114
115
  SYMBOL_LOOKUP = ->(record, namespace: nil, **) {
115
116
  next unless record.is_a?(Symbol)
116
117
 
117
- policy_name = "#{record.classify}Policy"
118
- if namespace.nil?
119
- policy_name.safe_constantize
120
- else
121
- lookup_within_namespace(policy_name, namespace)
122
- end
118
+ policy_name = "#{record.camelize}Policy"
119
+ lookup_within_namespace(policy_name, namespace) || policy_name.safe_constantize
120
+ }
121
+
122
+ # (Optional) Infer using String#classify if available
123
+ CLASSIFY_SYMBOL_LOOKUP = ->(record, namespace: nil, **) {
124
+ next unless record.is_a?(Symbol)
125
+
126
+ policy_name = "#{record.to_s.classify}Policy"
127
+ lookup_within_namespace(policy_name, namespace) || policy_name.safe_constantize
123
128
  }
124
129
 
125
130
  self.chain = [
126
131
  SYMBOL_LOOKUP,
132
+ (CLASSIFY_SYMBOL_LOOKUP if String.method_defined?(:classify)),
127
133
  INSTANCE_POLICY_CLASS,
128
134
  CLASS_POLICY_CLASS,
129
135
  NAMESPACE_LOOKUP,
130
136
  INFER_FROM_CLASS
131
- ]
137
+ ].compact
132
138
  end
133
139
  end