action_policy 0.2.4 → 0.3.0.beta1
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 +26 -64
- data/.travis.yml +13 -10
- data/CHANGELOG.md +216 -1
- data/Gemfile +7 -0
- data/LICENSE.txt +1 -1
- data/Rakefile +10 -0
- data/action_policy.gemspec +5 -3
- data/benchmarks/namespaced_lookup_cache.rb +18 -22
- data/docs/README.md +3 -3
- data/docs/_sidebar.md +4 -0
- data/docs/aliases.md +9 -5
- data/docs/authorization_context.md +59 -1
- data/docs/behaviour.md +113 -0
- data/docs/caching.md +6 -4
- data/docs/custom_policy.md +1 -2
- data/docs/debugging.md +55 -0
- data/docs/decorators.md +27 -0
- data/docs/i18n.md +41 -2
- data/docs/instrumentation.md +70 -2
- data/docs/lookup_chain.md +5 -4
- data/docs/namespaces.md +1 -1
- data/docs/non_rails.md +2 -3
- data/docs/pundit_migration.md +77 -2
- data/docs/quick_start.md +5 -5
- data/docs/rails.md +5 -2
- data/docs/reasons.md +50 -3
- data/docs/scoping.md +262 -0
- data/docs/testing.md +232 -21
- data/docs/writing_policies.md +1 -1
- data/gemfiles/jruby.gemfile +3 -0
- data/gemfiles/rails42.gemfile +3 -0
- data/gemfiles/rails6.gemfile +8 -0
- data/gemfiles/railsmaster.gemfile +1 -1
- data/lib/action_policy.rb +3 -3
- data/lib/action_policy/authorizer.rb +12 -4
- data/lib/action_policy/base.rb +2 -0
- data/lib/action_policy/behaviour.rb +14 -3
- data/lib/action_policy/behaviours/memoized.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +12 -3
- data/lib/action_policy/behaviours/scoping.rb +32 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
- data/lib/action_policy/ext/hash_transform_keys.rb +19 -0
- data/lib/action_policy/ext/module_namespace.rb +1 -1
- data/lib/action_policy/ext/policy_cache_key.rb +2 -1
- data/lib/action_policy/ext/proc_case_eq.rb +14 -0
- data/lib/action_policy/ext/string_constantize.rb +1 -0
- data/lib/action_policy/ext/symbol_classify.rb +22 -0
- data/lib/action_policy/i18n.rb +56 -0
- data/lib/action_policy/lookup_chain.rb +21 -3
- data/lib/action_policy/policy/cache.rb +10 -6
- data/lib/action_policy/policy/core.rb +31 -19
- data/lib/action_policy/policy/execution_result.rb +12 -0
- data/lib/action_policy/policy/pre_check.rb +2 -6
- data/lib/action_policy/policy/reasons.rb +99 -12
- data/lib/action_policy/policy/scoping.rb +165 -0
- data/lib/action_policy/rails/authorizer.rb +20 -0
- data/lib/action_policy/rails/controller.rb +4 -14
- data/lib/action_policy/rails/ext/active_record.rb +10 -0
- data/lib/action_policy/rails/policy/instrumentation.rb +24 -0
- data/lib/action_policy/rails/scope_matchers/action_controller_params.rb +19 -0
- data/lib/action_policy/rails/scope_matchers/active_record.rb +29 -0
- data/lib/action_policy/railtie.rb +29 -7
- data/lib/action_policy/rspec.rb +1 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +1 -1
- data/lib/action_policy/rspec/dsl.rb +103 -0
- data/lib/action_policy/rspec/have_authorized_scope.rb +126 -0
- data/lib/action_policy/rspec/pundit_syntax.rb +1 -1
- data/lib/action_policy/test_helper.rb +69 -4
- data/lib/action_policy/testing.rb +54 -0
- data/lib/action_policy/utils/pretty_print.rb +137 -0
- data/lib/action_policy/utils/suggest_message.rb +21 -0
- data/lib/action_policy/version.rb +1 -1
- metadata +58 -11
@@ -5,10 +5,10 @@ module ActionPolicy
|
|
5
5
|
# Adds `policy_for` method
|
6
6
|
module PolicyFor
|
7
7
|
# Returns policy instance for the record.
|
8
|
-
def policy_for(record:, with: nil, namespace: nil)
|
8
|
+
def policy_for(record:, with: nil, namespace: nil, context: nil, **options)
|
9
9
|
namespace ||= authorization_namespace
|
10
|
-
policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace)
|
11
|
-
policy_class
|
10
|
+
policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace, **options)
|
11
|
+
policy_class&.new(record, authorization_context.tap { |ctx| ctx.merge!(context) if context })
|
12
12
|
end
|
13
13
|
|
14
14
|
def authorization_context
|
@@ -18,6 +18,15 @@ module ActionPolicy
|
|
18
18
|
def authorization_namespace
|
19
19
|
# override to provide specific authorization namespace
|
20
20
|
end
|
21
|
+
|
22
|
+
# Override this method to provide implicit authorization target
|
23
|
+
# that would be used in case `record` is not specified in
|
24
|
+
# `authorize!` and `allowed_to?` call.
|
25
|
+
#
|
26
|
+
# It is also used to infer a policy for scoping (in `authorized_scope` method).
|
27
|
+
def implicit_authorization_target
|
28
|
+
# no-op
|
29
|
+
end
|
21
30
|
end
|
22
31
|
end
|
23
32
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Behaviours
|
5
|
+
# Adds `authorized_scop` method to behaviour
|
6
|
+
module Scoping
|
7
|
+
# Apply scope to the target of the specified type.
|
8
|
+
#
|
9
|
+
# NOTE: policy lookup consists of the following steps:
|
10
|
+
# - first, check whether `with` option is present
|
11
|
+
# - secondly, try to infer policy class from `target` (non-raising lookup)
|
12
|
+
# - use `implicit_authorization_target` if none of the above works.
|
13
|
+
def authorized_scope(target, type: nil, as: :default, scope_options: nil, **options)
|
14
|
+
policy = policy_for(record: target, allow_nil: true, **options)
|
15
|
+
policy ||= policy_for(record: implicit_authorization_target, **options)
|
16
|
+
|
17
|
+
type ||= authorization_scope_type_for(policy, target)
|
18
|
+
|
19
|
+
Authorizer.scopify(target, policy, type: type, name: as, scope_options: scope_options)
|
20
|
+
end
|
21
|
+
|
22
|
+
# For backward compatibility
|
23
|
+
alias authorized authorized_scope
|
24
|
+
|
25
|
+
# Infer scope type for target if none provided.
|
26
|
+
# Raises an exception if type couldn't be inferred.
|
27
|
+
def authorization_scope_type_for(policy, target)
|
28
|
+
policy.resolve_scope_type(target)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -54,7 +54,7 @@ module ActionPolicy
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
def __policy_thread_memoize__(record, with: nil, namespace: nil)
|
57
|
+
def __policy_thread_memoize__(record, with: nil, namespace: nil, **_opts)
|
58
58
|
record_key = record._policy_cache_key(use_object_id: true)
|
59
59
|
cache_key = "#{namespace}/#{with}/#{record_key}"
|
60
60
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext
|
5
|
+
# Add transform_keys to Hash for older Rubys
|
6
|
+
module HashTransformKeys
|
7
|
+
refine Hash do
|
8
|
+
def transform_keys
|
9
|
+
return enum_for(:transform_keys) { size } unless block_given?
|
10
|
+
result = {}
|
11
|
+
each_key do |key|
|
12
|
+
result[yield(key)] = self[key]
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -23,6 +23,7 @@ module ActionPolicy
|
|
23
23
|
|
24
24
|
# JRuby doesn't support _global_ modules refinements (see https://github.com/jruby/jruby/issues/5220)
|
25
25
|
# Fallback to monkey-patching.
|
26
|
+
# TODO: remove after 9.2.7.0 (See https://github.com/jruby/jruby/pull/5627)
|
26
27
|
::Object.include(ObjectExt) if RUBY_PLATFORM =~ /java/i
|
27
28
|
|
28
29
|
refine Object do
|
@@ -59,7 +60,7 @@ module ActionPolicy
|
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
62
|
-
if RUBY_PLATFORM
|
63
|
+
if RUBY_PLATFORM.match?(/java/i)
|
63
64
|
refine Integer do
|
64
65
|
def _policy_cache_key(*)
|
65
66
|
to_s
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext
|
5
|
+
# Add `classify` to Symbol
|
6
|
+
module SymbolClassify
|
7
|
+
refine Symbol do
|
8
|
+
if "".respond_to?(:classify)
|
9
|
+
def classify
|
10
|
+
to_s.classify
|
11
|
+
end
|
12
|
+
else
|
13
|
+
def classify
|
14
|
+
word = to_s.capitalize
|
15
|
+
word.gsub!(/(?:_)([a-z\d]*)/) { $1.capitalize }
|
16
|
+
word
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module I18n # :nodoc:
|
5
|
+
DEFAULT_UNAUTHORIZED_MESSAGE = "You are not authorized to perform this action"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def full_message(policy_class, rule, details = nil)
|
9
|
+
candidates = candidates_for(policy_class, rule)
|
10
|
+
|
11
|
+
options = {scope: :action_policy}
|
12
|
+
options.merge!(details) unless details.nil?
|
13
|
+
|
14
|
+
::I18n.t(
|
15
|
+
candidates.shift,
|
16
|
+
default: candidates,
|
17
|
+
**options
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def candidates_for(policy_class, rule)
|
24
|
+
policy_hierarchy = policy_class.ancestors.select { |klass| klass.respond_to?(:identifier) }
|
25
|
+
[
|
26
|
+
*policy_hierarchy.map { |klass| :"policy.#{klass.identifier}.#{rule}" },
|
27
|
+
:"policy.#{rule}",
|
28
|
+
:unauthorized,
|
29
|
+
DEFAULT_UNAUTHORIZED_MESSAGE
|
30
|
+
]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
ActionPolicy::Policy::FailureReasons.prepend(Module.new do
|
35
|
+
def full_messages
|
36
|
+
reasons.flat_map do |policy_klass, rules|
|
37
|
+
rules.flat_map do |rule|
|
38
|
+
if rule.is_a?(::Hash)
|
39
|
+
rule.map do |key, details|
|
40
|
+
ActionPolicy::I18n.full_message(policy_klass, key, details)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
ActionPolicy::I18n.full_message(policy_klass, rule)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end)
|
49
|
+
|
50
|
+
ActionPolicy::Policy::ExecutionResult.prepend(Module.new do
|
51
|
+
def message
|
52
|
+
ActionPolicy::I18n.full_message(policy, rule, details)
|
53
|
+
end
|
54
|
+
end)
|
55
|
+
end
|
56
|
+
end
|
@@ -12,6 +12,9 @@ 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
|
17
|
+
|
15
18
|
require "action_policy/ext/module_namespace"
|
16
19
|
using ActionPolicy::Ext::ModuleNamespace
|
17
20
|
|
@@ -50,8 +53,7 @@ module ActionPolicy
|
|
50
53
|
|
51
54
|
private
|
52
55
|
|
53
|
-
def lookup_within_namespace(
|
54
|
-
policy_name = policy_class_name_for(record)
|
56
|
+
def lookup_within_namespace(policy_name, namespace)
|
55
57
|
NamespaceCache.fetch(namespace.name, policy_name) do
|
56
58
|
mod = namespace
|
57
59
|
|
@@ -68,6 +70,8 @@ module ActionPolicy
|
|
68
70
|
end
|
69
71
|
|
70
72
|
def policy_class_name_for(record)
|
73
|
+
return record.policy_name.to_s if record.respond_to?(:policy_name)
|
74
|
+
|
71
75
|
record_class = record.is_a?(Module) ? record : record.class
|
72
76
|
|
73
77
|
if record_class.respond_to?(:policy_name)
|
@@ -97,7 +101,8 @@ module ActionPolicy
|
|
97
101
|
NAMESPACE_LOOKUP = ->(record, namespace: nil, **) {
|
98
102
|
next if namespace.nil?
|
99
103
|
|
100
|
-
|
104
|
+
policy_name = policy_class_name_for(record)
|
105
|
+
lookup_within_namespace(policy_name, namespace)
|
101
106
|
}
|
102
107
|
|
103
108
|
# Infer from class name
|
@@ -105,7 +110,20 @@ module ActionPolicy
|
|
105
110
|
policy_class_name_for(record).safe_constantize
|
106
111
|
}
|
107
112
|
|
113
|
+
# Infer from passed symbol
|
114
|
+
SYMBOL_LOOKUP = ->(record, namespace: nil, **) {
|
115
|
+
next unless record.is_a?(Symbol)
|
116
|
+
|
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
|
123
|
+
}
|
124
|
+
|
108
125
|
self.chain = [
|
126
|
+
SYMBOL_LOOKUP,
|
109
127
|
INSTANCE_POLICY_CLASS,
|
110
128
|
CLASS_POLICY_CLASS,
|
111
129
|
NAMESPACE_LOOKUP,
|
@@ -4,12 +4,15 @@ require "action_policy/version"
|
|
4
4
|
|
5
5
|
module ActionPolicy # :nodoc:
|
6
6
|
# By default cache namespace (or prefix) contains major and minor version of the gem
|
7
|
-
CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split(
|
7
|
+
CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split(".").take(2).join(".")}"
|
8
8
|
|
9
9
|
require "action_policy/ext/yield_self_then"
|
10
10
|
require "action_policy/ext/policy_cache_key"
|
11
11
|
|
12
|
-
|
12
|
+
unless "".respond_to?(:then)
|
13
|
+
require "action_policy/ext/yield_self_then"
|
14
|
+
using ActionPolicy::Ext::YieldSelfThen
|
15
|
+
end
|
13
16
|
using ActionPolicy::Ext::PolicyCacheKey
|
14
17
|
|
15
18
|
module Policy
|
@@ -36,24 +39,25 @@ module ActionPolicy # :nodoc:
|
|
36
39
|
authorization_context.map { |_k, v| v._policy_cache_key.to_s }.join("/")
|
37
40
|
end
|
38
41
|
|
39
|
-
# rubocop: disable Metrics/AbcSize
|
40
42
|
def apply_with_cache(rule)
|
41
43
|
options = self.class.cached_rules.fetch(rule)
|
42
44
|
key = cache_key(rule)
|
43
45
|
|
44
46
|
ActionPolicy.cache_store.then do |store|
|
45
47
|
@result = store.read(key)
|
46
|
-
|
48
|
+
unless result.nil?
|
49
|
+
result.cached!
|
50
|
+
next result.value
|
51
|
+
end
|
47
52
|
yield
|
48
53
|
store.write(key, result, options)
|
49
54
|
result.value
|
50
55
|
end
|
51
56
|
end
|
52
|
-
# rubocop: enable Metrics/AbcSize
|
53
57
|
|
54
58
|
def apply(rule)
|
55
59
|
return super if ActionPolicy.cache_store.nil? ||
|
56
|
-
|
60
|
+
!self.class.cached_rules.key?(rule)
|
57
61
|
|
58
62
|
apply_with_cache(rule) { super }
|
59
63
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require "action_policy/behaviours/policy_for"
|
4
4
|
require "action_policy/policy/execution_result"
|
5
|
+
require "action_policy/utils/suggest_message"
|
6
|
+
require "action_policy/utils/pretty_print"
|
5
7
|
|
6
8
|
unless "".respond_to?(:underscore)
|
7
9
|
require "action_policy/ext/string_underscore"
|
@@ -12,26 +14,16 @@ module ActionPolicy
|
|
12
14
|
# Raised when `resolve_rule` failed to find an approriate
|
13
15
|
# policy rule method for the activity
|
14
16
|
class UnknownRule < Error
|
17
|
+
include ActionPolicy::SuggestMessage
|
18
|
+
|
15
19
|
attr_reader :policy, :rule, :message
|
16
20
|
|
17
21
|
def initialize(policy, rule)
|
18
22
|
@policy = policy.class
|
19
23
|
@rule = rule
|
20
|
-
@message =
|
21
|
-
|
22
|
-
|
23
|
-
if defined?(::DidYouMean::SpellChecker)
|
24
|
-
def suggest(policy, error)
|
25
|
-
suggestion = ::DidYouMean::SpellChecker.new(
|
26
|
-
dictionary: policy.public_methods
|
27
|
-
).correct(error).first
|
28
|
-
|
29
|
-
suggestion ? "\nDid you mean? #{suggestion}" : ""
|
30
|
-
end
|
31
|
-
else
|
32
|
-
def suggest(*)
|
33
|
-
""
|
34
|
-
end
|
24
|
+
@message =
|
25
|
+
"Couldn't find rule '#{@rule}' for #{@policy}" \
|
26
|
+
"#{suggest(@rule, @policy.instance_methods - Object.instance_methods)}"
|
35
27
|
end
|
36
28
|
end
|
37
29
|
|
@@ -73,7 +65,7 @@ module ActionPolicy
|
|
73
65
|
|
74
66
|
attr_reader :record, :result
|
75
67
|
|
76
|
-
def initialize(record = nil)
|
68
|
+
def initialize(record = nil, **_opts)
|
77
69
|
@record = record
|
78
70
|
end
|
79
71
|
|
@@ -94,10 +86,10 @@ module ActionPolicy
|
|
94
86
|
|
95
87
|
# Wrap code that could modify result
|
96
88
|
# to prevent the current result modification
|
97
|
-
def with_clean_result
|
89
|
+
def with_clean_result # :nodoc:
|
98
90
|
was_result = @result
|
99
|
-
|
100
|
-
@result = was_result
|
91
|
+
yield
|
92
|
+
res, @result = @result, was_result
|
101
93
|
res
|
102
94
|
end
|
103
95
|
|
@@ -129,6 +121,26 @@ module ActionPolicy
|
|
129
121
|
respond_to?(activity)
|
130
122
|
activity
|
131
123
|
end
|
124
|
+
|
125
|
+
# Return annotated source code for the rule
|
126
|
+
# NOTE: require "method_source" and "unparser" gems to be installed.
|
127
|
+
# Otherwise returns empty string.
|
128
|
+
def inspect_rule(rule)
|
129
|
+
PrettyPrint.print_method(self, rule)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Helper for printing the annotated rule source.
|
133
|
+
# Useful for debugging: type `pp :show?` within the context of the policy
|
134
|
+
# to preview the rule.
|
135
|
+
def pp(rule)
|
136
|
+
with_clean_result do
|
137
|
+
# We need result to exist for `allowed_to?` to work correctly
|
138
|
+
@result = self.class.result_class.new(self.class, rule)
|
139
|
+
header = "#{self.class.name}##{rule}"
|
140
|
+
source = inspect_rule(rule)
|
141
|
+
$stdout.puts "#{header}\n#{source}"
|
142
|
+
end
|
143
|
+
end
|
132
144
|
end
|
133
145
|
end
|
134
146
|
end
|
@@ -88,8 +88,6 @@ module ActionPolicy
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
-
# rubocop: disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
92
|
-
# rubocop: disable Metrics/PerceivedComplexity, Metrics/MethodLength
|
93
91
|
def skip!(except: nil, only: nil)
|
94
92
|
if !except.nil? && !only.nil?
|
95
93
|
raise ArgumentError,
|
@@ -114,8 +112,8 @@ module ActionPolicy
|
|
114
112
|
|
115
113
|
rebuild_filter
|
116
114
|
end
|
117
|
-
# rubocop: enable
|
118
|
-
# rubocop: enable
|
115
|
+
# rubocop: enable
|
116
|
+
# rubocop: enable
|
119
117
|
|
120
118
|
def dup
|
121
119
|
self.class.new(policy_class, name, except: blacklist&.dup, only: whitelist&.dup)
|
@@ -174,7 +172,6 @@ module ActionPolicy
|
|
174
172
|
end
|
175
173
|
end
|
176
174
|
|
177
|
-
# rubocop: disable Metrics/AbcSize
|
178
175
|
def skip_pre_check(*names, **options)
|
179
176
|
names.each do |name|
|
180
177
|
check = pre_checks.find { |c| c.name == name }
|
@@ -187,7 +184,6 @@ module ActionPolicy
|
|
187
184
|
pre_checks[pre_checks.index(check)] = check.dup.tap { |c| c.skip! options }
|
188
185
|
end
|
189
186
|
end
|
190
|
-
# rubocop: enable Metrics/AbcSize
|
191
187
|
|
192
188
|
def pre_checks
|
193
189
|
return @pre_checks if instance_variable_defined?(:@pre_checks)
|