action_policy 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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