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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +4 -1
- data/.github/bug_report_template.rb +175 -0
- data/.travis.yml +4 -4
- data/CHANGELOG.md +54 -0
- data/LICENSE.txt +1 -1
- data/README.md +7 -10
- data/benchmarks/namespaced_lookup_cache.rb +8 -5
- data/benchmarks/pre_checks.rb +73 -0
- data/docs/README.md +2 -0
- data/docs/caching.md +22 -4
- data/docs/instrumentation.md +11 -0
- data/docs/lookup_chain.md +6 -1
- data/docs/namespaces.md +2 -2
- data/docs/quick_start.md +5 -3
- data/docs/rails.md +7 -0
- data/docs/testing.md +63 -0
- data/docs/writing_policies.md +1 -1
- data/gemfiles/rails42.gemfile +1 -0
- data/lib/action_policy.rb +1 -1
- data/lib/action_policy/behaviour.rb +4 -0
- data/lib/action_policy/behaviours/memoized.rb +3 -7
- data/lib/action_policy/behaviours/policy_for.rb +13 -4
- data/lib/action_policy/behaviours/scoping.rb +2 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +3 -7
- data/lib/action_policy/ext/policy_cache_key.rb +8 -6
- data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
- data/lib/action_policy/lookup_chain.rb +19 -13
- data/lib/action_policy/policy/authorization.rb +7 -11
- data/lib/action_policy/policy/cache.rb +26 -4
- data/lib/action_policy/policy/core.rb +2 -2
- data/lib/action_policy/policy/pre_check.rb +2 -2
- data/lib/action_policy/policy/reasons.rb +2 -2
- data/lib/action_policy/rails/ext/active_record.rb +7 -0
- data/lib/action_policy/rails/policy/instrumentation.rb +9 -2
- data/lib/action_policy/rspec/dsl.rb +6 -6
- data/lib/action_policy/rspec/have_authorized_scope.rb +2 -2
- data/lib/action_policy/testing.rb +3 -3
- data/lib/action_policy/version.rb +1 -1
- data/lib/generators/action_policy/install/templates/application_policy.rb +1 -1
- metadata +5 -4
- data/.github/FUNDING.yml +0 -1
data/docs/README.md
CHANGED
@@ -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)]
|
data/docs/caching.md
CHANGED
@@ -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
|
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
|
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 `
|
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
|
|
data/docs/instrumentation.md
CHANGED
@@ -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:
|
data/docs/lookup_chain.md
CHANGED
@@ -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
|
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);
|
data/docs/namespaces.md
CHANGED
@@ -6,7 +6,7 @@ Consider an example:
|
|
6
6
|
|
7
7
|
```ruby
|
8
8
|
module Admin
|
9
|
-
class UsersController <
|
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 <
|
23
|
+
class UsersController < ApplicationController
|
24
24
|
def index
|
25
25
|
# lookup for Admin::Client::UserPolicy -> Admin::UserPolicy -> UserPolicy
|
26
26
|
authorize!
|
data/docs/quick_start.md
CHANGED
@@ -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
|
-
|
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
|
-
|
101
|
+
<%= link_to "Edit", post %>
|
100
102
|
<% end %>
|
101
103
|
</li>
|
102
104
|
<% end %>
|
data/docs/rails.md
CHANGED
@@ -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`).
|
data/docs/testing.md
CHANGED
@@ -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
|
+
```
|
data/docs/writing_policies.md
CHANGED
@@ -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?)
|
data/gemfiles/rails42.gemfile
CHANGED
data/lib/action_policy.rb
CHANGED
@@ -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,
|
40
|
-
|
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:
|
9
|
-
namespace
|
10
|
-
policy_class
|
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,
|
58
|
-
|
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 `
|
6
|
-
module
|
5
|
+
# Add `camelize` to Symbol
|
6
|
+
module SymbolCamelize
|
7
7
|
refine Symbol do
|
8
|
-
if "".respond_to?(:
|
9
|
-
def
|
10
|
-
to_s.
|
8
|
+
if "".respond_to?(:camelize)
|
9
|
+
def camelize
|
10
|
+
to_s.camelize
|
11
11
|
end
|
12
12
|
else
|
13
|
-
def
|
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/
|
16
|
-
using ActionPolicy::Ext::
|
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.
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|