action_policy 0.5.0 → 0.5.1

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
  3. data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
  4. data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
  5. data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
  6. data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
  7. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
  8. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
  9. data/lib/.rbnext/3.0/action_policy/behaviour.rb +115 -0
  10. data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +62 -0
  11. data/lib/.rbnext/3.0/action_policy/behaviours/scoping.rb +35 -0
  12. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
  13. data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
  14. data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
  15. data/lib/.rbnext/3.0/action_policy/policy/authorization.rb +87 -0
  16. data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
  17. data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
  18. data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
  19. data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
  20. data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
  21. data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +210 -0
  22. data/lib/.rbnext/3.0/action_policy/policy/scoping.rb +160 -0
  23. data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
  24. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
  25. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
  26. data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
  27. data/lib/action_policy/version.rb +1 -1
  28. metadata +27 -2
@@ -0,0 +1,89 @@
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
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 match(_expected, actual)
36
+ raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
37
+
38
+ @policy ||= ::ActionPolicy.lookup(target)
39
+
40
+ begin
41
+ ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
42
+ rescue ActionPolicy::Unauthorized
43
+ # we don't want to care about authorization result
44
+ end
45
+
46
+ @actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
47
+
48
+ actual_calls.any? { _1.matches?(policy, rule, target) }
49
+ end
50
+
51
+ def does_not_match?(*)
52
+ raise "This matcher doesn't support negation"
53
+ end
54
+
55
+ def supports_block_expectations?() ; true; end
56
+
57
+ def failure_message
58
+ "expected #{formatted_record} " \
59
+ "to be authorized with #{policy}##{rule}, " \
60
+ "but #{actual_calls_message}"
61
+ end
62
+
63
+ def actual_calls_message
64
+ if actual_calls.empty?
65
+ "no authorization calls have been made"
66
+ else
67
+ "the following calls were encountered:\n" \
68
+ "#{formatted_calls}"
69
+ end
70
+ end
71
+
72
+ def formatted_calls
73
+ actual_calls.map do
74
+ " - #{_1.inspect}"
75
+ end.join("\n")
76
+ end
77
+
78
+ def formatted_record(record = target) ; ::RSpec::Support::ObjectFormatter.format(record); end
79
+ end
80
+ end
81
+ end
82
+
83
+ RSpec.configure do |config|
84
+ config.include(Module.new do
85
+ def be_authorized_to(rule, target)
86
+ ActionPolicy::RSpec::BeAuthorizedTo.new(rule, target)
87
+ end
88
+ end)
89
+ end
@@ -0,0 +1,124 @@
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
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 match(_expected, actual)
53
+ raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
54
+
55
+ ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
56
+
57
+ @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
58
+
59
+ matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
60
+
61
+ return false if matching_scopes.empty?
62
+
63
+ return true unless target_expectations
64
+
65
+ if matching_scopes.size > 1
66
+ raise "Too many matching scopings (#{matching_scopes.size}), " \
67
+ "you can run `.with_target` only when there is the only one match"
68
+ end
69
+
70
+ target_expectations.call(matching_scopes.first.target)
71
+ true
72
+ end
73
+
74
+ def does_not_match?(*)
75
+ raise "This matcher doesn't support negation"
76
+ end
77
+
78
+ def supports_block_expectations?() ; true; end
79
+
80
+ def failure_message
81
+ "expected a scoping named :#{name} for type :#{type} " \
82
+ "#{scope_options_message} " \
83
+ "from #{policy} to have been applied, " \
84
+ "but #{actual_scopes_message}"
85
+ end
86
+
87
+ def scope_options_message
88
+ if scope_options
89
+ if defined?(::RSpec::Matchers::Composable) &&
90
+ scope_options.is_a?(::RSpec::Matchers::Composable)
91
+ "with scope options #{scope_options.description}"
92
+ else
93
+ "with scope options #{scope_options}"
94
+ end
95
+ else
96
+ "without scope options"
97
+ end
98
+ end
99
+
100
+ def actual_scopes_message
101
+ if actual_scopes.empty?
102
+ "no scopings have been made"
103
+ else
104
+ "the following scopings were encountered:\n" \
105
+ "#{formatted_scopings}"
106
+ end
107
+ end
108
+
109
+ def formatted_scopings
110
+ actual_scopes.map do
111
+ " - #{_1.inspect}"
112
+ end.join("\n")
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ RSpec.configure do |config|
119
+ config.include(Module.new do
120
+ def have_authorized_scope(type)
121
+ ActionPolicy::RSpec::HaveAuthorizedScope.new(type)
122
+ end
123
+ end)
124
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ old_verbose = $VERBOSE
4
+
5
+ begin
6
+ require "method_source"
7
+ # Ignore parse warnings when patch
8
+ # Ruby version mismatches
9
+ $VERBOSE = nil
10
+ require "parser/current"
11
+ require "unparser"
12
+ rescue LoadError
13
+ # do nothing
14
+ ensure
15
+ $VERBOSE = old_verbose
16
+ end
17
+
18
+ module ActionPolicy
19
+ using RubyNext
20
+
21
+ # Takes the object and a method name,
22
+ # and returns the "annotated" source code for the method:
23
+ # code is split into parts by logical operators and each
24
+ # part is evaluated separately.
25
+ #
26
+ # Example:
27
+ #
28
+ # class MyClass
29
+ # def access?
30
+ # admin? && access_feed?
31
+ # end
32
+ # end
33
+ #
34
+ # puts PrettyPrint.format_method(MyClass.new, :access?)
35
+ #
36
+ # #=> MyClass#access?
37
+ # #=> ↳ admin? #=> false
38
+ # #=> AND
39
+ # #=> access_feed? #=> true
40
+ module PrettyPrint
41
+ TRUE = "\e[32mtrue\e[0m"
42
+ FALSE = "\e[31mfalse\e[0m"
43
+
44
+ class Visitor
45
+ attr_reader :lines, :object
46
+ attr_accessor :indent
47
+
48
+ def initialize(object)
49
+ @object = object
50
+ end
51
+
52
+ def collect(ast)
53
+ @lines = []
54
+ @indent = 0
55
+
56
+ visit_node(ast)
57
+
58
+ lines.join("\n")
59
+ end
60
+
61
+ def visit_node(ast)
62
+ if respond_to?("visit_#{ast.type}")
63
+ send("visit_#{ast.type}", ast)
64
+ else
65
+ visit_missing ast
66
+ end
67
+ end
68
+
69
+ def expression_with_result(sexp)
70
+ expression = Unparser.unparse(sexp)
71
+ "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
72
+ end
73
+
74
+ def eval_exp(exp)
75
+ return "<skipped>" if ignore_exp?(exp)
76
+ object.instance_eval(exp)
77
+ rescue => e
78
+ "Failed: #{e.message}"
79
+ end
80
+
81
+ def visit_and(ast)
82
+ visit_node(ast.children[0])
83
+ lines << indented("AND")
84
+ visit_node(ast.children[1])
85
+ end
86
+
87
+ def visit_or(ast)
88
+ visit_node(ast.children[0])
89
+ lines << indented("OR")
90
+ visit_node(ast.children[1])
91
+ end
92
+
93
+ def visit_begin(ast)
94
+ # Parens
95
+ if ast.children.size == 1
96
+ lines << indented("(")
97
+ self.indent += 2
98
+ visit_node(ast.children[0])
99
+ self.indent -= 2
100
+ lines << indented(")")
101
+ else
102
+ # Multiple expressions
103
+ ast.children.each do |node|
104
+ visit_node(node)
105
+ # restore indent after each expression
106
+ self.indent -= 2
107
+ end
108
+ end
109
+ end
110
+
111
+ def visit_missing(ast)
112
+ lines << indented(expression_with_result(ast))
113
+ end
114
+
115
+ def indented(str)
116
+ "#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
117
+ # increase indent after the first expression
118
+ self.indent += 2 if indent.zero?
119
+ end
120
+ end
121
+
122
+ # Some lines should not be evaled
123
+ def ignore_exp?(exp)
124
+ PrettyPrint.ignore_expressions.any? { exp.match?(_1) }
125
+ end
126
+ end
127
+
128
+ class << self
129
+ attr_accessor :ignore_expressions
130
+
131
+ if defined?(::Unparser) && defined?(::MethodSource)
132
+ def available?() ; true; end
133
+
134
+ def print_method(object, method_name)
135
+ ast = object.method(method_name).source.then(&Unparser.method(:parse))
136
+ # outer node is a method definition itself
137
+ body = ast.children[2]
138
+
139
+ Visitor.new(object).collect(body)
140
+ end
141
+ else
142
+ def available?() ; false; end
143
+
144
+ def print_method(_, _) ; ""; end
145
+ end
146
+
147
+ def colorize(val)
148
+ return val unless $stdout.isatty
149
+ return TRUE if val.eql?(true)
150
+ return FALSE if val.eql?(false)
151
+ val
152
+ end
153
+ end
154
+
155
+ self.ignore_expressions = [
156
+ /^\s*binding\.(pry|irb)\s*$/s
157
+ ]
158
+ end
159
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ # Adds `suggest` method which uses did_you_mean
5
+ # to generate a suggestion message
6
+ module SuggestMessage
7
+ if defined?(::DidYouMean::SpellChecker)
8
+ def suggest(needle, heystack)
9
+ suggestion = ::DidYouMean::SpellChecker.new(
10
+ dictionary: heystack
11
+ ).correct(needle).first
12
+
13
+ suggestion ? "\nDid you mean? #{suggestion}" : ""
14
+ end
15
+ else
16
+ def suggest(*) ; ""; end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
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.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-29 00:00:00.000000000 Z
11
+ date: 2020-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -132,6 +132,31 @@ files:
132
132
  - CHANGELOG.md
133
133
  - LICENSE.txt
134
134
  - README.md
135
+ - lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb
136
+ - lib/.rbnext/2.7/action_policy/i18n.rb
137
+ - lib/.rbnext/2.7/action_policy/policy/cache.rb
138
+ - lib/.rbnext/2.7/action_policy/policy/pre_check.rb
139
+ - lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb
140
+ - lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb
141
+ - lib/.rbnext/2.7/action_policy/utils/pretty_print.rb
142
+ - lib/.rbnext/3.0/action_policy/behaviour.rb
143
+ - lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb
144
+ - lib/.rbnext/3.0/action_policy/behaviours/scoping.rb
145
+ - lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb
146
+ - lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb
147
+ - lib/.rbnext/3.0/action_policy/policy/aliases.rb
148
+ - lib/.rbnext/3.0/action_policy/policy/authorization.rb
149
+ - lib/.rbnext/3.0/action_policy/policy/cache.rb
150
+ - lib/.rbnext/3.0/action_policy/policy/core.rb
151
+ - lib/.rbnext/3.0/action_policy/policy/defaults.rb
152
+ - lib/.rbnext/3.0/action_policy/policy/execution_result.rb
153
+ - lib/.rbnext/3.0/action_policy/policy/pre_check.rb
154
+ - lib/.rbnext/3.0/action_policy/policy/reasons.rb
155
+ - lib/.rbnext/3.0/action_policy/policy/scoping.rb
156
+ - lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb
157
+ - lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb
158
+ - lib/.rbnext/3.0/action_policy/utils/pretty_print.rb
159
+ - lib/.rbnext/3.0/action_policy/utils/suggest_message.rb
135
160
  - lib/action_policy.rb
136
161
  - lib/action_policy/authorizer.rb
137
162
  - lib/action_policy/base.rb