action_policy 0.1.4 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/.travis.yml +2 -2
- data/CHANGELOG.md +21 -0
- data/docs/caching.md +3 -1
- data/docs/custom_policy.md +3 -1
- data/docs/quick_start.md +9 -1
- data/docs/reasons.md +13 -4
- data/docs/writing_policies.md +36 -0
- data/lib/action_policy/authorizer.rb +2 -3
- data/lib/action_policy/ext/module_namespace.rb +17 -10
- data/lib/action_policy/ext/policy_cache_key.rb +27 -5
- data/lib/action_policy/ext/string_underscore.rb +18 -0
- data/lib/action_policy/ext/yield_self_then.rb +25 -0
- data/lib/action_policy/policy/aliases.rb +5 -10
- data/lib/action_policy/policy/authorization.rb +11 -16
- data/lib/action_policy/policy/cache.rb +18 -11
- data/lib/action_policy/policy/cached_apply.rb +9 -11
- data/lib/action_policy/policy/core.rb +61 -10
- data/lib/action_policy/policy/execution_result.rb +26 -0
- data/lib/action_policy/policy/pre_check.rb +5 -10
- data/lib/action_policy/policy/reasons.rb +49 -51
- data/lib/action_policy/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a261b126090b29222039294b84b4575fa796bf115033c2514fe301e0ec3d34af
|
4
|
+
data.tar.gz: 05f9d1cd84b2f4ce2b951267e0122fb54a436a5e51f0dd2f9a7d79e06f30d6ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80bbbe6ec61bf7241fe0e823844120292e6b8efd98a3c06834d98ac797cf92ebdffa21332e40305770622d5120d391443d0570193001a5e358f1ee6056c09bbb
|
7
|
+
data.tar.gz: 7f6990f7bcd5998eef536640a451591801afd073e29b25a764c500b1d0f535e3cfaf1a176f0adf930f498386ca9f206aa377b4c5adf1b2d119be642681fa7e81
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
@@ -11,7 +11,7 @@ matrix:
|
|
11
11
|
include:
|
12
12
|
- rvm: ruby-head
|
13
13
|
gemfile: gemfiles/railsmaster.gemfile
|
14
|
-
- rvm: jruby-9.
|
14
|
+
- rvm: jruby-9.2.0.0
|
15
15
|
gemfile: gemfiles/jruby.gemfile
|
16
16
|
- rvm: 2.5.1
|
17
17
|
gemfile: Gemfile
|
@@ -24,5 +24,5 @@ matrix:
|
|
24
24
|
allow_failures:
|
25
25
|
- rvm: ruby-head
|
26
26
|
gemfile: gemfiles/railsmaster.gemfile
|
27
|
-
- rvm: jruby-9.
|
27
|
+
- rvm: jruby-9.2.0.0
|
28
28
|
gemfile: gemfiles/jruby.gemfile
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
## master
|
2
2
|
|
3
|
+
## 0.2.0 (2018-06-17)
|
4
|
+
|
5
|
+
- Make `action_policy` JRuby-compatible. ([@palkan][])
|
6
|
+
|
7
|
+
- Add `reasons.details`. ([@palkan][])
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
rescue_from ActionPolicy::Unauthorized do |ex|
|
11
|
+
ex.result.reasons.details #=> { stage: [:show?] }
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
- Add `ExecutionResult`. ([@palkan][])
|
16
|
+
|
17
|
+
ExecutionResult contains all the rule application artifacts: the result (`true` / `false`),
|
18
|
+
failures reasons.
|
19
|
+
|
20
|
+
This value is now stored in a cache (if any) instead of just the call result (`true` / `false`).
|
21
|
+
|
22
|
+
- Add `Policy.identifier`. ([@palkan][])
|
23
|
+
|
3
24
|
## 0.1.4 (2018-06-06)
|
4
25
|
|
5
26
|
- Fix Railtie injection hook. ([@palkan][])
|
data/docs/caching.md
CHANGED
@@ -155,7 +155,9 @@ Rails.application.configure do |config|
|
|
155
155
|
end
|
156
156
|
```
|
157
157
|
|
158
|
-
Cache store must provide at least a `#
|
158
|
+
Cache store must provide at least a `#read(key)` and `write(key, value, **options)` methods.
|
159
|
+
|
160
|
+
**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.
|
159
161
|
|
160
162
|
By default, Action Policy builds a cache key using the following scheme:
|
161
163
|
|
data/docs/custom_policy.md
CHANGED
@@ -10,10 +10,12 @@ It looks like this:
|
|
10
10
|
class ActionPolicy::Base
|
11
11
|
include ActionPolicy::Policy::Core
|
12
12
|
include ActionPolicy::Policy::Authorization
|
13
|
-
include ActionPolicy::Policy::Reasons
|
14
13
|
include ActionPolicy::Policy::PreCheck
|
14
|
+
include ActionPolicy::Policy::Reasons
|
15
15
|
include ActionPolicy::Policy::Aliases
|
16
|
+
include ActionPolicy::Policy::Cache
|
16
17
|
include ActionPolicy::Policy::CachedApply
|
18
|
+
include ActionPolicy::Policy::Defaults
|
17
19
|
|
18
20
|
# ActionPolicy::Policy::Defaults module adds the following
|
19
21
|
|
data/docs/quick_start.md
CHANGED
@@ -74,7 +74,15 @@ end
|
|
74
74
|
|
75
75
|
In the above case, Action Policy automatically infers a policy class and a rule to verify access: `@post -> Post -> PostPolicy`, rule is inferred from the action name (`update -> update?`), and `current_user` is used as `user` within the policy by default (read more about [authorization context](authorization_context.md)).
|
76
76
|
|
77
|
-
When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised
|
77
|
+
When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
rescue_from ActionPolicy::Unauthorized do |ex|
|
81
|
+
# Exception object contains the following information
|
82
|
+
ex.policy #=> policy class, e.g. UserPolicy
|
83
|
+
ex.rule #=> applied rule, e.g. :show?
|
84
|
+
end
|
85
|
+
```
|
78
86
|
|
79
87
|
There is also an `allowed_to?` method which returns `true` or `false` and could be used, for example, in views:
|
80
88
|
|
data/docs/reasons.md
CHANGED
@@ -17,16 +17,23 @@ class ApplicantPolicy < ApplicationPolicy
|
|
17
17
|
end
|
18
18
|
```
|
19
19
|
|
20
|
-
When `ApplicantPolicy#show?` check fails, the exception has the `
|
20
|
+
When `ApplicantPolicy#show?` check fails, the exception has the `result` object, which in its turn contains additional information about the failure (`reasons`):
|
21
21
|
|
22
22
|
```ruby
|
23
23
|
class ApplicationController < ActionController::Base
|
24
24
|
rescue_from ActionPolicy::Unauthorized do |ex|
|
25
|
-
p ex.reasons.
|
25
|
+
p ex.result.reasons.details #=> { stage: [:show?] }
|
26
|
+
|
27
|
+
# or with i18n support
|
28
|
+
p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
|
26
29
|
end
|
27
30
|
end
|
28
31
|
```
|
29
32
|
|
33
|
+
The reason key is the corresponding policy [identifier](writing_policies.md#identifiers).
|
34
|
+
|
35
|
+
**NOTE:** `full_messages` support hasn't been released yet. See [the issue](https://github.com/palkan/action_policy/issues/15).
|
36
|
+
|
30
37
|
You can also wrap _local_ rules into `allowed_to?` to populate reasons:
|
31
38
|
|
32
39
|
```ruby
|
@@ -42,12 +49,14 @@ class ApplicantPolicy < ApplicationPolicy
|
|
42
49
|
end
|
43
50
|
|
44
51
|
# then the reasons object could be
|
45
|
-
p ex.reasons.
|
52
|
+
p ex.result.reasons.details #=> { applicant: [:view_applicants?] }
|
46
53
|
|
47
54
|
# or
|
48
|
-
p ex.reasons.
|
55
|
+
p ex.result.reasons.details #=> { stage: [:show?] }
|
49
56
|
```
|
50
57
|
|
58
|
+
|
59
|
+
|
51
60
|
**What is the point of failure reasons?**
|
52
61
|
|
53
62
|
First, you can provide a user with helpful feedback. For example, in the above scenario, when the reason is `ApplicantPolicy#view_applicants?`, you could show the following message:
|
data/docs/writing_policies.md
CHANGED
@@ -53,3 +53,39 @@ end
|
|
53
53
|
```
|
54
54
|
|
55
55
|
You can also specify all the usual options (such as `with`).
|
56
|
+
|
57
|
+
## Identifiers
|
58
|
+
|
59
|
+
Each policy class has an `identifier`, which is by default just an underscored class name:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class CommentPolicy < ApplicationPolicy
|
63
|
+
end
|
64
|
+
|
65
|
+
CommentPolicy.identifier #=> :comment
|
66
|
+
```
|
67
|
+
|
68
|
+
For namespaced policies it has a form of:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
module ActiveAdmin
|
72
|
+
class UserPolicy < ApplicationPolicy
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
ActiveAdmin::UserPolicy.identifier # => :"active_admin/user"
|
77
|
+
```
|
78
|
+
|
79
|
+
You can specify your own identifier:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
module MyVeryLong
|
83
|
+
class LongLongNamePolicy < ApplicationPolicy
|
84
|
+
self.identifier = :long_name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
MyVeryLong::LongLongNamePolicy.identifier #=> :long_name
|
89
|
+
```
|
90
|
+
|
91
|
+
Identifiers are required for some modules, such as [failure reasons tracking](reasons.md) and [i18n](i18n.md).
|
@@ -3,13 +3,12 @@
|
|
3
3
|
module ActionPolicy
|
4
4
|
# Raised when `authorize!` check fails
|
5
5
|
class Unauthorized < Error
|
6
|
-
attr_reader :policy, :rule, :
|
6
|
+
attr_reader :policy, :rule, :result
|
7
7
|
|
8
8
|
def initialize(policy, rule)
|
9
9
|
@policy = policy.class
|
10
10
|
@rule = rule
|
11
|
-
|
12
|
-
@reasons = policy.reasons if policy.respond_to?(:reasons)
|
11
|
+
@result = policy.result
|
13
12
|
end
|
14
13
|
end
|
15
14
|
|
@@ -3,24 +3,31 @@
|
|
3
3
|
module ActionPolicy
|
4
4
|
module Ext
|
5
5
|
# Add Module#namespace method
|
6
|
-
module ModuleNamespace
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
6
|
+
module ModuleNamespace # :nodoc: all
|
7
|
+
unless "".respond_to?(:safe_constantize)
|
8
|
+
require "action_policy/ext/string_constantize"
|
9
|
+
using ActionPolicy::Ext::StringConstantize
|
10
|
+
end
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
unless "".respond_to?(:match?)
|
13
|
+
require "action_policy/ext/string_match"
|
14
|
+
using ActionPolicy::Ext::StringMatch
|
15
|
+
end
|
17
16
|
|
17
|
+
module Ext
|
18
18
|
def namespace
|
19
19
|
return unless name.match?(/[^^]::/)
|
20
20
|
|
21
21
|
name.sub(/::[^:]+$/, "").safe_constantize
|
22
22
|
end
|
23
23
|
end
|
24
|
+
|
25
|
+
# See https://github.com/jruby/jruby/issues/5220
|
26
|
+
::Module.include(Ext) if RUBY_PLATFORM =~ /java/i
|
27
|
+
|
28
|
+
refine Module do
|
29
|
+
include Ext
|
30
|
+
end
|
24
31
|
end
|
25
32
|
end
|
26
33
|
end
|
@@ -9,8 +9,8 @@ module ActionPolicy
|
|
9
9
|
# For other core classes returns string representation.
|
10
10
|
#
|
11
11
|
# Raises ArgumentError otherwise.
|
12
|
-
module PolicyCacheKey
|
13
|
-
|
12
|
+
module PolicyCacheKey # :nodoc: all
|
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
16
|
return cache_key if respond_to?(:cache_key)
|
@@ -21,6 +21,14 @@ module ActionPolicy
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
# JRuby doesn't support _global_ modules refinements (see https://github.com/jruby/jruby/issues/5220)
|
25
|
+
# Fallback to monkey-patching.
|
26
|
+
::Object.include(ObjectExt) if RUBY_PLATFORM =~ /java/i
|
27
|
+
|
28
|
+
refine Object do
|
29
|
+
include ObjectExt
|
30
|
+
end
|
31
|
+
|
24
32
|
refine NilClass do
|
25
33
|
def _policy_cache_key(*)
|
26
34
|
""
|
@@ -51,9 +59,23 @@ module ActionPolicy
|
|
51
59
|
end
|
52
60
|
end
|
53
61
|
|
54
|
-
|
55
|
-
|
56
|
-
|
62
|
+
if RUBY_PLATFORM =~ /java/i
|
63
|
+
refine Integer do
|
64
|
+
def _policy_cache_key(*)
|
65
|
+
to_s
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
refine Float do
|
70
|
+
def _policy_cache_key(*)
|
71
|
+
to_s
|
72
|
+
end
|
73
|
+
end
|
74
|
+
else
|
75
|
+
refine Numeric do
|
76
|
+
def _policy_cache_key(*)
|
77
|
+
to_s
|
78
|
+
end
|
57
79
|
end
|
58
80
|
end
|
59
81
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext
|
5
|
+
# Add underscore to String
|
6
|
+
module StringUnderscore
|
7
|
+
refine String do
|
8
|
+
def underscore
|
9
|
+
word = gsub(/::/, "/")
|
10
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
11
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
12
|
+
word.downcase!
|
13
|
+
word
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext # :nodoc: all
|
5
|
+
# Add yield_self and then if missing
|
6
|
+
module YieldSelfThen
|
7
|
+
module Ext
|
8
|
+
unless nil.respond_to?(:yield_self)
|
9
|
+
def yield_self
|
10
|
+
yield self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
alias then yield_self
|
15
|
+
end
|
16
|
+
|
17
|
+
# See https://github.com/jruby/jruby/issues/5220
|
18
|
+
::Object.include(Ext) if RUBY_PLATFORM =~ /java/i
|
19
|
+
|
20
|
+
refine Object do
|
21
|
+
include Ext
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -7,7 +7,7 @@ module ActionPolicy
|
|
7
7
|
#
|
8
8
|
# class ApplicationPolicy
|
9
9
|
# include ActionPolicy::Policy::Core
|
10
|
-
#
|
10
|
+
# include ActionPolicy::Policy::Aliases
|
11
11
|
#
|
12
12
|
# # define which rule to use if `authorize!` called with
|
13
13
|
# # unknown rule
|
@@ -23,19 +23,14 @@ module ActionPolicy
|
|
23
23
|
DEFAULT = :__default__
|
24
24
|
|
25
25
|
class << self
|
26
|
-
def
|
26
|
+
def included(base)
|
27
27
|
base.extend ClassMethods
|
28
|
-
base.prepend InstanceMethods
|
29
28
|
end
|
30
|
-
|
31
|
-
alias included prepended
|
32
29
|
end
|
33
30
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
self.class.lookup_alias(activity) || super
|
38
|
-
end
|
31
|
+
def resolve_rule(activity)
|
32
|
+
return activity if respond_to?(activity)
|
33
|
+
self.class.lookup_alias(activity) || super
|
39
34
|
end
|
40
35
|
|
41
36
|
module ClassMethods # :nodoc:
|
@@ -36,34 +36,29 @@ module ActionPolicy
|
|
36
36
|
# ApplicantPolicy.new(user: user, account: account)
|
37
37
|
module Authorization
|
38
38
|
class << self
|
39
|
-
def
|
39
|
+
def included(base)
|
40
40
|
base.extend ClassMethods
|
41
|
-
base.prepend InstanceMethods
|
42
41
|
end
|
43
|
-
|
44
|
-
alias included prepended
|
45
42
|
end
|
46
43
|
|
47
44
|
attr_reader :authorization_context
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
super(*args)
|
52
|
-
|
53
|
-
@authorization_context = {}
|
46
|
+
def initialize(*args, **params)
|
47
|
+
super(*args)
|
54
48
|
|
55
|
-
|
56
|
-
raise AuthorizationContextMissing, id unless params.key?(id)
|
49
|
+
@authorization_context = {}
|
57
50
|
|
58
|
-
|
51
|
+
self.class.authorization_targets.each do |id, opts|
|
52
|
+
raise AuthorizationContextMissing, id unless params.key?(id)
|
59
53
|
|
60
|
-
|
54
|
+
val = params.fetch(id)
|
61
55
|
|
62
|
-
|
63
|
-
end
|
56
|
+
raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
|
64
57
|
|
65
|
-
authorization_context
|
58
|
+
authorization_context[id] = instance_variable_set("@#{id}", val)
|
66
59
|
end
|
60
|
+
|
61
|
+
authorization_context.freeze
|
67
62
|
end
|
68
63
|
|
69
64
|
module ClassMethods # :nodoc:
|
@@ -6,7 +6,10 @@ module ActionPolicy # :nodoc:
|
|
6
6
|
# By default cache namespace (or prefix) contains major and minor version of the gem
|
7
7
|
CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split('.').take(2).join('.')}"
|
8
8
|
|
9
|
+
require "action_policy/ext/yield_self_then"
|
9
10
|
require "action_policy/ext/policy_cache_key"
|
11
|
+
|
12
|
+
using ActionPolicy::Ext::YieldSelfThen
|
10
13
|
using ActionPolicy::Ext::PolicyCacheKey
|
11
14
|
|
12
15
|
module Policy
|
@@ -15,12 +18,9 @@ module ActionPolicy # :nodoc:
|
|
15
18
|
# NOTE: if cache_store is nil then we silently skip all the caching.
|
16
19
|
module Cache
|
17
20
|
class << self
|
18
|
-
def
|
21
|
+
def included(base)
|
19
22
|
base.extend ClassMethods
|
20
|
-
base.prepend InstanceMethods
|
21
23
|
end
|
22
|
-
|
23
|
-
alias included prepended
|
24
24
|
end
|
25
25
|
|
26
26
|
def cache_namespace
|
@@ -36,19 +36,26 @@ module ActionPolicy # :nodoc:
|
|
36
36
|
authorization_context.map { |_k, v| v._policy_cache_key.to_s }.join("/")
|
37
37
|
end
|
38
38
|
|
39
|
+
# rubocop: disable Metrics/AbcSize
|
39
40
|
def apply_with_cache(rule)
|
40
41
|
options = self.class.cached_rules.fetch(rule)
|
42
|
+
key = cache_key(rule)
|
41
43
|
|
42
|
-
ActionPolicy.cache_store.
|
44
|
+
ActionPolicy.cache_store.then do |store|
|
45
|
+
@result = store.read(key)
|
46
|
+
next result.value unless result.nil?
|
47
|
+
yield
|
48
|
+
store.write(key, result, options)
|
49
|
+
result.value
|
50
|
+
end
|
43
51
|
end
|
52
|
+
# rubocop: enable Metrics/AbcSize
|
44
53
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
!self.class.cached_rules.key?(rule)
|
54
|
+
def apply(rule)
|
55
|
+
return super if ActionPolicy.cache_store.nil? ||
|
56
|
+
!self.class.cached_rules.key?(rule)
|
49
57
|
|
50
|
-
|
51
|
-
end
|
58
|
+
apply_with_cache(rule) { super }
|
52
59
|
end
|
53
60
|
|
54
61
|
module ClassMethods # :nodoc:
|
@@ -7,21 +7,19 @@ module ActionPolicy
|
|
7
7
|
# When you call `apply` twice on the same policy and for the same rule,
|
8
8
|
# the check (and pre-checks) is only called once.
|
9
9
|
module CachedApply
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def apply(rule)
|
11
|
+
@__rules_cache__ ||= {}
|
12
|
+
|
13
|
+
if @__rules_cache__.key?(rule)
|
14
|
+
@result = @__rules_cache__[rule]
|
15
|
+
return result.value
|
13
16
|
end
|
14
17
|
|
15
|
-
|
16
|
-
end
|
18
|
+
super
|
17
19
|
|
18
|
-
|
19
|
-
def apply(rule)
|
20
|
-
@__rules_cache__ ||= {}
|
21
|
-
return @__rules_cache__[rule] if @__rules_cache__.key?(rule)
|
20
|
+
@__rules_cache__[rule] = result
|
22
21
|
|
23
|
-
|
24
|
-
end
|
22
|
+
result.value
|
25
23
|
end
|
26
24
|
end
|
27
25
|
end
|
@@ -1,6 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "action_policy/behaviours/policy_for"
|
4
|
+
require "action_policy/policy/execution_result"
|
5
|
+
|
6
|
+
unless "".respond_to?(:underscore)
|
7
|
+
require "action_policy/ext/string_underscore"
|
8
|
+
using ActionPolicy::Ext::StringUnderscore
|
9
|
+
end
|
4
10
|
|
5
11
|
module ActionPolicy
|
6
12
|
# Raised when `resolve_rule` failed to find an approriate
|
@@ -18,35 +24,80 @@ module ActionPolicy
|
|
18
24
|
module Policy
|
19
25
|
# Core policy API
|
20
26
|
module Core
|
27
|
+
class << self
|
28
|
+
def included(base)
|
29
|
+
base.extend ClassMethods
|
30
|
+
|
31
|
+
# Generate a new class for each _policy chain_
|
32
|
+
# in order to extend it independently
|
33
|
+
base.module_eval do
|
34
|
+
@result_class = Class.new(ExecutionResult)
|
35
|
+
|
36
|
+
# we need to make this class _named_,
|
37
|
+
# 'cause anonymous classes couldn't be marshalled
|
38
|
+
base.const_set(:APR, @result_class)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods # :nodoc:
|
44
|
+
attr_writer :identifier
|
45
|
+
|
46
|
+
def result_class
|
47
|
+
return @result_class if instance_variable_defined?(:@result_class)
|
48
|
+
@result_class = superclass.result_class
|
49
|
+
end
|
50
|
+
|
51
|
+
def identifier
|
52
|
+
return @identifier if instance_variable_defined?(:@identifier)
|
53
|
+
|
54
|
+
@identifier = name.sub(/Policy$/, "").underscore.to_sym
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
21
58
|
include ActionPolicy::Behaviours::PolicyFor
|
22
59
|
|
23
|
-
attr_reader :record
|
60
|
+
attr_reader :record, :result
|
24
61
|
|
25
62
|
def initialize(record = nil)
|
26
63
|
@record = record
|
27
64
|
end
|
28
65
|
|
29
|
-
# Returns a result of applying the specified rule.
|
66
|
+
# Returns a result of applying the specified rule (true of false).
|
30
67
|
# Unlike simply calling a predicate rule (`policy.manage?`),
|
31
68
|
# `apply` also calls pre-checks.
|
32
69
|
def apply(rule)
|
70
|
+
@result = self.class.result_class.new
|
71
|
+
@result.load __apply__(rule)
|
72
|
+
end
|
73
|
+
|
74
|
+
# This method performs the rule call.
|
75
|
+
# Override or extend it to provide custom functionality
|
76
|
+
# (such as caching, pre checks, etc.)
|
77
|
+
def __apply__(rule)
|
33
78
|
public_send(rule)
|
34
79
|
end
|
35
80
|
|
81
|
+
# Wrap code that could modify result
|
82
|
+
# to prevent the current result modification
|
83
|
+
def with_clean_result
|
84
|
+
was_result = @result
|
85
|
+
res = yield
|
86
|
+
@result = was_result
|
87
|
+
res
|
88
|
+
end
|
89
|
+
|
36
90
|
# Returns a result of applying the specified rule to the specified record.
|
37
91
|
# Under the hood a policy class for record is resolved
|
38
92
|
# (unless it's explicitly set through `with` option).
|
39
93
|
#
|
40
94
|
# If record is `nil` then we uses the current policy.
|
41
95
|
def allowed_to?(rule, record = :__undef__, **options)
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
policy.apply(rule)
|
96
|
+
if record == :__undef__
|
97
|
+
__apply__(rule)
|
98
|
+
else
|
99
|
+
policy_for(record: record, **options).apply(rule)
|
100
|
+
end
|
50
101
|
end
|
51
102
|
|
52
103
|
# Returns a rule name (policy method name) for activity.
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Result of applying a policy rule
|
6
|
+
#
|
7
|
+
# This class could be extended by some modules to provide
|
8
|
+
# additional functionality
|
9
|
+
class ExecutionResult
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
# Populate the final value
|
13
|
+
def load(value)
|
14
|
+
@value = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def success?
|
18
|
+
@value == true
|
19
|
+
end
|
20
|
+
|
21
|
+
def fail?
|
22
|
+
@value == false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -83,8 +83,8 @@ module ActionPolicy
|
|
83
83
|
def call(policy)
|
84
84
|
Result.new(policy.send(name)).tap do |res|
|
85
85
|
# add denial reason if Reasons included
|
86
|
-
policy.reasons.add(
|
87
|
-
res.denied? && policy.respond_to?(:reasons)
|
86
|
+
policy.result.reasons.add(policy, name) if
|
87
|
+
res.denied? && policy.result.respond_to?(:reasons)
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
@@ -136,12 +136,9 @@ module ActionPolicy
|
|
136
136
|
end
|
137
137
|
|
138
138
|
class << self
|
139
|
-
def
|
139
|
+
def included(base)
|
140
140
|
base.extend ClassMethods
|
141
|
-
base.prepend InstanceMethods
|
142
141
|
end
|
143
|
-
|
144
|
-
alias included prepended
|
145
142
|
end
|
146
143
|
|
147
144
|
def run_pre_checks(rule)
|
@@ -162,10 +159,8 @@ module ActionPolicy
|
|
162
159
|
Check::Result::ALLOW
|
163
160
|
end
|
164
161
|
|
165
|
-
|
166
|
-
|
167
|
-
run_pre_checks(rule) { super }
|
168
|
-
end
|
162
|
+
def __apply__(rule)
|
163
|
+
run_pre_checks(rule) { super }
|
169
164
|
end
|
170
165
|
|
171
166
|
module ClassMethods # :nodoc:
|
@@ -2,28 +2,33 @@
|
|
2
2
|
|
3
3
|
module ActionPolicy
|
4
4
|
module Policy
|
5
|
-
|
6
|
-
|
5
|
+
# Failures reasons store
|
6
|
+
class FailureReasons
|
7
|
+
attr_reader :details
|
7
8
|
|
8
|
-
def initialize
|
9
|
-
@
|
10
|
-
@rule = rule
|
9
|
+
def initialize
|
10
|
+
@details = {}
|
11
11
|
end
|
12
|
-
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
def add(policy_or_class, rule)
|
14
|
+
policy_class = policy_or_class.is_a?(Class) ? policy_or_class : policy_or_class.class
|
15
|
+
details[policy_class.identifier] ||= []
|
16
|
+
details[policy_class.identifier] << rule
|
17
|
+
end
|
18
18
|
|
19
|
-
|
19
|
+
def empty?
|
20
|
+
details.empty?
|
21
|
+
end
|
20
22
|
|
21
|
-
def
|
22
|
-
|
23
|
+
def present?
|
24
|
+
!empty?
|
23
25
|
end
|
26
|
+
end
|
24
27
|
|
25
|
-
|
26
|
-
|
28
|
+
# Extend ExecutionResult with `reasons` method
|
29
|
+
module ResultFailureReasons
|
30
|
+
def reasons
|
31
|
+
@reasons ||= FailureReasons.new
|
27
32
|
end
|
28
33
|
end
|
29
34
|
|
@@ -45,9 +50,21 @@ module ActionPolicy
|
|
45
50
|
# information about the failure:
|
46
51
|
#
|
47
52
|
# rescue_from ActionPolicy::Unauthorized do |ex|
|
48
|
-
# ex.
|
53
|
+
# ex.policy #=> ApplicantPolicy
|
54
|
+
# ex.rule #=> :show?
|
55
|
+
# ex.result.reasons.details #=> { stage: [:show?] }
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# NOTE: the reason key (`stage`) is a policy identifier (underscored class name by default).
|
59
|
+
# For namespaced policies it has a form of:
|
60
|
+
#
|
61
|
+
# class Admin::UserPolicy < ApplicationPolicy
|
62
|
+
# # ..
|
49
63
|
# end
|
50
64
|
#
|
65
|
+
# reasons.details #=> { :"admin/user" => [:show?] }
|
66
|
+
#
|
67
|
+
#
|
51
68
|
# You can also wrap _local_ rules into `allowed_to?` to populate reasons:
|
52
69
|
#
|
53
70
|
# class ApplicantPolicy < ApplicationPolicy
|
@@ -62,48 +79,29 @@ module ActionPolicy
|
|
62
79
|
# end
|
63
80
|
module Reasons
|
64
81
|
class << self
|
65
|
-
def
|
66
|
-
base.
|
82
|
+
def included(base)
|
83
|
+
base.result_class.include(ResultFailureReasons)
|
67
84
|
end
|
68
|
-
|
69
|
-
alias included prepended
|
70
85
|
end
|
71
86
|
|
72
|
-
|
87
|
+
# rubocop: disable Metrics/MethodLength
|
88
|
+
def allowed_to?(rule, record = :__undef__, **options)
|
89
|
+
policy = nil
|
73
90
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
91
|
+
succeed =
|
92
|
+
if record == :__undef__
|
93
|
+
policy = self
|
94
|
+
with_clean_result { apply(rule) }
|
95
|
+
else
|
96
|
+
policy = policy_for(record: record, **options)
|
81
97
|
|
82
|
-
|
83
|
-
|
84
|
-
@reasons = FailureReasons.new
|
85
|
-
super
|
86
|
-
end
|
87
|
-
|
88
|
-
# rubocop: disable Metrics/MethodLength
|
89
|
-
def allowed_to?(rule, record = :__undef__, **options)
|
90
|
-
policy = nil
|
91
|
-
|
92
|
-
succeed =
|
93
|
-
if record == :__undef__
|
94
|
-
policy = self
|
95
|
-
with_clean_reasons { apply(rule) }
|
96
|
-
else
|
97
|
-
policy = policy_for(record: record, **options)
|
98
|
+
policy.apply(rule)
|
99
|
+
end
|
98
100
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
reasons.add(policy, rule) if reasons && !succeed
|
103
|
-
succeed
|
104
|
-
end
|
105
|
-
# rubocop: enable Metrics/MethodLength
|
101
|
+
result.reasons.add(policy, rule) unless succeed
|
102
|
+
succeed
|
106
103
|
end
|
104
|
+
# rubocop: enable Metrics/MethodLength
|
107
105
|
end
|
108
106
|
end
|
109
107
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_policy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-06-
|
11
|
+
date: 2018-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -163,6 +163,8 @@ files:
|
|
163
163
|
- lib/action_policy/ext/policy_cache_key.rb
|
164
164
|
- lib/action_policy/ext/string_constantize.rb
|
165
165
|
- lib/action_policy/ext/string_match.rb
|
166
|
+
- lib/action_policy/ext/string_underscore.rb
|
167
|
+
- lib/action_policy/ext/yield_self_then.rb
|
166
168
|
- lib/action_policy/lookup_chain.rb
|
167
169
|
- lib/action_policy/policy/aliases.rb
|
168
170
|
- lib/action_policy/policy/authorization.rb
|
@@ -170,6 +172,7 @@ files:
|
|
170
172
|
- lib/action_policy/policy/cached_apply.rb
|
171
173
|
- lib/action_policy/policy/core.rb
|
172
174
|
- lib/action_policy/policy/defaults.rb
|
175
|
+
- lib/action_policy/policy/execution_result.rb
|
173
176
|
- lib/action_policy/policy/pre_check.rb
|
174
177
|
- lib/action_policy/policy/reasons.rb
|
175
178
|
- lib/action_policy/rails/channel.rb
|