action_policy 0.7.4 → 0.7.6
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/CHANGELOG.md +14 -3
- data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/cache.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +3 -3
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +4 -4
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +4 -4
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +2 -2
- data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb +4 -4
- data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +4 -4
- data/lib/.rbnext/3.4/action_policy/behaviours/policy_for.rb +70 -0
- data/lib/.rbnext/3.4/action_policy/i18n.rb +56 -0
- data/lib/.rbnext/3.4/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/3.4/action_policy/policy/pre_check.rb +160 -0
- data/lib/.rbnext/3.4/action_policy/rspec/be_authorized_to.rb +96 -0
- data/lib/.rbnext/3.4/action_policy/rspec/have_authorized_scope.rb +130 -0
- data/lib/.rbnext/3.4/action_policy/utils/pretty_print.rb +155 -0
- data/lib/action_policy/behaviour.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/action_policy/i18n.rb +1 -1
- data/lib/action_policy/policy/cache.rb +1 -1
- data/lib/action_policy/policy/execution_result.rb +1 -1
- data/lib/action_policy/policy/pre_check.rb +3 -3
- data/lib/action_policy/rails/controller.rb +45 -4
- data/lib/action_policy/rails/policy/instrumentation.rb +1 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +3 -3
- data/lib/action_policy/rspec/have_authorized_scope.rb +3 -3
- data/lib/action_policy/test_helper.rb +1 -1
- data/lib/action_policy/utils/pretty_print.rb +2 -2
- data/lib/action_policy/version.rb +1 -1
- data/lib/action_policy.rb +1 -1
- data/lib/ruby_lsp/action_policy/addon.rb +170 -0
- metadata +11 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 322dfe78ddec91cb0d7c46b290f69484ba2fdf417c85c687d23e6b521db3e097
|
|
4
|
+
data.tar.gz: b263d5a7000f202b480331e4dd486726563ab83a4828bd57dbaa8c2e8a3b86a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93628bb047eb417395cc9b7f0b15f2f97882d327c32212105d186ef42187fc24e84abd37bdf40968ae5a5988d318d717f0fb3af454f5fb14059a3dc84fe98c08
|
|
7
|
+
data.tar.gz: 9d0387f6aea496ac9d3affdb6cfe89b9fef008d9ff2f96a97a69ce19c0832f8fe01d2023f4497a150325e00e341d6a170b8218e3ce47e06a96d8de28aea38cb5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## master
|
|
4
4
|
|
|
5
|
+
## 0.7.6 (2025-01-13)
|
|
6
|
+
|
|
7
|
+
- Execute proc passed to the `:through` option of `authorize` against `self` ([@Pagehey][])
|
|
8
|
+
|
|
9
|
+
- Add authorization result to the instrumentation payload (as `event[:result]`). ([@palkan][])
|
|
10
|
+
|
|
11
|
+
## 0.7.5 (2025-05-09) 🎇
|
|
12
|
+
|
|
13
|
+
- Ensure `result.value` is true or false. ([@palkan][])
|
|
14
|
+
|
|
5
15
|
## 0.7.4 (2025-03-12)
|
|
6
16
|
|
|
7
|
-
- Let authorize! return the policy record ([@sedubois][])
|
|
17
|
+
- Let authorize! return the policy record. ([@sedubois][])
|
|
8
18
|
|
|
9
|
-
- Enable `allowance_to` as a helper method by default ([@stephannv][])
|
|
19
|
+
- Enable `allowance_to` as a helper method by default. ([@stephannv][])
|
|
10
20
|
|
|
11
|
-
- Allow the `:through` option of `authorize` to be passed a
|
|
21
|
+
- Allow the `:through` option of `authorize` to be passed a Proc. ([@brendon][])
|
|
12
22
|
|
|
13
23
|
## 0.7.3 (2024-12-18)
|
|
14
24
|
|
|
@@ -549,3 +559,4 @@ This value is now stored in a cache (if any) instead of just the call result (`t
|
|
|
549
559
|
[@Spone]: https://github.com/Spone
|
|
550
560
|
[@stephannv]: https://github.com/stephannv
|
|
551
561
|
[@sedubois]: https://github.com/sedubois
|
|
562
|
+
[@Pagehey]: https://github.com/Pagehey
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
|
|
63
63
|
record_key = record._policy_cache_key(use_object_id: true)
|
|
64
|
-
context_key = context.values.map {
|
|
64
|
+
context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
|
|
65
65
|
|
|
66
66
|
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
|
67
67
|
end
|
|
@@ -125,7 +125,7 @@ module ActionPolicy
|
|
|
125
125
|
def pre_check(*names, **options)
|
|
126
126
|
names.each do |name|
|
|
127
127
|
# do not allow pre-check override
|
|
128
|
-
check = pre_checks.find {
|
|
128
|
+
check = pre_checks.find { |it| it.name == name }
|
|
129
129
|
raise "Pre-check already defined: #{name}" unless check.nil?
|
|
130
130
|
|
|
131
131
|
pre_checks << Check.new(self, name, **options)
|
|
@@ -134,14 +134,14 @@ module ActionPolicy
|
|
|
134
134
|
|
|
135
135
|
def skip_pre_check(*names, **options)
|
|
136
136
|
names.each do |name|
|
|
137
|
-
check = pre_checks.find {
|
|
137
|
+
check = pre_checks.find { |it| it.name == name }
|
|
138
138
|
raise "Pre-check not found: #{name}" if check.nil?
|
|
139
139
|
|
|
140
140
|
# when no options provided we remove this check completely
|
|
141
141
|
next pre_checks.delete(check) if options.empty?
|
|
142
142
|
|
|
143
143
|
# otherwise duplicate and apply skip options
|
|
144
|
-
pre_checks[pre_checks.index(check)] = check.dup.tap {
|
|
144
|
+
pre_checks[pre_checks.index(check)] = check.dup.tap { |it| it.skip!(**options) }
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
@@ -51,7 +51,7 @@ module ActionPolicy
|
|
|
51
51
|
|
|
52
52
|
@actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
|
|
53
53
|
|
|
54
|
-
actual_calls.any? {
|
|
54
|
+
actual_calls.any? { |it| it.matches?(policy, rule, target, context) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def does_not_match?(*__rest__)
|
|
@@ -63,7 +63,7 @@ module ActionPolicy
|
|
|
63
63
|
def failure_message
|
|
64
64
|
"expected #{formatted_record} " \
|
|
65
65
|
"to be authorized with #{policy}##{rule}, " \
|
|
66
|
-
"#{
|
|
66
|
+
"#{"and context #{context.inspect}, " if context}" \
|
|
67
67
|
"but #{actual_calls_message}"
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -77,8 +77,8 @@ module ActionPolicy
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def formatted_calls
|
|
80
|
-
actual_calls.map do
|
|
81
|
-
" - #{
|
|
80
|
+
actual_calls.map do |it|
|
|
81
|
+
" - #{it.inspect}"
|
|
82
82
|
end.join("\n")
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
|
63
63
|
|
|
64
|
-
matching_scopes = actual_scopes.select {
|
|
64
|
+
matching_scopes = actual_scopes.select { |it| it.matches?(policy, type, name, scope_options, context) }
|
|
65
65
|
|
|
66
66
|
return false if matching_scopes.empty?
|
|
67
67
|
|
|
@@ -85,7 +85,7 @@ module ActionPolicy
|
|
|
85
85
|
def failure_message
|
|
86
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
|
87
87
|
"#{scope_options_message} " \
|
|
88
|
-
"#{
|
|
88
|
+
"#{"and context #{context.inspect} " if context}" \
|
|
89
89
|
"from #{policy} to have been applied, " \
|
|
90
90
|
"but #{actual_scopes_message}"
|
|
91
91
|
end
|
|
@@ -113,8 +113,8 @@ module ActionPolicy
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def formatted_scopings
|
|
116
|
-
actual_scopes.map do
|
|
117
|
-
" - #{
|
|
116
|
+
actual_scopes.map do |it|
|
|
117
|
+
" - #{it.inspect}"
|
|
118
118
|
end.join("\n")
|
|
119
119
|
end
|
|
120
120
|
end
|
|
@@ -111,7 +111,7 @@ module ActionPolicy
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def indented(str)
|
|
114
|
-
"#{
|
|
114
|
+
"#{"↳ " if indent.zero?}#{" " * indent}#{str}".tap do
|
|
115
115
|
# increase indent after the first expression
|
|
116
116
|
self.indent += 2 if indent.zero?
|
|
117
117
|
end
|
|
@@ -119,7 +119,7 @@ module ActionPolicy
|
|
|
119
119
|
|
|
120
120
|
# Some lines should not be evaled
|
|
121
121
|
def ignore_exp?(exp)
|
|
122
|
-
PrettyPrint.ignore_expressions.any? { exp.match?(
|
|
122
|
+
PrettyPrint.ignore_expressions.any? { |it| exp.match?(it) }
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
|
|
63
63
|
record_key = record._policy_cache_key(use_object_id: true)
|
|
64
|
-
context_key = context.values.map {
|
|
64
|
+
context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
|
|
65
65
|
|
|
66
66
|
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
|
67
67
|
end
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
|
|
63
63
|
record_key = record._policy_cache_key(use_object_id: true)
|
|
64
|
-
context_key = context.values.map {
|
|
64
|
+
context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
|
|
65
65
|
|
|
66
66
|
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
|
67
67
|
end
|
|
@@ -51,7 +51,7 @@ module ActionPolicy
|
|
|
51
51
|
|
|
52
52
|
@actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
|
|
53
53
|
|
|
54
|
-
actual_calls.any? {
|
|
54
|
+
actual_calls.any? { |it| it.matches?(policy, rule, target, context) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def does_not_match?(*__rest__)
|
|
@@ -63,7 +63,7 @@ module ActionPolicy
|
|
|
63
63
|
def failure_message
|
|
64
64
|
"expected #{formatted_record} " \
|
|
65
65
|
"to be authorized with #{policy}##{rule}, " \
|
|
66
|
-
"#{
|
|
66
|
+
"#{"and context #{context.inspect}, " if context}" \
|
|
67
67
|
"but #{actual_calls_message}"
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -77,8 +77,8 @@ module ActionPolicy
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def formatted_calls
|
|
80
|
-
actual_calls.map do
|
|
81
|
-
" - #{
|
|
80
|
+
actual_calls.map do |it|
|
|
81
|
+
" - #{it.inspect}"
|
|
82
82
|
end.join("\n")
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
|
63
63
|
|
|
64
|
-
matching_scopes = actual_scopes.select {
|
|
64
|
+
matching_scopes = actual_scopes.select { |it| it.matches?(policy, type, name, scope_options, context) }
|
|
65
65
|
|
|
66
66
|
return false if matching_scopes.empty?
|
|
67
67
|
|
|
@@ -85,7 +85,7 @@ module ActionPolicy
|
|
|
85
85
|
def failure_message
|
|
86
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
|
87
87
|
"#{scope_options_message} " \
|
|
88
|
-
"#{
|
|
88
|
+
"#{"and context #{context.inspect} " if context}" \
|
|
89
89
|
"from #{policy} to have been applied, " \
|
|
90
90
|
"but #{actual_scopes_message}"
|
|
91
91
|
end
|
|
@@ -113,8 +113,8 @@ module ActionPolicy
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def formatted_scopings
|
|
116
|
-
actual_scopes.map do
|
|
117
|
-
" - #{
|
|
116
|
+
actual_scopes.map do |it|
|
|
117
|
+
" - #{it.inspect}"
|
|
118
118
|
end.join("\n")
|
|
119
119
|
end
|
|
120
120
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPolicy
|
|
4
|
+
module Behaviours
|
|
5
|
+
# Adds `policy_for` method
|
|
6
|
+
module PolicyFor
|
|
7
|
+
require "action_policy/ext/policy_cache_key"
|
|
8
|
+
using ActionPolicy::Ext::PolicyCacheKey
|
|
9
|
+
|
|
10
|
+
# Returns policy instance for the record.
|
|
11
|
+
def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
|
|
12
|
+
context = context ? build_authorization_context.merge(context) : authorization_context
|
|
13
|
+
|
|
14
|
+
policy_class = with || ::ActionPolicy.lookup(
|
|
15
|
+
record,
|
|
16
|
+
namespace:, context:, allow_nil:, default:, strict_namespace:
|
|
17
|
+
)
|
|
18
|
+
policy_class&.new(record, **context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def authorization_context = @authorization_context ||= build_authorization_context
|
|
22
|
+
|
|
23
|
+
def build_authorization_context
|
|
24
|
+
Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def authorization_namespace
|
|
28
|
+
# override to provide specific authorization namespace
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_authorization_policy_class
|
|
32
|
+
# override to provide a policy class use when no policy found
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def authorization_strict_namespace
|
|
36
|
+
# override to provide strict namespace lookup option
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Override this method to provide implicit authorization target
|
|
40
|
+
# that would be used in case `record` is not specified in
|
|
41
|
+
# `authorize!` and `allowed_to?` call.
|
|
42
|
+
#
|
|
43
|
+
# It is also used to infer a policy for scoping (in `authorized_scope` method).
|
|
44
|
+
def implicit_authorization_target
|
|
45
|
+
# no-op
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return implicit authorization target or raises an exception if it's nil
|
|
49
|
+
def implicit_authorization_target!
|
|
50
|
+
implicit_authorization_target || Kernel.raise(
|
|
51
|
+
NotFound,
|
|
52
|
+
[
|
|
53
|
+
self,
|
|
54
|
+
"Couldn't find implicit authorization target " \
|
|
55
|
+
"for #{self.class}. " \
|
|
56
|
+
"Please, provide policy class explicitly using `with` option or " \
|
|
57
|
+
"define the `implicit_authorization_target` method."
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
|
|
63
|
+
record_key = record._policy_cache_key(use_object_id: true)
|
|
64
|
+
context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
|
|
65
|
+
|
|
66
|
+
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
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 { |it| it.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
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_policy/version"
|
|
4
|
+
|
|
5
|
+
module ActionPolicy # :nodoc:
|
|
6
|
+
using RubyNext
|
|
7
|
+
|
|
8
|
+
# By default cache namespace (or prefix) contains major and minor version of the gem
|
|
9
|
+
CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split(".").take(2).join(".")}"
|
|
10
|
+
|
|
11
|
+
require "action_policy/ext/policy_cache_key"
|
|
12
|
+
|
|
13
|
+
using ActionPolicy::Ext::PolicyCacheKey
|
|
14
|
+
|
|
15
|
+
module Policy
|
|
16
|
+
# Provides long-lived cache through ActionPolicy.cache_store.
|
|
17
|
+
#
|
|
18
|
+
# NOTE: if cache_store is nil then we silently skip all the caching.
|
|
19
|
+
module Cache
|
|
20
|
+
class << self
|
|
21
|
+
def included(base)
|
|
22
|
+
base.extend ClassMethods
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cache_namespace() = ActionPolicy::CACHE_NAMESPACE
|
|
27
|
+
|
|
28
|
+
def cache_key(*parts)
|
|
29
|
+
[
|
|
30
|
+
cache_namespace,
|
|
31
|
+
*parts
|
|
32
|
+
].map { |it| it._policy_cache_key }.join("/")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def rule_cache_key(rule)
|
|
36
|
+
cache_key(
|
|
37
|
+
context_cache_key,
|
|
38
|
+
record,
|
|
39
|
+
self.class,
|
|
40
|
+
rule
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def context_cache_key
|
|
45
|
+
authorization_context.map { _2._policy_cache_key.to_s }.join("/")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def apply_with_cache(rule)
|
|
49
|
+
options = self.class.cached_rules.fetch(rule)
|
|
50
|
+
key = rule_cache_key(rule)
|
|
51
|
+
|
|
52
|
+
ActionPolicy.cache_store.then do |store|
|
|
53
|
+
result = store.read(key)
|
|
54
|
+
unless result.nil?
|
|
55
|
+
result.cached!
|
|
56
|
+
next result
|
|
57
|
+
end
|
|
58
|
+
yield.tap do |result|
|
|
59
|
+
store.write(key, result, options)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def apply_r(rule)
|
|
65
|
+
return super if ActionPolicy.cache_store.nil? ||
|
|
66
|
+
!self.class.cached_rules.key?(rule)
|
|
67
|
+
|
|
68
|
+
apply_with_cache(rule) { super }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cache(*parts, **options)
|
|
72
|
+
key = cache_key(*parts)
|
|
73
|
+
ActionPolicy.cache_store.then do |store|
|
|
74
|
+
res = store.read(key)
|
|
75
|
+
next res unless res.nil?
|
|
76
|
+
res = yield
|
|
77
|
+
store.write(key, res, options)
|
|
78
|
+
res
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
module ClassMethods # :nodoc:
|
|
83
|
+
def cache(*rules, **options)
|
|
84
|
+
rules.each do |rule|
|
|
85
|
+
cached_rules[rule] = options
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def cached_rules
|
|
90
|
+
return @cached_rules if instance_variable_defined?(:@cached_rules)
|
|
91
|
+
|
|
92
|
+
@cached_rules = if superclass.respond_to?(:cached_rules)
|
|
93
|
+
superclass.cached_rules.dup
|
|
94
|
+
else
|
|
95
|
+
{}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPolicy
|
|
4
|
+
module Policy
|
|
5
|
+
# Adds callback-style checks to policies to
|
|
6
|
+
# extract common checks from rules.
|
|
7
|
+
#
|
|
8
|
+
# class ApplicationPolicy < ActionPolicy::Base
|
|
9
|
+
# authorize :user
|
|
10
|
+
# pre_check :allow_admins
|
|
11
|
+
#
|
|
12
|
+
# private
|
|
13
|
+
# # Allow every action for admins
|
|
14
|
+
# def allow_admins
|
|
15
|
+
# allow! if user.admin?
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# You can specify conditional pre-checks (through `except` / `only`) options
|
|
20
|
+
# and skip already defined pre-checks if necessary.
|
|
21
|
+
#
|
|
22
|
+
# class UserPolicy < ApplicationPolicy
|
|
23
|
+
# skip_pre_check :allow_admins, only: :destroy?
|
|
24
|
+
#
|
|
25
|
+
# def destroy?
|
|
26
|
+
# user.admin? && !record.admin?
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
module PreCheck
|
|
30
|
+
# Single pre-check instance.
|
|
31
|
+
#
|
|
32
|
+
# Implements filtering logic.
|
|
33
|
+
class Check
|
|
34
|
+
attr_reader :name, :policy_class
|
|
35
|
+
|
|
36
|
+
def initialize(policy, name, except: nil, only: nil)
|
|
37
|
+
if !except.nil? && !only.nil?
|
|
38
|
+
raise ArgumentError,
|
|
39
|
+
"Only one of `except` and `only` may be specified for pre-check"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@policy_class = policy
|
|
43
|
+
@name = name
|
|
44
|
+
@blacklist = Array(except) unless except.nil?
|
|
45
|
+
@whitelist = Array(only) unless only.nil?
|
|
46
|
+
|
|
47
|
+
rebuild_filter
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def applicable?(rule)
|
|
51
|
+
return true if filter.nil?
|
|
52
|
+
filter.call(rule)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call(policy) = policy.send(name)
|
|
56
|
+
|
|
57
|
+
def skip!(except: nil, only: nil)
|
|
58
|
+
if !except.nil? && !only.nil?
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
"Only one of `except` and `only` may be specified when skipping pre-check"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if except.nil? && only.nil?
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"At least one of `except` and `only` must be specified when skipping pre-check"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if except
|
|
69
|
+
@whitelist = Array(except)
|
|
70
|
+
@whitelist -= blacklist if blacklist
|
|
71
|
+
@blacklist = nil
|
|
72
|
+
else
|
|
73
|
+
# only
|
|
74
|
+
@blacklist += Array(only) if blacklist
|
|
75
|
+
@whitelist -= Array(only) if whitelist
|
|
76
|
+
@blacklist = Array(only) if filter.nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
rebuild_filter
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def dup
|
|
83
|
+
self.class.new(
|
|
84
|
+
policy_class,
|
|
85
|
+
name,
|
|
86
|
+
except: blacklist&.dup,
|
|
87
|
+
only: whitelist&.dup
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
attr_reader :whitelist, :blacklist, :filter
|
|
94
|
+
|
|
95
|
+
def rebuild_filter
|
|
96
|
+
@filter =
|
|
97
|
+
if whitelist
|
|
98
|
+
proc { |rule| whitelist.include?(rule) }
|
|
99
|
+
elsif blacklist
|
|
100
|
+
proc { |rule| !blacklist.include?(rule) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
def included(base)
|
|
107
|
+
base.extend ClassMethods
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def run_pre_checks(rule)
|
|
112
|
+
self.class.pre_checks.each do |check|
|
|
113
|
+
next unless check.applicable?(rule)
|
|
114
|
+
check.call(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
yield if block_given?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def __apply__(rule)
|
|
121
|
+
run_pre_checks(rule) { super }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
module ClassMethods # :nodoc:
|
|
125
|
+
def pre_check(*names, **options)
|
|
126
|
+
names.each do |name|
|
|
127
|
+
# do not allow pre-check override
|
|
128
|
+
check = pre_checks.find { |it| it.name == name }
|
|
129
|
+
raise "Pre-check already defined: #{name}" unless check.nil?
|
|
130
|
+
|
|
131
|
+
pre_checks << Check.new(self, name, **options)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def skip_pre_check(*names, **options)
|
|
136
|
+
names.each do |name|
|
|
137
|
+
check = pre_checks.find { |it| it.name == name }
|
|
138
|
+
raise "Pre-check not found: #{name}" if check.nil?
|
|
139
|
+
|
|
140
|
+
# when no options provided we remove this check completely
|
|
141
|
+
next pre_checks.delete(check) if options.empty?
|
|
142
|
+
|
|
143
|
+
# otherwise duplicate and apply skip options
|
|
144
|
+
pre_checks[pre_checks.index(check)] = check.dup.tap { |it| it.skip!(**options) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def pre_checks
|
|
149
|
+
return @pre_checks if instance_variable_defined?(:@pre_checks)
|
|
150
|
+
|
|
151
|
+
@pre_checks = if superclass.respond_to?(:pre_checks)
|
|
152
|
+
superclass.pre_checks.dup
|
|
153
|
+
else
|
|
154
|
+
[]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|