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