action_policy 0.7.4 → 0.7.6
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 +14 -3
- data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/cache.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +3 -3
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +4 -4
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +4 -4
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +2 -2
- data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb +4 -4
- data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +4 -4
- data/lib/.rbnext/3.4/action_policy/behaviours/policy_for.rb +70 -0
- data/lib/.rbnext/3.4/action_policy/i18n.rb +56 -0
- data/lib/.rbnext/3.4/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/3.4/action_policy/policy/pre_check.rb +160 -0
- data/lib/.rbnext/3.4/action_policy/rspec/be_authorized_to.rb +96 -0
- data/lib/.rbnext/3.4/action_policy/rspec/have_authorized_scope.rb +130 -0
- data/lib/.rbnext/3.4/action_policy/utils/pretty_print.rb +155 -0
- data/lib/action_policy/behaviour.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/action_policy/i18n.rb +1 -1
- data/lib/action_policy/policy/cache.rb +1 -1
- data/lib/action_policy/policy/execution_result.rb +1 -1
- data/lib/action_policy/policy/pre_check.rb +3 -3
- data/lib/action_policy/rails/controller.rb +45 -4
- data/lib/action_policy/rails/policy/instrumentation.rb +1 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +3 -3
- data/lib/action_policy/rspec/have_authorized_scope.rb +3 -3
- data/lib/action_policy/test_helper.rb +1 -1
- data/lib/action_policy/utils/pretty_print.rb +2 -2
- data/lib/action_policy/version.rb +1 -1
- data/lib/action_policy.rb +1 -1
- data/lib/ruby_lsp/action_policy/addon.rb +170 -0
- metadata +11 -6
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_policy/testing"
|
|
4
|
+
|
|
5
|
+
module ActionPolicy
|
|
6
|
+
module RSpec
|
|
7
|
+
# Authorization matcher `be_authorized_to`.
|
|
8
|
+
#
|
|
9
|
+
# Verifies that a block of code has been authorized using specific policy.
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
#
|
|
13
|
+
# # in controller/request specs
|
|
14
|
+
# subject { patch :update, id: product.id }
|
|
15
|
+
#
|
|
16
|
+
# it "is authorized" do
|
|
17
|
+
# expect { subject }
|
|
18
|
+
# .to be_authorized_to(:manage?, product)
|
|
19
|
+
# .with(ProductPolicy)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class BeAuthorizedTo < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
|
23
|
+
attr_reader :rule, :target, :policy, :actual_calls, :context
|
|
24
|
+
|
|
25
|
+
def initialize(rule, target)
|
|
26
|
+
@rule = rule
|
|
27
|
+
@target = target
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def with(policy)
|
|
31
|
+
@policy = policy
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_context(context)
|
|
36
|
+
@context = context
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def match(_expected, actual)
|
|
41
|
+
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
|
42
|
+
|
|
43
|
+
@policy ||= ::ActionPolicy.lookup(target)
|
|
44
|
+
@context ||= nil
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
|
|
48
|
+
rescue ActionPolicy::Unauthorized
|
|
49
|
+
# we don't want to care about authorization result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
|
|
53
|
+
|
|
54
|
+
actual_calls.any? { |it| it.matches?(policy, rule, target, context) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def does_not_match?(*)
|
|
58
|
+
raise "This matcher doesn't support negation"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def supports_block_expectations?() = true
|
|
62
|
+
|
|
63
|
+
def failure_message
|
|
64
|
+
"expected #{formatted_record} " \
|
|
65
|
+
"to be authorized with #{policy}##{rule}, " \
|
|
66
|
+
"#{"and context #{context.inspect}, " if context}" \
|
|
67
|
+
"but #{actual_calls_message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def actual_calls_message
|
|
71
|
+
if actual_calls.empty?
|
|
72
|
+
"no authorization calls have been made"
|
|
73
|
+
else
|
|
74
|
+
"the following calls were encountered:\n" \
|
|
75
|
+
"#{formatted_calls}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def formatted_calls
|
|
80
|
+
actual_calls.map do |it|
|
|
81
|
+
" - #{it.inspect}"
|
|
82
|
+
end.join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def formatted_record(record = target) = ::RSpec::Support::ObjectFormatter.format(record)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
RSpec.configure do |config|
|
|
91
|
+
config.include(Module.new do
|
|
92
|
+
def be_authorized_to(rule, target)
|
|
93
|
+
ActionPolicy::RSpec::BeAuthorizedTo.new(rule, target)
|
|
94
|
+
end
|
|
95
|
+
end)
|
|
96
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_policy/testing"
|
|
4
|
+
|
|
5
|
+
module ActionPolicy
|
|
6
|
+
module RSpec
|
|
7
|
+
# Implements `have_authorized_scope` matcher.
|
|
8
|
+
#
|
|
9
|
+
# Verifies that a block of code applies authorization scoping using specific policy.
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
#
|
|
13
|
+
# # in controller/request specs
|
|
14
|
+
# subject { get :index }
|
|
15
|
+
#
|
|
16
|
+
# it "has authorized scope" do
|
|
17
|
+
# expect { subject }
|
|
18
|
+
# .to have_authorized_scope(:active_record_relation)
|
|
19
|
+
# .with(ProductPolicy)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
|
23
|
+
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
|
|
24
|
+
:target_expectations, :context
|
|
25
|
+
|
|
26
|
+
def initialize(type)
|
|
27
|
+
@type = type
|
|
28
|
+
@name = :default
|
|
29
|
+
@scope_options = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with(policy)
|
|
33
|
+
@policy = policy
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def as(name)
|
|
38
|
+
@name = name
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def with_scope_options(scope_options)
|
|
43
|
+
@scope_options = scope_options
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def with_target(&block)
|
|
48
|
+
@target_expectations = block
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_context(context)
|
|
53
|
+
@context = context
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def match(_expected, actual)
|
|
58
|
+
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
|
59
|
+
|
|
60
|
+
ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
|
|
61
|
+
|
|
62
|
+
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
|
63
|
+
|
|
64
|
+
matching_scopes = actual_scopes.select { |it| it.matches?(policy, type, name, scope_options, context) }
|
|
65
|
+
|
|
66
|
+
return false if matching_scopes.empty?
|
|
67
|
+
|
|
68
|
+
return true unless target_expectations
|
|
69
|
+
|
|
70
|
+
if matching_scopes.size > 1
|
|
71
|
+
raise "Too many matching scopings (#{matching_scopes.size}), " \
|
|
72
|
+
"you can run `.with_target` only when there is the only one match"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
target_expectations.call(matching_scopes.first.target)
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def does_not_match?(*)
|
|
80
|
+
raise "This matcher doesn't support negation"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def supports_block_expectations?() = true
|
|
84
|
+
|
|
85
|
+
def failure_message
|
|
86
|
+
"expected a scoping named :#{name} for type :#{type} " \
|
|
87
|
+
"#{scope_options_message} " \
|
|
88
|
+
"#{"and context #{context.inspect} " if context}" \
|
|
89
|
+
"from #{policy} to have been applied, " \
|
|
90
|
+
"but #{actual_scopes_message}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def scope_options_message
|
|
94
|
+
if scope_options
|
|
95
|
+
if defined?(::RSpec::Matchers::Composable) &&
|
|
96
|
+
scope_options.is_a?(::RSpec::Matchers::Composable)
|
|
97
|
+
"with scope options #{scope_options.description}"
|
|
98
|
+
else
|
|
99
|
+
"with scope options #{scope_options}"
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
"without scope options"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def actual_scopes_message
|
|
107
|
+
if actual_scopes.empty?
|
|
108
|
+
"no scopings have been made"
|
|
109
|
+
else
|
|
110
|
+
"the following scopings were encountered:\n" \
|
|
111
|
+
"#{formatted_scopings}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def formatted_scopings
|
|
116
|
+
actual_scopes.map do |it|
|
|
117
|
+
" - #{it.inspect}"
|
|
118
|
+
end.join("\n")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
RSpec.configure do |config|
|
|
125
|
+
config.include(Module.new do
|
|
126
|
+
def have_authorized_scope(type)
|
|
127
|
+
ActionPolicy::RSpec::HaveAuthorizedScope.new(type)
|
|
128
|
+
end
|
|
129
|
+
end)
|
|
130
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
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 "prism"
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# do nothing
|
|
13
|
+
ensure
|
|
14
|
+
$VERBOSE = old_verbose
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ActionPolicy
|
|
18
|
+
using RubyNext
|
|
19
|
+
|
|
20
|
+
# Takes the object and a method name,
|
|
21
|
+
# and returns the "annotated" source code for the method:
|
|
22
|
+
# code is split into parts by logical operators and each
|
|
23
|
+
# part is evaluated separately.
|
|
24
|
+
#
|
|
25
|
+
# Example:
|
|
26
|
+
#
|
|
27
|
+
# class MyClass
|
|
28
|
+
# def access?
|
|
29
|
+
# admin? && access_feed?
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# puts PrettyPrint.format_method(MyClass.new, :access?)
|
|
34
|
+
#
|
|
35
|
+
# #=> MyClass#access?
|
|
36
|
+
# #=> ↳ admin? #=> false
|
|
37
|
+
# #=> AND
|
|
38
|
+
# #=> access_feed? #=> true
|
|
39
|
+
module PrettyPrint
|
|
40
|
+
TRUE = "\e[32mtrue\e[0m"
|
|
41
|
+
FALSE = "\e[31mfalse\e[0m"
|
|
42
|
+
|
|
43
|
+
class Visitor
|
|
44
|
+
attr_reader :lines, :object, :source
|
|
45
|
+
attr_accessor :indent
|
|
46
|
+
|
|
47
|
+
def initialize(object)
|
|
48
|
+
@object = object
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def collect(ast)
|
|
52
|
+
@lines = []
|
|
53
|
+
@indent = 0
|
|
54
|
+
@source = ast.source.source
|
|
55
|
+
ast = ast.value.child_nodes[0].child_nodes[0].body
|
|
56
|
+
|
|
57
|
+
visit_node(ast)
|
|
58
|
+
|
|
59
|
+
lines.join("\n")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def visit_node(ast)
|
|
63
|
+
if respond_to?("visit_#{ast.type}")
|
|
64
|
+
send("visit_#{ast.type}", ast)
|
|
65
|
+
else
|
|
66
|
+
visit_missing ast
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def expression_with_result(sexp)
|
|
71
|
+
expression = source[sexp.location.start_offset...sexp.location.end_offset]
|
|
72
|
+
"#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def eval_exp(exp)
|
|
76
|
+
return "<skipped>" if ignore_exp?(exp)
|
|
77
|
+
object.instance_eval(exp)
|
|
78
|
+
rescue => e
|
|
79
|
+
"Failed: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_and_node(ast)
|
|
83
|
+
visit_node(ast.left)
|
|
84
|
+
lines << indented("AND")
|
|
85
|
+
visit_node(ast.right)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def visit_or_node(ast)
|
|
89
|
+
visit_node(ast.left)
|
|
90
|
+
lines << indented("OR")
|
|
91
|
+
visit_node(ast.right)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def visit_statements_node(ast)
|
|
95
|
+
ast.child_nodes.each do |node|
|
|
96
|
+
visit_node(node)
|
|
97
|
+
# restore indent after each expression
|
|
98
|
+
self.indent -= 2
|
|
99
|
+
end
|
|
100
|
+
end
|
|
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
|
+
|
|
109
|
+
def visit_missing(ast)
|
|
110
|
+
lines << indented(expression_with_result(ast))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def indented(str)
|
|
114
|
+
"#{"↳ " if indent.zero?}#{" " * indent}#{str}".tap do
|
|
115
|
+
# increase indent after the first expression
|
|
116
|
+
self.indent += 2 if indent.zero?
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Some lines should not be evaled
|
|
121
|
+
def ignore_exp?(exp)
|
|
122
|
+
PrettyPrint.ignore_expressions.any? { |it| exp.match?(it) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class << self
|
|
127
|
+
attr_accessor :ignore_expressions
|
|
128
|
+
|
|
129
|
+
if defined?(::Prism) && defined?(::MethodSource)
|
|
130
|
+
def available?() = true
|
|
131
|
+
|
|
132
|
+
def print_method(object, method_name)
|
|
133
|
+
ast = Prism.parse(object.method(method_name).source)
|
|
134
|
+
|
|
135
|
+
Visitor.new(object).collect(ast)
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
def available?() = false
|
|
139
|
+
|
|
140
|
+
def print_method(_, _) = ""
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def colorize(val)
|
|
144
|
+
return val unless $stdout.isatty
|
|
145
|
+
return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
|
|
146
|
+
return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
|
|
147
|
+
val
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
self.ignore_expressions = [
|
|
152
|
+
/^\s*binding\.(pry|irb)\s*$/s
|
|
153
|
+
]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -65,7 +65,7 @@ module ActionPolicy
|
|
|
65
65
|
private def build_authorization_context
|
|
66
66
|
self.class.authorization_targets
|
|
67
67
|
.each_with_object({}) do |(key, method_or_proc), obj|
|
|
68
|
-
|
|
68
|
+
obj[key] = method_or_proc.is_a?(Proc) ? instance_exec(&method_or_proc) : send(method_or_proc)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
|
|
63
63
|
record_key = record._policy_cache_key(use_object_id: true)
|
|
64
|
-
context_key = context.values.map {
|
|
64
|
+
context_key = context.values.map { it._policy_cache_key(use_object_id: true) }.join(".")
|
|
65
65
|
|
|
66
66
|
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
|
67
67
|
end
|
data/lib/action_policy/i18n.rb
CHANGED
|
@@ -21,7 +21,7 @@ module ActionPolicy
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def candidates_for(policy_class, rule)
|
|
24
|
-
policy_hierarchy = policy_class.ancestors.select {
|
|
24
|
+
policy_hierarchy = policy_class.ancestors.select { it.respond_to?(:identifier) }
|
|
25
25
|
[
|
|
26
26
|
*policy_hierarchy.map { |klass| :"policy.#{klass.identifier}.#{rule}" },
|
|
27
27
|
:"policy.#{rule}",
|
|
@@ -125,7 +125,7 @@ module ActionPolicy
|
|
|
125
125
|
def pre_check(*names, **options)
|
|
126
126
|
names.each do |name|
|
|
127
127
|
# do not allow pre-check override
|
|
128
|
-
check = pre_checks.find {
|
|
128
|
+
check = pre_checks.find { it.name == name }
|
|
129
129
|
raise "Pre-check already defined: #{name}" unless check.nil?
|
|
130
130
|
|
|
131
131
|
pre_checks << Check.new(self, name, **options)
|
|
@@ -134,14 +134,14 @@ module ActionPolicy
|
|
|
134
134
|
|
|
135
135
|
def skip_pre_check(*names, **options)
|
|
136
136
|
names.each do |name|
|
|
137
|
-
check = pre_checks.find {
|
|
137
|
+
check = pre_checks.find { it.name == name }
|
|
138
138
|
raise "Pre-check not found: #{name}" if check.nil?
|
|
139
139
|
|
|
140
140
|
# when no options provided we remove this check completely
|
|
141
141
|
next pre_checks.delete(check) if options.empty?
|
|
142
142
|
|
|
143
143
|
# otherwise duplicate and apply skip options
|
|
144
|
-
pre_checks[pre_checks.index(check)] = check.dup.tap {
|
|
144
|
+
pre_checks[pre_checks.index(check)] = check.dup.tap { it.skip!(**options) }
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
@@ -11,9 +11,16 @@ module ActionPolicy
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Raised when `authorized_scope` hasn't been called for action
|
|
15
|
+
class UnscopedAction < Error
|
|
16
|
+
def initialize(controller, action)
|
|
17
|
+
super("Action '#{controller}##{action}' hasn't been scoped")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
# Controller concern.
|
|
15
22
|
# Add `authorize!` and `allowed_to?` methods,
|
|
16
|
-
# provide `verify_authorized`
|
|
23
|
+
# provide `verify_authorized` and `verify_authorized_scoped` hooks.
|
|
17
24
|
module Controller
|
|
18
25
|
extend ActiveSupport::Concern
|
|
19
26
|
|
|
@@ -29,10 +36,10 @@ module ActionPolicy
|
|
|
29
36
|
helper_method :allowance_to
|
|
30
37
|
end
|
|
31
38
|
|
|
32
|
-
attr_writer :authorize_count
|
|
33
|
-
attr_reader :verify_authorized_skipped
|
|
39
|
+
attr_writer :authorize_count, :scoped_count
|
|
40
|
+
attr_reader :verify_authorized_skipped, :verify_authorized_scoped_skipped
|
|
34
41
|
|
|
35
|
-
protected :authorize_count=, :authorize_count
|
|
42
|
+
protected :authorize_count=, :authorize_count, :scoped_count=, :scoped_count
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
# Authorize action against a policy.
|
|
@@ -56,6 +63,16 @@ module ActionPolicy
|
|
|
56
63
|
policy_record
|
|
57
64
|
end
|
|
58
65
|
|
|
66
|
+
# Apply scope to the target.
|
|
67
|
+
#
|
|
68
|
+
# @return the scoped target
|
|
69
|
+
def authorized_scope(target, **options)
|
|
70
|
+
scoped = super
|
|
71
|
+
|
|
72
|
+
self.scoped_count += 1
|
|
73
|
+
scoped
|
|
74
|
+
end
|
|
75
|
+
|
|
59
76
|
# Tries to infer the resource class from controller name
|
|
60
77
|
# (i.e. `controller_name.classify.safe_constantize`).
|
|
61
78
|
def implicit_authorization_target
|
|
@@ -67,14 +84,27 @@ module ActionPolicy
|
|
|
67
84
|
authorize_count.zero? && !verify_authorized_skipped
|
|
68
85
|
end
|
|
69
86
|
|
|
87
|
+
def verify_authorized_scoped
|
|
88
|
+
Kernel.raise UnscopedAction.new(controller_path, action_name) if
|
|
89
|
+
scoped_count.zero? && !verify_authorized_scoped_skipped
|
|
90
|
+
end
|
|
91
|
+
|
|
70
92
|
def authorize_count
|
|
71
93
|
@authorize_count ||= 0
|
|
72
94
|
end
|
|
73
95
|
|
|
96
|
+
def scoped_count
|
|
97
|
+
@scoped_count ||= 0
|
|
98
|
+
end
|
|
99
|
+
|
|
74
100
|
def skip_verify_authorized!
|
|
75
101
|
@verify_authorized_skipped = true
|
|
76
102
|
end
|
|
77
103
|
|
|
104
|
+
def skip_verify_authorized_scoped!
|
|
105
|
+
@verify_authorized_scoped_skipped = true
|
|
106
|
+
end
|
|
107
|
+
|
|
78
108
|
class_methods do
|
|
79
109
|
# Adds after_action callback to check that
|
|
80
110
|
# authorize! method has been called.
|
|
@@ -86,6 +116,17 @@ module ActionPolicy
|
|
|
86
116
|
def skip_verify_authorized(**options)
|
|
87
117
|
skip_after_action :verify_authorized, options
|
|
88
118
|
end
|
|
119
|
+
|
|
120
|
+
# Adds after_action callback to check that
|
|
121
|
+
# authorized_scope method has been called.
|
|
122
|
+
def verify_authorized_scoped(**options)
|
|
123
|
+
after_action :verify_authorized_scoped, options
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Skips verify_authorized_scoped after_action callback.
|
|
127
|
+
def skip_verify_authorized_scoped(**options)
|
|
128
|
+
skip_after_action :verify_authorized_scoped, options
|
|
129
|
+
end
|
|
89
130
|
end
|
|
90
131
|
end
|
|
91
132
|
end
|
|
@@ -51,7 +51,7 @@ module ActionPolicy
|
|
|
51
51
|
|
|
52
52
|
@actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
|
|
53
53
|
|
|
54
|
-
actual_calls.any? {
|
|
54
|
+
actual_calls.any? { it.matches?(policy, rule, target, context) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def does_not_match?(*)
|
|
@@ -63,7 +63,7 @@ module ActionPolicy
|
|
|
63
63
|
def failure_message
|
|
64
64
|
"expected #{formatted_record} " \
|
|
65
65
|
"to be authorized with #{policy}##{rule}, " \
|
|
66
|
-
"#{
|
|
66
|
+
"#{"and context #{context.inspect}, " if context}" \
|
|
67
67
|
"but #{actual_calls_message}"
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -78,7 +78,7 @@ module ActionPolicy
|
|
|
78
78
|
|
|
79
79
|
def formatted_calls
|
|
80
80
|
actual_calls.map do
|
|
81
|
-
" - #{
|
|
81
|
+
" - #{it.inspect}"
|
|
82
82
|
end.join("\n")
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -61,7 +61,7 @@ module ActionPolicy
|
|
|
61
61
|
|
|
62
62
|
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
|
63
63
|
|
|
64
|
-
matching_scopes = actual_scopes.select {
|
|
64
|
+
matching_scopes = actual_scopes.select { it.matches?(policy, type, name, scope_options, context) }
|
|
65
65
|
|
|
66
66
|
return false if matching_scopes.empty?
|
|
67
67
|
|
|
@@ -85,7 +85,7 @@ module ActionPolicy
|
|
|
85
85
|
def failure_message
|
|
86
86
|
"expected a scoping named :#{name} for type :#{type} " \
|
|
87
87
|
"#{scope_options_message} " \
|
|
88
|
-
"#{
|
|
88
|
+
"#{"and context #{context.inspect} " if context}" \
|
|
89
89
|
"from #{policy} to have been applied, " \
|
|
90
90
|
"but #{actual_scopes_message}"
|
|
91
91
|
end
|
|
@@ -114,7 +114,7 @@ module ActionPolicy
|
|
|
114
114
|
|
|
115
115
|
def formatted_scopings
|
|
116
116
|
actual_scopes.map do
|
|
117
|
-
" - #{
|
|
117
|
+
" - #{it.inspect}"
|
|
118
118
|
end.join("\n")
|
|
119
119
|
end
|
|
120
120
|
end
|
|
@@ -52,7 +52,7 @@ module ActionPolicy
|
|
|
52
52
|
assert(
|
|
53
53
|
actual_calls.any? { |call| call.matches?(policy, rule, target, context) },
|
|
54
54
|
"Expected #{target.inspect} to be authorized with #{policy}##{rule}, " \
|
|
55
|
-
"#{
|
|
55
|
+
"#{"and context #{context}, " if context}" \
|
|
56
56
|
"but no such authorization has been made.\n" \
|
|
57
57
|
"Registered authorizations: " \
|
|
58
58
|
"#{actual_calls.empty? ? "none" : actual_calls.map(&:inspect).join(",")}"
|
|
@@ -111,7 +111,7 @@ module ActionPolicy
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def indented(str)
|
|
114
|
-
"#{
|
|
114
|
+
"#{"↳ " if indent.zero?}#{" " * indent}#{str}".tap do
|
|
115
115
|
# increase indent after the first expression
|
|
116
116
|
self.indent += 2 if indent.zero?
|
|
117
117
|
end
|
|
@@ -119,7 +119,7 @@ module ActionPolicy
|
|
|
119
119
|
|
|
120
120
|
# Some lines should not be evaled
|
|
121
121
|
def ignore_exp?(exp)
|
|
122
|
-
PrettyPrint.ignore_expressions.any? { exp.match?(
|
|
122
|
+
PrettyPrint.ignore_expressions.any? { exp.match?(it) }
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
data/lib/action_policy.rb
CHANGED