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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -2
  3. data/config/rubocop-rspec.yml +17 -0
  4. data/lib/.rbnext/1995.next/action_policy/behaviours/policy_for.rb +62 -0
  5. data/lib/.rbnext/1995.next/action_policy/behaviours/scoping.rb +35 -0
  6. data/lib/.rbnext/1995.next/action_policy/policy/authorization.rb +87 -0
  7. data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +159 -0
  8. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
  9. data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
  10. data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
  11. data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
  12. data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
  13. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
  14. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
  15. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
  16. data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
  17. data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
  18. data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
  19. data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
  20. data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
  21. data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
  22. data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
  23. data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +212 -0
  24. data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
  25. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
  26. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
  27. data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
  28. data/lib/action_policy/behaviour.rb +2 -2
  29. data/lib/action_policy/behaviours/memoized.rb +1 -1
  30. data/lib/action_policy/behaviours/namespaced.rb +1 -1
  31. data/lib/action_policy/behaviours/policy_for.rb +1 -1
  32. data/lib/action_policy/behaviours/scoping.rb +2 -2
  33. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  34. data/lib/action_policy/lookup_chain.rb +11 -27
  35. data/lib/action_policy/policy/aliases.rb +2 -2
  36. data/lib/action_policy/policy/authorization.rb +2 -2
  37. data/lib/action_policy/policy/cache.rb +2 -2
  38. data/lib/action_policy/policy/pre_check.rb +2 -2
  39. data/lib/action_policy/policy/reasons.rb +4 -2
  40. data/lib/action_policy/policy/scoping.rb +2 -2
  41. data/lib/action_policy/test_helper.rb +1 -0
  42. data/lib/action_policy/version.rb +1 -1
  43. 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