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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +5 -3
  4. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +8 -2
  5. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +25 -29
  6. data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +70 -0
  7. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +1 -1
  8. data/lib/.rbnext/3.0/action_policy/policy/core.rb +1 -1
  9. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +8 -2
  10. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +25 -29
  11. data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +5 -3
  12. data/lib/.rbnext/3.1/action_policy/policy/authorization.rb +1 -0
  13. data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +5 -3
  14. data/lib/.rbnext/3.2/action_policy/policy/core.rb +1 -1
  15. data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +8 -2
  16. data/lib/action_policy/behaviours/memoized.rb +1 -1
  17. data/lib/action_policy/behaviours/policy_for.rb +5 -3
  18. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  19. data/lib/action_policy/policy/authorization.rb +1 -0
  20. data/lib/action_policy/policy/core.rb +1 -1
  21. data/lib/action_policy/policy/scoping.rb +12 -1
  22. data/lib/action_policy/rails/controller.rb +1 -1
  23. data/lib/action_policy/rspec/have_authorized_scope.rb +8 -2
  24. data/lib/action_policy/test_helper.rb +5 -2
  25. data/lib/action_policy/testing.rb +17 -10
  26. data/lib/action_policy/utils/pretty_print.rb +25 -29
  27. data/lib/action_policy/version.rb +1 -1
  28. metadata +8 -8
  29. 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: 1e78f24a8faa881e8cad62f6bb4fc58921668fb7d41ac933c7fb1934117acc7d
4
- data.tar.gz: 39f55de2e98b9a1d67519df40452349a2007e8d7168039438d2c8469cc7779e7
3
+ metadata.gz: 0e9a1595099a6b87f22ec08fbd6676cd73cea94672c8cc9276525e82e9ba8c1a
4
+ data.tar.gz: bc743bbc45679d6583d3549cad77fc37f4004d051ef54d4cf8baa252730b0621
5
5
  SHA512:
6
- metadata.gz: 66b0de36683bc63ce349b0fba097585b7fcb4d7d65145da53c26cd5d6f0c798ef76c447d75a987c1f16110d3d4bb379153407ef79b531613cc6cfa8e55171570
7
- data.tar.gz: b7eadbbd0020e60addce888bf8287722c8bafd32a8b35ff2c66d8ea51eaa3b9c6f406e5214fdcddda4e7d4d527d12f08be3f86eac64d28db3b62b7976de28f17
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 ? authorization_context.merge(context) : authorization_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
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
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 "parser/current"
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 = Unparser.unparse(sexp)
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 visit_and(ast)
82
- visit_node(ast.children[0])
82
+ def visit_and_node(ast)
83
+ visit_node(ast.left)
83
84
  lines << indented("AND")
84
- visit_node(ast.children[1])
85
+ visit_node(ast.right)
85
86
  end
86
87
 
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
88
+ def visit_or_node(ast)
89
+ visit_node(ast.left)
89
90
  lines << indented("OR")
90
- visit_node(ast.children[1])
91
+ visit_node(ast.right)
91
92
  end
92
93
 
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])
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?(::Unparser) && defined?(::MethodSource)
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.then(&Unparser.method(:parse))
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(body)
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
@@ -45,7 +45,7 @@ module ActionPolicy
45
45
 
46
46
  module InstanceMethods # :nodoc:
47
47
  def policy_for(record:, **opts)
48
- __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
48
+ __policy_thread_memoize__(record, **opts) { super }
49
49
  end
50
50
  end
51
51
 
@@ -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 "unparser" gems to be installed.
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 "parser/current"
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 = Unparser.unparse(sexp)
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 visit_and(ast)
82
- visit_node(ast.children[0])
82
+ def visit_and_node(ast)
83
+ visit_node(ast.left)
83
84
  lines << indented("AND")
84
- visit_node(ast.children[1])
85
+ visit_node(ast.right)
85
86
  end
86
87
 
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
88
+ def visit_or_node(ast)
89
+ visit_node(ast.left)
89
90
  lines << indented("OR")
90
- visit_node(ast.children[1])
91
+ visit_node(ast.right)
91
92
  end
92
93
 
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])
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?(::Unparser) && defined?(::MethodSource)
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.then(&Unparser.method(:parse))
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(body)
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 ? authorization_context.merge(context) : authorization_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
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
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
@@ -42,6 +42,7 @@ module ActionPolicy
42
42
  end
43
43
 
44
44
  attr_reader :authorization_context
45
+ alias_method :build_authorization_context, :authorization_context
45
46
 
46
47
  def initialize(record = nil, **params)
47
48
  super(record)
@@ -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 ? authorization_context.merge(context) : authorization_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
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
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 "unparser" gems to be installed.
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
@@ -29,7 +29,7 @@ module ActionPolicy
29
29
 
30
30
  module InstanceMethods # :nodoc:
31
31
  def policy_for(record:, **opts)
32
- __policy_memoize__(record, **opts) { super(record: record, **opts) }
32
+ __policy_memoize__(record, **opts) { super }
33
33
  end
34
34
  end
35
35
 
@@ -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 ? authorization_context.merge(context) : authorization_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
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
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
@@ -45,7 +45,7 @@ module ActionPolicy
45
45
 
46
46
  module InstanceMethods # :nodoc:
47
47
  def policy_for(record:, **opts)
48
- __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
48
+ __policy_thread_memoize__(record, **opts) { super }
49
49
  end
50
50
  end
51
51
 
@@ -42,6 +42,7 @@ module ActionPolicy
42
42
  end
43
43
 
44
44
  attr_reader :authorization_context
45
+ alias_method :build_authorization_context, :authorization_context
45
46
 
46
47
  def initialize(record = nil, **params)
47
48
  super(record)
@@ -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 "unparser" gems to be installed.
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
@@ -48,7 +48,7 @@ module ActionPolicy
48
48
  def authorize!(record = :__undef__, to: nil, **options)
49
49
  to ||= :"#{action_name}?"
50
50
 
51
- super(record, to: to, **options)
51
+ super
52
52
 
53
53
  self.authorize_count += 1
54
54
  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 "parser/current"
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 = Unparser.unparse(sexp)
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 visit_and(ast)
82
- visit_node(ast.children[0])
82
+ def visit_and_node(ast)
83
+ visit_node(ast.left)
83
84
  lines << indented("AND")
84
- visit_node(ast.children[1])
85
+ visit_node(ast.right)
85
86
  end
86
87
 
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
88
+ def visit_or_node(ast)
89
+ visit_node(ast.left)
89
90
  lines << indented("OR")
90
- visit_node(ast.children[1])
91
+ visit_node(ast.right)
91
92
  end
92
93
 
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])
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?(::Unparser) && defined?(::MethodSource)
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.then(&Unparser.:parse)
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(body)
135
+ Visitor.new(object).collect(ast)
140
136
  end
141
137
  else
142
138
  def available?() = false
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.6.8"
4
+ VERSION = "0.7.0"
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.6.8
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-01-17 00:00:00.000000000 Z
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.6.0
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.20
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