action_policy 0.5.0 → 0.5.5
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 +13 -2
- data/config/rubocop-rspec.yml +17 -0
- data/lib/.rbnext/1995.next/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/1995.next/action_policy/behaviours/scoping.rb +35 -0
- data/lib/.rbnext/1995.next/action_policy/policy/authorization.rb +87 -0
- data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
- data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
- data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
- data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
- data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
- data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
- data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
- data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +212 -0
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
- data/lib/action_policy/behaviour.rb +2 -2
- data/lib/action_policy/behaviours/memoized.rb +1 -1
- data/lib/action_policy/behaviours/namespaced.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/action_policy/behaviours/scoping.rb +2 -2
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
- data/lib/action_policy/lookup_chain.rb +11 -27
- data/lib/action_policy/policy/aliases.rb +2 -2
- data/lib/action_policy/policy/authorization.rb +2 -2
- data/lib/action_policy/policy/cache.rb +2 -2
- data/lib/action_policy/policy/pre_check.rb +2 -2
- data/lib/action_policy/policy/reasons.rb +4 -2
- data/lib/action_policy/policy/scoping.rb +2 -2
- data/lib/action_policy/test_helper.rb +1 -0
- data/lib/action_policy/version.rb +1 -1
- metadata +29 -4
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
old_verbose = $VERBOSE
|
4
|
+
|
5
|
+
begin
|
6
|
+
require "method_source"
|
7
|
+
# Ignore parse warnings when patch
|
8
|
+
# Ruby version mismatches
|
9
|
+
$VERBOSE = nil
|
10
|
+
require "parser/current"
|
11
|
+
require "unparser"
|
12
|
+
rescue LoadError
|
13
|
+
# do nothing
|
14
|
+
ensure
|
15
|
+
$VERBOSE = old_verbose
|
16
|
+
end
|
17
|
+
|
18
|
+
module ActionPolicy
|
19
|
+
using RubyNext
|
20
|
+
|
21
|
+
# Takes the object and a method name,
|
22
|
+
# and returns the "annotated" source code for the method:
|
23
|
+
# code is split into parts by logical operators and each
|
24
|
+
# part is evaluated separately.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# class MyClass
|
29
|
+
# def access?
|
30
|
+
# admin? && access_feed?
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# puts PrettyPrint.format_method(MyClass.new, :access?)
|
35
|
+
#
|
36
|
+
# #=> MyClass#access?
|
37
|
+
# #=> ↳ admin? #=> false
|
38
|
+
# #=> AND
|
39
|
+
# #=> access_feed? #=> true
|
40
|
+
module PrettyPrint
|
41
|
+
TRUE = "\e[32mtrue\e[0m"
|
42
|
+
FALSE = "\e[31mfalse\e[0m"
|
43
|
+
|
44
|
+
class Visitor
|
45
|
+
attr_reader :lines, :object
|
46
|
+
attr_accessor :indent
|
47
|
+
|
48
|
+
def initialize(object)
|
49
|
+
@object = object
|
50
|
+
end
|
51
|
+
|
52
|
+
def collect(ast)
|
53
|
+
@lines = []
|
54
|
+
@indent = 0
|
55
|
+
|
56
|
+
visit_node(ast)
|
57
|
+
|
58
|
+
lines.join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_node(ast)
|
62
|
+
if respond_to?("visit_#{ast.type}")
|
63
|
+
send("visit_#{ast.type}", ast)
|
64
|
+
else
|
65
|
+
visit_missing ast
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def expression_with_result(sexp)
|
70
|
+
expression = Unparser.unparse(sexp)
|
71
|
+
"#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def eval_exp(exp)
|
75
|
+
return "<skipped>" if ignore_exp?(exp)
|
76
|
+
object.instance_eval(exp)
|
77
|
+
rescue => e
|
78
|
+
"Failed: #{e.message}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def visit_and(ast)
|
82
|
+
visit_node(ast.children[0])
|
83
|
+
lines << indented("AND")
|
84
|
+
visit_node(ast.children[1])
|
85
|
+
end
|
86
|
+
|
87
|
+
def visit_or(ast)
|
88
|
+
visit_node(ast.children[0])
|
89
|
+
lines << indented("OR")
|
90
|
+
visit_node(ast.children[1])
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit_begin(ast)
|
94
|
+
# Parens
|
95
|
+
if ast.children.size == 1
|
96
|
+
lines << indented("(")
|
97
|
+
self.indent += 2
|
98
|
+
visit_node(ast.children[0])
|
99
|
+
self.indent -= 2
|
100
|
+
lines << indented(")")
|
101
|
+
else
|
102
|
+
# Multiple expressions
|
103
|
+
ast.children.each do |node|
|
104
|
+
visit_node(node)
|
105
|
+
# restore indent after each expression
|
106
|
+
self.indent -= 2
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_missing(ast)
|
112
|
+
lines << indented(expression_with_result(ast))
|
113
|
+
end
|
114
|
+
|
115
|
+
def indented(str)
|
116
|
+
"#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
|
117
|
+
# increase indent after the first expression
|
118
|
+
self.indent += 2 if indent.zero?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Some lines should not be evaled
|
123
|
+
def ignore_exp?(exp)
|
124
|
+
PrettyPrint.ignore_expressions.any? { |_1| exp.match?(_1) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
attr_accessor :ignore_expressions
|
130
|
+
|
131
|
+
if defined?(::Unparser) && defined?(::MethodSource)
|
132
|
+
def available?() ; true; end
|
133
|
+
|
134
|
+
def print_method(object, method_name)
|
135
|
+
ast = object.method(method_name).source.then(&Unparser.method(:parse))
|
136
|
+
# outer node is a method definition itself
|
137
|
+
body = ast.children[2]
|
138
|
+
|
139
|
+
Visitor.new(object).collect(body)
|
140
|
+
end
|
141
|
+
else
|
142
|
+
def available?() ; false; end
|
143
|
+
|
144
|
+
def print_method(_, _) ; ""; end
|
145
|
+
end
|
146
|
+
|
147
|
+
def colorize(val)
|
148
|
+
return val unless $stdout.isatty
|
149
|
+
return TRUE if val.eql?(true)
|
150
|
+
return FALSE if val.eql?(false)
|
151
|
+
val
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
self.ignore_expressions = [
|
156
|
+
/^\s*binding\.(pry|irb)\s*$/s
|
157
|
+
]
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module PerThreadCache # :nodoc:
|
5
|
+
CACHE_KEY = "action_policy.per_thread_cache"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_writer :enabled
|
9
|
+
|
10
|
+
def enabled?() ; @enabled == true; end
|
11
|
+
|
12
|
+
def fetch(key)
|
13
|
+
return yield unless enabled?
|
14
|
+
|
15
|
+
store = (Thread.current[CACHE_KEY] ||= {})
|
16
|
+
|
17
|
+
return store[key] if store.key?(key)
|
18
|
+
|
19
|
+
store[key] = yield
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear_all
|
23
|
+
Thread.current[CACHE_KEY] = {}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Turn off by default in test env
|
28
|
+
self.enabled = !(ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test")
|
29
|
+
end
|
30
|
+
|
31
|
+
module Behaviours
|
32
|
+
# Per-thread memoization for policies.
|
33
|
+
#
|
34
|
+
# Used by `policy_for` to re-use policy object for records.
|
35
|
+
#
|
36
|
+
# NOTE: don't forget to clear thread cache with ActionPolicy::PerThreadCache.clear_all
|
37
|
+
module ThreadMemoized
|
38
|
+
class << self
|
39
|
+
def prepended(base)
|
40
|
+
base.prepend InstanceMethods
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method :included, :prepended
|
44
|
+
end
|
45
|
+
|
46
|
+
module InstanceMethods # :nodoc:
|
47
|
+
def policy_for(record:, **opts)
|
48
|
+
__policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def __policy_thread_memoize__(record, **options)
|
53
|
+
cache_key = policy_for_cache_key(record: record, **options)
|
54
|
+
|
55
|
+
ActionPolicy::PerThreadCache.fetch(cache_key) { yield }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext
|
5
|
+
# Adds #_policy_cache_key method to Object,
|
6
|
+
# which just call #policy_cache_key or #cache_key
|
7
|
+
# or #object_id (if `use_object_id` parameter is set to true).
|
8
|
+
#
|
9
|
+
# For other core classes returns string representation.
|
10
|
+
#
|
11
|
+
# Raises ArgumentError otherwise.
|
12
|
+
module PolicyCacheKey # :nodoc: all
|
13
|
+
module ObjectExt
|
14
|
+
def _policy_cache_key(use_object_id: false)
|
15
|
+
return policy_cache_key if respond_to?(:policy_cache_key)
|
16
|
+
return cache_key_with_version if respond_to?(:cache_key_with_version)
|
17
|
+
return cache_key if respond_to?(:cache_key)
|
18
|
+
|
19
|
+
return object_id.to_s if use_object_id == true
|
20
|
+
|
21
|
+
raise ArgumentError, "object is not cacheable"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
refine Object do
|
26
|
+
include ObjectExt
|
27
|
+
end
|
28
|
+
|
29
|
+
refine NilClass do
|
30
|
+
def _policy_cache_key(*) ; ""; end
|
31
|
+
end
|
32
|
+
|
33
|
+
refine TrueClass do
|
34
|
+
def _policy_cache_key(*) ; "t"; end
|
35
|
+
end
|
36
|
+
|
37
|
+
refine FalseClass do
|
38
|
+
def _policy_cache_key(*) ; "f"; end
|
39
|
+
end
|
40
|
+
|
41
|
+
refine String do
|
42
|
+
def _policy_cache_key(*) ; self; end
|
43
|
+
end
|
44
|
+
|
45
|
+
refine Symbol do
|
46
|
+
def _policy_cache_key(*) ; to_s; end
|
47
|
+
end
|
48
|
+
|
49
|
+
if RUBY_PLATFORM.match?(/java/i)
|
50
|
+
refine Integer do
|
51
|
+
def _policy_cache_key(*) ; to_s; end
|
52
|
+
end
|
53
|
+
|
54
|
+
refine Float do
|
55
|
+
def _policy_cache_key(*) ; to_s; end
|
56
|
+
end
|
57
|
+
else
|
58
|
+
refine Numeric do
|
59
|
+
def _policy_cache_key(*) ; to_s; end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
refine Time do
|
64
|
+
def _policy_cache_key(*) ; to_s; end
|
65
|
+
end
|
66
|
+
|
67
|
+
refine Module do
|
68
|
+
def _policy_cache_key(*) ; name; end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Adds rules aliases support and ability to specify
|
6
|
+
# the default rule.
|
7
|
+
#
|
8
|
+
# class ApplicationPolicy
|
9
|
+
# include ActionPolicy::Policy::Core
|
10
|
+
# include ActionPolicy::Policy::Aliases
|
11
|
+
#
|
12
|
+
# # define which rule to use if `authorize!` called with
|
13
|
+
# # unknown rule
|
14
|
+
# default_rule :manage?
|
15
|
+
#
|
16
|
+
# alias_rule :publish?, :unpublish?, to: :update?
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Aliases are used only during `authorize!` call (and do not act like _real_ aliases).
|
20
|
+
#
|
21
|
+
# Aliases useful when combined with `CachedApply` (since we can cache only the target rule).
|
22
|
+
module Aliases
|
23
|
+
DEFAULT = :__default__
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def included(base)
|
27
|
+
base.extend ClassMethods
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def resolve_rule(activity)
|
32
|
+
self.class.lookup_alias(activity) ||
|
33
|
+
(activity if respond_to?(activity)) ||
|
34
|
+
self.class.lookup_default_rule ||
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods # :nodoc:
|
39
|
+
def default_rule(val)
|
40
|
+
rules_aliases[DEFAULT] = val
|
41
|
+
end
|
42
|
+
|
43
|
+
def alias_rule(*rules, to:)
|
44
|
+
rules.each do |rule|
|
45
|
+
rules_aliases[rule] = to
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def lookup_alias(rule) ; rules_aliases[rule]; end
|
50
|
+
|
51
|
+
def lookup_default_rule() ; rules_aliases[DEFAULT]; end
|
52
|
+
|
53
|
+
def rules_aliases
|
54
|
+
return @rules_aliases if instance_variable_defined?(:@rules_aliases)
|
55
|
+
|
56
|
+
@rules_aliases = if superclass.respond_to?(:rules_aliases)
|
57
|
+
superclass.rules_aliases.dup
|
58
|
+
else
|
59
|
+
{}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def method_added(name)
|
64
|
+
rules_aliases.delete(name) if public_method_defined?(name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
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; end
|
27
|
+
|
28
|
+
def cache_key(*parts)
|
29
|
+
[
|
30
|
+
cache_namespace,
|
31
|
+
*parts
|
32
|
+
].map { _1._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.value
|
57
|
+
end
|
58
|
+
yield
|
59
|
+
store.write(key, result, options)
|
60
|
+
result.value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def apply(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
|