action_policy 0.6.8 → 0.7.0
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 +11 -0
- data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +5 -3
- data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +8 -2
- data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +25 -29
- data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +70 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/core.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +8 -2
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +25 -29
- data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +5 -3
- data/lib/.rbnext/3.1/action_policy/policy/authorization.rb +1 -0
- data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +5 -3
- data/lib/.rbnext/3.2/action_policy/policy/core.rb +1 -1
- data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +8 -2
- data/lib/action_policy/behaviours/memoized.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +5 -3
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
- data/lib/action_policy/policy/authorization.rb +1 -0
- data/lib/action_policy/policy/core.rb +1 -1
- data/lib/action_policy/policy/scoping.rb +12 -1
- data/lib/action_policy/rails/controller.rb +1 -1
- data/lib/action_policy/rspec/have_authorized_scope.rb +8 -2
- data/lib/action_policy/test_helper.rb +5 -2
- data/lib/action_policy/testing.rb +17 -10
- data/lib/action_policy/utils/pretty_print.rb +25 -29
- data/lib/action_policy/version.rb +1 -1
- metadata +8 -8
- data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +0 -159
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e9a1595099a6b87f22ec08fbd6676cd73cea94672c8cc9276525e82e9ba8c1a
|
4
|
+
data.tar.gz: bc743bbc45679d6583d3549cad77fc37f4004d051ef54d4cf8baa252730b0621
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf8db058edbeb5b198fb58f8ccab0d35e1f71aeb7891a6bab93bf6e04b9819fc321706bfbbe6437b58021325f01f62d5e891e812690a80f8464ef2c825ef8e47
|
7
|
+
data.tar.gz: 8aceb6e4a30d97e487b7bb29de4393c3c7f8598261c12f36dd66743d2a3c469bce61423c062e38b757d5265711d07172a743d04060dc336521f2dd10f80b7693
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.7.0 (2024-06-20)
|
6
|
+
|
7
|
+
- **Ruby 2.7+** is required.
|
8
|
+
|
9
|
+
- Support using callable objects as scopes. ([@killondark][])
|
10
|
+
|
11
|
+
## 0.6.9 (2024-04-19)
|
12
|
+
|
13
|
+
- Add `.with_context` modifier to the `#have_authorized_scope` matcher. ([@killondark][])
|
14
|
+
|
5
15
|
## 0.6.8 (2024-01-17)
|
6
16
|
|
7
17
|
- Do not preload Rails base classes, use load hooks everywhere. ([@palkan][])
|
@@ -509,3 +519,4 @@ This value is now stored in a cache (if any) instead of just the call result (`t
|
|
509
519
|
[@skojin]: https://github.com/skojin
|
510
520
|
[@tomdalling]: https://github.com/tomdalling
|
511
521
|
[@matsales28]: https://github.com/matsales28
|
522
|
+
[@killondark]: https://github.com/killondark
|
@@ -9,7 +9,7 @@ module ActionPolicy
|
|
9
9
|
|
10
10
|
# Returns policy instance for the record.
|
11
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 ?
|
12
|
+
context = context ? build_authorization_context.merge(context) : authorization_context
|
13
13
|
|
14
14
|
policy_class = with || ::ActionPolicy.lookup(
|
15
15
|
record,
|
@@ -18,8 +18,10 @@ module ActionPolicy
|
|
18
18
|
policy_class&.new(record, **context)
|
19
19
|
end
|
20
20
|
|
21
|
-
def authorization_context
|
22
|
-
|
21
|
+
def authorization_context ; @authorization_context ||= build_authorization_context; end
|
22
|
+
|
23
|
+
def build_authorization_context
|
24
|
+
Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
|
23
25
|
end
|
24
26
|
|
25
27
|
def authorization_namespace
|
@@ -21,7 +21,7 @@ module ActionPolicy
|
|
21
21
|
#
|
22
22
|
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
23
23
|
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
|
24
|
-
:target_expectations
|
24
|
+
:target_expectations, :context
|
25
25
|
|
26
26
|
def initialize(type)
|
27
27
|
@type = type
|
@@ -49,6 +49,11 @@ module ActionPolicy
|
|
49
49
|
self
|
50
50
|
end
|
51
51
|
|
52
|
+
def with_context(context)
|
53
|
+
@context = context
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
52
57
|
def match(_expected, actual)
|
53
58
|
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
54
59
|
|
@@ -56,7 +61,7 @@ module ActionPolicy
|
|
56
61
|
|
57
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
58
63
|
|
59
|
-
matching_scopes = actual_scopes.select { |_1| _1.matches?(policy, type, name, scope_options) }
|
64
|
+
matching_scopes = actual_scopes.select { |_1| _1.matches?(policy, type, name, scope_options, context) }
|
60
65
|
|
61
66
|
return false if matching_scopes.empty?
|
62
67
|
|
@@ -80,6 +85,7 @@ module ActionPolicy
|
|
80
85
|
def failure_message
|
81
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
82
87
|
"#{scope_options_message} " \
|
88
|
+
"#{context ? "and context #{context.inspect} " : ""}" \
|
83
89
|
"from #{policy} to have been applied, " \
|
84
90
|
"but #{actual_scopes_message}"
|
85
91
|
end
|
@@ -7,8 +7,7 @@ begin
|
|
7
7
|
# Ignore parse warnings when patch
|
8
8
|
# Ruby version mismatches
|
9
9
|
$VERBOSE = nil
|
10
|
-
require "
|
11
|
-
require "unparser"
|
10
|
+
require "prism"
|
12
11
|
rescue LoadError
|
13
12
|
# do nothing
|
14
13
|
ensure
|
@@ -42,7 +41,7 @@ module ActionPolicy
|
|
42
41
|
FALSE = "\e[31mfalse\e[0m"
|
43
42
|
|
44
43
|
class Visitor
|
45
|
-
attr_reader :lines, :object
|
44
|
+
attr_reader :lines, :object, :source
|
46
45
|
attr_accessor :indent
|
47
46
|
|
48
47
|
def initialize(object)
|
@@ -52,6 +51,8 @@ module ActionPolicy
|
|
52
51
|
def collect(ast)
|
53
52
|
@lines = []
|
54
53
|
@indent = 0
|
54
|
+
@source = ast.source.source
|
55
|
+
ast = ast.value.child_nodes[0].child_nodes[0].body
|
55
56
|
|
56
57
|
visit_node(ast)
|
57
58
|
|
@@ -67,7 +68,7 @@ module ActionPolicy
|
|
67
68
|
end
|
68
69
|
|
69
70
|
def expression_with_result(sexp)
|
70
|
-
expression =
|
71
|
+
expression = source[sexp.location.start_offset...sexp.location.end_offset]
|
71
72
|
"#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
|
72
73
|
end
|
73
74
|
|
@@ -78,36 +79,33 @@ module ActionPolicy
|
|
78
79
|
"Failed: #{e.message}"
|
79
80
|
end
|
80
81
|
|
81
|
-
def
|
82
|
-
visit_node(ast.
|
82
|
+
def visit_and_node(ast)
|
83
|
+
visit_node(ast.left)
|
83
84
|
lines << indented("AND")
|
84
|
-
visit_node(ast.
|
85
|
+
visit_node(ast.right)
|
85
86
|
end
|
86
87
|
|
87
|
-
def
|
88
|
-
visit_node(ast.
|
88
|
+
def visit_or_node(ast)
|
89
|
+
visit_node(ast.left)
|
89
90
|
lines << indented("OR")
|
90
|
-
visit_node(ast.
|
91
|
+
visit_node(ast.right)
|
91
92
|
end
|
92
93
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
self.indent += 2
|
98
|
-
visit_node(ast.children[0])
|
94
|
+
def visit_statements_node(ast)
|
95
|
+
ast.child_nodes.each do |node|
|
96
|
+
visit_node(node)
|
97
|
+
# restore indent after each expression
|
99
98
|
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
99
|
end
|
109
100
|
end
|
110
101
|
|
102
|
+
def visit_parentheses_node(ast)
|
103
|
+
lines << indented("(")
|
104
|
+
self.indent += 2
|
105
|
+
visit_node(ast.child_nodes[0])
|
106
|
+
lines << indented(")")
|
107
|
+
end
|
108
|
+
|
111
109
|
def visit_missing(ast)
|
112
110
|
lines << indented(expression_with_result(ast))
|
113
111
|
end
|
@@ -128,15 +126,13 @@ module ActionPolicy
|
|
128
126
|
class << self
|
129
127
|
attr_accessor :ignore_expressions
|
130
128
|
|
131
|
-
if defined?(::
|
129
|
+
if defined?(::Prism) && defined?(::MethodSource)
|
132
130
|
def available?() ; true; end
|
133
131
|
|
134
132
|
def print_method(object, method_name)
|
135
|
-
ast = object.method(method_name).source
|
136
|
-
# outer node is a method definition itself
|
137
|
-
body = ast.children[2]
|
133
|
+
ast = Prism.parse(object.method(method_name).source)
|
138
134
|
|
139
|
-
Visitor.new(object).collect(
|
135
|
+
Visitor.new(object).collect(ast)
|
140
136
|
end
|
141
137
|
else
|
142
138
|
def available?() ; false; 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: namespace, context: context, allow_nil: allow_nil, default: default, strict_namespace: strict_namespace
|
17
|
+
)
|
18
|
+
policy_class&.new(record, **context)
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorization_context ; @authorization_context ||= build_authorization_context; end
|
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, **__kwrest__)
|
63
|
+
record_key = record._policy_cache_key(use_object_id: true)
|
64
|
+
context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
|
65
|
+
|
66
|
+
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -147,7 +147,7 @@ module ActionPolicy
|
|
147
147
|
end
|
148
148
|
|
149
149
|
# Return annotated source code for the rule
|
150
|
-
# NOTE: require "method_source" and "
|
150
|
+
# NOTE: require "method_source" and "prism" gems to be installed.
|
151
151
|
# Otherwise returns empty string.
|
152
152
|
def inspect_rule(rule) ; PrettyPrint.print_method(self, rule); end
|
153
153
|
|
@@ -21,7 +21,7 @@ module ActionPolicy
|
|
21
21
|
#
|
22
22
|
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
23
23
|
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
|
24
|
-
:target_expectations
|
24
|
+
:target_expectations, :context
|
25
25
|
|
26
26
|
def initialize(type)
|
27
27
|
@type = type
|
@@ -49,6 +49,11 @@ module ActionPolicy
|
|
49
49
|
self
|
50
50
|
end
|
51
51
|
|
52
|
+
def with_context(context)
|
53
|
+
@context = context
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
52
57
|
def match(_expected, actual)
|
53
58
|
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
54
59
|
|
@@ -56,7 +61,7 @@ module ActionPolicy
|
|
56
61
|
|
57
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
58
63
|
|
59
|
-
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
|
64
|
+
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
|
60
65
|
|
61
66
|
return false if matching_scopes.empty?
|
62
67
|
|
@@ -80,6 +85,7 @@ module ActionPolicy
|
|
80
85
|
def failure_message
|
81
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
82
87
|
"#{scope_options_message} " \
|
88
|
+
"#{context ? "and context #{context.inspect} " : ""}" \
|
83
89
|
"from #{policy} to have been applied, " \
|
84
90
|
"but #{actual_scopes_message}"
|
85
91
|
end
|
@@ -7,8 +7,7 @@ begin
|
|
7
7
|
# Ignore parse warnings when patch
|
8
8
|
# Ruby version mismatches
|
9
9
|
$VERBOSE = nil
|
10
|
-
require "
|
11
|
-
require "unparser"
|
10
|
+
require "prism"
|
12
11
|
rescue LoadError
|
13
12
|
# do nothing
|
14
13
|
ensure
|
@@ -42,7 +41,7 @@ module ActionPolicy
|
|
42
41
|
FALSE = "\e[31mfalse\e[0m"
|
43
42
|
|
44
43
|
class Visitor
|
45
|
-
attr_reader :lines, :object
|
44
|
+
attr_reader :lines, :object, :source
|
46
45
|
attr_accessor :indent
|
47
46
|
|
48
47
|
def initialize(object)
|
@@ -52,6 +51,8 @@ module ActionPolicy
|
|
52
51
|
def collect(ast)
|
53
52
|
@lines = []
|
54
53
|
@indent = 0
|
54
|
+
@source = ast.source.source
|
55
|
+
ast = ast.value.child_nodes[0].child_nodes[0].body
|
55
56
|
|
56
57
|
visit_node(ast)
|
57
58
|
|
@@ -67,7 +68,7 @@ module ActionPolicy
|
|
67
68
|
end
|
68
69
|
|
69
70
|
def expression_with_result(sexp)
|
70
|
-
expression =
|
71
|
+
expression = source[sexp.location.start_offset...sexp.location.end_offset]
|
71
72
|
"#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
|
72
73
|
end
|
73
74
|
|
@@ -78,36 +79,33 @@ module ActionPolicy
|
|
78
79
|
"Failed: #{e.message}"
|
79
80
|
end
|
80
81
|
|
81
|
-
def
|
82
|
-
visit_node(ast.
|
82
|
+
def visit_and_node(ast)
|
83
|
+
visit_node(ast.left)
|
83
84
|
lines << indented("AND")
|
84
|
-
visit_node(ast.
|
85
|
+
visit_node(ast.right)
|
85
86
|
end
|
86
87
|
|
87
|
-
def
|
88
|
-
visit_node(ast.
|
88
|
+
def visit_or_node(ast)
|
89
|
+
visit_node(ast.left)
|
89
90
|
lines << indented("OR")
|
90
|
-
visit_node(ast.
|
91
|
+
visit_node(ast.right)
|
91
92
|
end
|
92
93
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
self.indent += 2
|
98
|
-
visit_node(ast.children[0])
|
94
|
+
def visit_statements_node(ast)
|
95
|
+
ast.child_nodes.each do |node|
|
96
|
+
visit_node(node)
|
97
|
+
# restore indent after each expression
|
99
98
|
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
99
|
end
|
109
100
|
end
|
110
101
|
|
102
|
+
def visit_parentheses_node(ast)
|
103
|
+
lines << indented("(")
|
104
|
+
self.indent += 2
|
105
|
+
visit_node(ast.child_nodes[0])
|
106
|
+
lines << indented(")")
|
107
|
+
end
|
108
|
+
|
111
109
|
def visit_missing(ast)
|
112
110
|
lines << indented(expression_with_result(ast))
|
113
111
|
end
|
@@ -128,15 +126,13 @@ module ActionPolicy
|
|
128
126
|
class << self
|
129
127
|
attr_accessor :ignore_expressions
|
130
128
|
|
131
|
-
if defined?(::
|
129
|
+
if defined?(::Prism) && defined?(::MethodSource)
|
132
130
|
def available?() ; true; end
|
133
131
|
|
134
132
|
def print_method(object, method_name)
|
135
|
-
ast = object.method(method_name).source
|
136
|
-
# outer node is a method definition itself
|
137
|
-
body = ast.children[2]
|
133
|
+
ast = Prism.parse(object.method(method_name).source)
|
138
134
|
|
139
|
-
Visitor.new(object).collect(
|
135
|
+
Visitor.new(object).collect(ast)
|
140
136
|
end
|
141
137
|
else
|
142
138
|
def available?() ; false; end
|
@@ -9,7 +9,7 @@ module ActionPolicy
|
|
9
9
|
|
10
10
|
# Returns policy instance for the record.
|
11
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 ?
|
12
|
+
context = context ? build_authorization_context.merge(context) : authorization_context
|
13
13
|
|
14
14
|
policy_class = with || ::ActionPolicy.lookup(
|
15
15
|
record,
|
@@ -18,8 +18,10 @@ module ActionPolicy
|
|
18
18
|
policy_class&.new(record, **context)
|
19
19
|
end
|
20
20
|
|
21
|
-
def authorization_context
|
22
|
-
|
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!"
|
23
25
|
end
|
24
26
|
|
25
27
|
def authorization_namespace
|
@@ -9,7 +9,7 @@ module ActionPolicy
|
|
9
9
|
|
10
10
|
# Returns policy instance for the record.
|
11
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 ?
|
12
|
+
context = context ? build_authorization_context.merge(context) : authorization_context
|
13
13
|
|
14
14
|
policy_class = with || ::ActionPolicy.lookup(
|
15
15
|
record,
|
@@ -18,8 +18,10 @@ module ActionPolicy
|
|
18
18
|
policy_class&.new(record, **context)
|
19
19
|
end
|
20
20
|
|
21
|
-
def authorization_context
|
22
|
-
|
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!"
|
23
25
|
end
|
24
26
|
|
25
27
|
def authorization_namespace
|
@@ -147,7 +147,7 @@ module ActionPolicy
|
|
147
147
|
end
|
148
148
|
|
149
149
|
# Return annotated source code for the rule
|
150
|
-
# NOTE: require "method_source" and "
|
150
|
+
# NOTE: require "method_source" and "prism" gems to be installed.
|
151
151
|
# Otherwise returns empty string.
|
152
152
|
def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
|
153
153
|
|
@@ -21,7 +21,7 @@ module ActionPolicy
|
|
21
21
|
#
|
22
22
|
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
23
23
|
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
|
24
|
-
:target_expectations
|
24
|
+
:target_expectations, :context
|
25
25
|
|
26
26
|
def initialize(type)
|
27
27
|
@type = type
|
@@ -49,6 +49,11 @@ module ActionPolicy
|
|
49
49
|
self
|
50
50
|
end
|
51
51
|
|
52
|
+
def with_context(context)
|
53
|
+
@context = context
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
52
57
|
def match(_expected, actual)
|
53
58
|
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
54
59
|
|
@@ -56,7 +61,7 @@ module ActionPolicy
|
|
56
61
|
|
57
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
58
63
|
|
59
|
-
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
|
64
|
+
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
|
60
65
|
|
61
66
|
return false if matching_scopes.empty?
|
62
67
|
|
@@ -80,6 +85,7 @@ module ActionPolicy
|
|
80
85
|
def failure_message
|
81
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
82
87
|
"#{scope_options_message} " \
|
88
|
+
"#{context ? "and context #{context.inspect} " : ""}" \
|
83
89
|
"from #{policy} to have been applied, " \
|
84
90
|
"but #{actual_scopes_message}"
|
85
91
|
end
|
@@ -9,7 +9,7 @@ module ActionPolicy
|
|
9
9
|
|
10
10
|
# Returns policy instance for the record.
|
11
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 ?
|
12
|
+
context = context ? build_authorization_context.merge(context) : authorization_context
|
13
13
|
|
14
14
|
policy_class = with || ::ActionPolicy.lookup(
|
15
15
|
record,
|
@@ -18,8 +18,10 @@ module ActionPolicy
|
|
18
18
|
policy_class&.new(record, **context)
|
19
19
|
end
|
20
20
|
|
21
|
-
def authorization_context
|
22
|
-
|
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!"
|
23
25
|
end
|
24
26
|
|
25
27
|
def authorization_namespace
|
@@ -147,7 +147,7 @@ module ActionPolicy
|
|
147
147
|
end
|
148
148
|
|
149
149
|
# Return annotated source code for the rule
|
150
|
-
# NOTE: require "method_source" and "
|
150
|
+
# NOTE: require "method_source" and "prism" gems to be installed.
|
151
151
|
# Otherwise returns empty string.
|
152
152
|
def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
|
153
153
|
|
@@ -113,8 +113,11 @@ module ActionPolicy
|
|
113
113
|
|
114
114
|
module ClassMethods # :nodoc:
|
115
115
|
# Register a new scoping method for the `type`
|
116
|
-
def scope_for(type, name = :default, &block)
|
116
|
+
def scope_for(type, name = :default, callable = nil, &block)
|
117
|
+
name, callable = prepare_args(name, callable)
|
118
|
+
|
117
119
|
mid = :"__scoping__#{type}__#{name}"
|
120
|
+
block = ->(target) { callable.call(self, target) } if callable
|
118
121
|
|
119
122
|
define_method(mid, &block)
|
120
123
|
|
@@ -154,6 +157,14 @@ module ActionPolicy
|
|
154
157
|
[]
|
155
158
|
end
|
156
159
|
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def prepare_args(name, callable)
|
164
|
+
return [name, callable] if name && callable
|
165
|
+
return [name, nil] if name.is_a?(Symbol) || name.is_a?(String)
|
166
|
+
[:default, name]
|
167
|
+
end
|
157
168
|
end
|
158
169
|
end
|
159
170
|
end
|
@@ -21,7 +21,7 @@ module ActionPolicy
|
|
21
21
|
#
|
22
22
|
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
23
23
|
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
|
24
|
-
:target_expectations
|
24
|
+
:target_expectations, :context
|
25
25
|
|
26
26
|
def initialize(type)
|
27
27
|
@type = type
|
@@ -49,6 +49,11 @@ module ActionPolicy
|
|
49
49
|
self
|
50
50
|
end
|
51
51
|
|
52
|
+
def with_context(context)
|
53
|
+
@context = context
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
52
57
|
def match(_expected, actual)
|
53
58
|
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
54
59
|
|
@@ -56,7 +61,7 @@ module ActionPolicy
|
|
56
61
|
|
57
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
58
63
|
|
59
|
-
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
|
64
|
+
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
|
60
65
|
|
61
66
|
return false if matching_scopes.empty?
|
62
67
|
|
@@ -80,6 +85,7 @@ module ActionPolicy
|
|
80
85
|
def failure_message
|
81
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
82
87
|
"#{scope_options_message} " \
|
88
|
+
"#{context ? "and context #{context.inspect} " : ""}" \
|
83
89
|
"from #{policy} to have been applied, " \
|
84
90
|
"but #{actual_scopes_message}"
|
85
91
|
end
|
@@ -82,7 +82,7 @@ module ActionPolicy
|
|
82
82
|
# end
|
83
83
|
# end
|
84
84
|
#
|
85
|
-
def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil)
|
85
|
+
def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil, context: {})
|
86
86
|
raise ArgumentError, "Block is required" unless block_given?
|
87
87
|
|
88
88
|
policy = with
|
@@ -97,10 +97,13 @@ module ActionPolicy
|
|
97
97
|
"without scope options"
|
98
98
|
end
|
99
99
|
|
100
|
+
context_message = context.empty? ? "without context" : "with context: #{context}"
|
101
|
+
|
100
102
|
assert(
|
101
|
-
actual_scopes.any? { |scope| scope.matches?(policy, type, as, scope_options) },
|
103
|
+
actual_scopes.any? { |scope| scope.matches?(policy, type, as, scope_options, context) },
|
102
104
|
"Expected a scoping named :#{as} for :#{type} type " \
|
103
105
|
"#{scope_options_message} " \
|
106
|
+
"and #{context_message} " \
|
104
107
|
"from #{policy} to have been applied, " \
|
105
108
|
"but no such scoping has been made.\n" \
|
106
109
|
"Registered scopings: " \
|
@@ -5,7 +5,19 @@ module ActionPolicy
|
|
5
5
|
module Testing
|
6
6
|
# Collects all Authorizer calls
|
7
7
|
module AuthorizeTracker
|
8
|
+
module Context
|
9
|
+
private
|
10
|
+
|
11
|
+
def context_matches?(context, actual)
|
12
|
+
return true unless context
|
13
|
+
|
14
|
+
context === actual || actual >= context
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
8
18
|
class Call # :nodoc:
|
19
|
+
include Context
|
20
|
+
|
9
21
|
attr_reader :policy, :rule
|
10
22
|
|
11
23
|
def initialize(policy, rule)
|
@@ -23,17 +35,11 @@ module ActionPolicy
|
|
23
35
|
"#{policy.record.inspect} was authorized with #{policy.class}##{rule} " \
|
24
36
|
"and context #{policy.authorization_context.inspect}"
|
25
37
|
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def context_matches?(context, actual)
|
30
|
-
return true unless context
|
31
|
-
|
32
|
-
context === actual || actual >= context
|
33
|
-
end
|
34
38
|
end
|
35
39
|
|
36
40
|
class Scoping # :nodoc:
|
41
|
+
include Context
|
42
|
+
|
37
43
|
attr_reader :policy, :target, :type, :name, :scope_options
|
38
44
|
|
39
45
|
def initialize(policy, target, type, name, scope_options)
|
@@ -44,11 +50,12 @@ module ActionPolicy
|
|
44
50
|
@scope_options = scope_options
|
45
51
|
end
|
46
52
|
|
47
|
-
def matches?(policy_class, actual_type, actual_name, actual_scope_options)
|
53
|
+
def matches?(policy_class, actual_type, actual_name, actual_scope_options, actual_context)
|
48
54
|
policy_class == policy.class &&
|
49
55
|
type == actual_type &&
|
50
56
|
name == actual_name &&
|
51
|
-
actual_scope_options === scope_options
|
57
|
+
actual_scope_options === scope_options &&
|
58
|
+
context_matches?(actual_context, policy.authorization_context)
|
52
59
|
end
|
53
60
|
|
54
61
|
def inspect
|
@@ -7,8 +7,7 @@ begin
|
|
7
7
|
# Ignore parse warnings when patch
|
8
8
|
# Ruby version mismatches
|
9
9
|
$VERBOSE = nil
|
10
|
-
require "
|
11
|
-
require "unparser"
|
10
|
+
require "prism"
|
12
11
|
rescue LoadError
|
13
12
|
# do nothing
|
14
13
|
ensure
|
@@ -42,7 +41,7 @@ module ActionPolicy
|
|
42
41
|
FALSE = "\e[31mfalse\e[0m"
|
43
42
|
|
44
43
|
class Visitor
|
45
|
-
attr_reader :lines, :object
|
44
|
+
attr_reader :lines, :object, :source
|
46
45
|
attr_accessor :indent
|
47
46
|
|
48
47
|
def initialize(object)
|
@@ -52,6 +51,8 @@ module ActionPolicy
|
|
52
51
|
def collect(ast)
|
53
52
|
@lines = []
|
54
53
|
@indent = 0
|
54
|
+
@source = ast.source.source
|
55
|
+
ast = ast.value.child_nodes[0].child_nodes[0].body
|
55
56
|
|
56
57
|
visit_node(ast)
|
57
58
|
|
@@ -67,7 +68,7 @@ module ActionPolicy
|
|
67
68
|
end
|
68
69
|
|
69
70
|
def expression_with_result(sexp)
|
70
|
-
expression =
|
71
|
+
expression = source[sexp.location.start_offset...sexp.location.end_offset]
|
71
72
|
"#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
|
72
73
|
end
|
73
74
|
|
@@ -78,36 +79,33 @@ module ActionPolicy
|
|
78
79
|
"Failed: #{e.message}"
|
79
80
|
end
|
80
81
|
|
81
|
-
def
|
82
|
-
visit_node(ast.
|
82
|
+
def visit_and_node(ast)
|
83
|
+
visit_node(ast.left)
|
83
84
|
lines << indented("AND")
|
84
|
-
visit_node(ast.
|
85
|
+
visit_node(ast.right)
|
85
86
|
end
|
86
87
|
|
87
|
-
def
|
88
|
-
visit_node(ast.
|
88
|
+
def visit_or_node(ast)
|
89
|
+
visit_node(ast.left)
|
89
90
|
lines << indented("OR")
|
90
|
-
visit_node(ast.
|
91
|
+
visit_node(ast.right)
|
91
92
|
end
|
92
93
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
self.indent += 2
|
98
|
-
visit_node(ast.children[0])
|
94
|
+
def visit_statements_node(ast)
|
95
|
+
ast.child_nodes.each do |node|
|
96
|
+
visit_node(node)
|
97
|
+
# restore indent after each expression
|
99
98
|
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
99
|
end
|
109
100
|
end
|
110
101
|
|
102
|
+
def visit_parentheses_node(ast)
|
103
|
+
lines << indented("(")
|
104
|
+
self.indent += 2
|
105
|
+
visit_node(ast.child_nodes[0])
|
106
|
+
lines << indented(")")
|
107
|
+
end
|
108
|
+
|
111
109
|
def visit_missing(ast)
|
112
110
|
lines << indented(expression_with_result(ast))
|
113
111
|
end
|
@@ -128,15 +126,13 @@ module ActionPolicy
|
|
128
126
|
class << self
|
129
127
|
attr_accessor :ignore_expressions
|
130
128
|
|
131
|
-
if defined?(::
|
129
|
+
if defined?(::Prism) && defined?(::MethodSource)
|
132
130
|
def available?() = true
|
133
131
|
|
134
132
|
def print_method(object, method_name)
|
135
|
-
ast = object.method(method_name).source
|
136
|
-
# outer node is a method definition itself
|
137
|
-
body = ast.children[2]
|
133
|
+
ast = Prism.parse(object.method(method_name).source)
|
138
134
|
|
139
|
-
Visitor.new(object).collect(
|
135
|
+
Visitor.new(object).collect(ast)
|
140
136
|
end
|
141
137
|
else
|
142
138
|
def available?() = false
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_policy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-next-core
|
@@ -133,7 +133,6 @@ files:
|
|
133
133
|
- LICENSE.txt
|
134
134
|
- README.md
|
135
135
|
- config/rubocop-rspec.yml
|
136
|
-
- lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb
|
137
136
|
- lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb
|
138
137
|
- lib/.rbnext/2.7/action_policy/i18n.rb
|
139
138
|
- lib/.rbnext/2.7/action_policy/policy/cache.rb
|
@@ -143,6 +142,7 @@ files:
|
|
143
142
|
- lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb
|
144
143
|
- lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb
|
145
144
|
- lib/.rbnext/2.7/action_policy/utils/pretty_print.rb
|
145
|
+
- lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb
|
146
146
|
- lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb
|
147
147
|
- lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb
|
148
148
|
- lib/.rbnext/3.0/action_policy/policy/aliases.rb
|
@@ -234,7 +234,7 @@ metadata:
|
|
234
234
|
documentation_uri: https://actionpolicy.evilmartians.io/
|
235
235
|
homepage_uri: https://actionpolicy.evilmartians.io/
|
236
236
|
source_code_uri: http://github.com/palkan/action_policy
|
237
|
-
post_install_message:
|
237
|
+
post_install_message:
|
238
238
|
rdoc_options: []
|
239
239
|
require_paths:
|
240
240
|
- lib
|
@@ -242,15 +242,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
242
242
|
requirements:
|
243
243
|
- - ">="
|
244
244
|
- !ruby/object:Gem::Version
|
245
|
-
version: 2.
|
245
|
+
version: 2.7.0
|
246
246
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
247
247
|
requirements:
|
248
248
|
- - ">="
|
249
249
|
- !ruby/object:Gem::Version
|
250
250
|
version: '0'
|
251
251
|
requirements: []
|
252
|
-
rubygems_version: 3.4.
|
253
|
-
signing_key:
|
252
|
+
rubygems_version: 3.4.19
|
253
|
+
signing_key:
|
254
254
|
specification_version: 4
|
255
255
|
summary: Authorization framework for Ruby/Rails application
|
256
256
|
test_files: []
|
@@ -1,159 +0,0 @@
|
|
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? { 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
|
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
|
143
|
-
|
144
|
-
def print_method(_, _) = ""
|
145
|
-
end
|
146
|
-
|
147
|
-
def colorize(val)
|
148
|
-
return val unless $stdout.isatty
|
149
|
-
return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
|
150
|
-
return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
|
151
|
-
val
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
self.ignore_expressions = [
|
156
|
-
/^\s*binding\.(pry|irb)\s*$/s
|
157
|
-
]
|
158
|
-
end
|
159
|
-
end
|