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