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