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