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,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| _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 |_1|
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? { |_1| 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,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/behaviours/policy_for"
4
+ require "action_policy/behaviours/scoping"
5
+ require "action_policy/behaviours/memoized"
6
+ require "action_policy/behaviours/thread_memoized"
7
+ require "action_policy/behaviours/namespaced"
8
+
9
+ require "action_policy/authorizer"
10
+
11
+ module ActionPolicy
12
+ # Provides `authorize!` and `allowed_to?` methods and
13
+ # `authorize` class method to define authorization context.
14
+ #
15
+ # Could be included anywhere to perform authorization.
16
+ module Behaviour
17
+ include ActionPolicy::Behaviours::PolicyFor
18
+ include ActionPolicy::Behaviours::Scoping
19
+
20
+ def self.included(base)
21
+ # Handle ActiveSupport::Concern differently
22
+ if base.respond_to?(:class_methods)
23
+ base.class_methods do
24
+ include ClassMethods
25
+ end
26
+ else
27
+ base.extend ClassMethods
28
+ end
29
+ end
30
+
31
+ # Authorize action against a policy.
32
+ #
33
+ # Policy is inferred from record
34
+ # (unless explicitly specified through `with` option).
35
+ #
36
+ # Raises `ActionPolicy::Unauthorized` if check failed.
37
+ def authorize!(record = :__undef__, to:, **options)
38
+ policy = lookup_authorization_policy(record, **options)
39
+
40
+ Authorizer.call(policy, authorization_rule_for(policy, to))
41
+ end
42
+
43
+ # Checks that an activity is allowed for the current context (e.g. user).
44
+ #
45
+ # Returns true of false.
46
+ def allowed_to?(rule, record = :__undef__, **options)
47
+ policy = lookup_authorization_policy(record, **options)
48
+
49
+ policy.apply(authorization_rule_for(policy, rule))
50
+ end
51
+
52
+ # Returns the authorization result object after applying a specified rule to a record.
53
+ def allowance_to(rule, record = :__undef__, **options)
54
+ policy = lookup_authorization_policy(record, **options)
55
+
56
+ policy.apply(authorization_rule_for(policy, rule))
57
+ policy.result
58
+ end
59
+
60
+ def authorization_context
61
+ return @__authorization_context if
62
+ instance_variable_defined?(:@__authorization_context)
63
+
64
+ @__authorization_context = self.class.authorization_targets
65
+ .each_with_object({}) do |(key, meth), obj|
66
+ obj[key] = send(meth)
67
+ end
68
+ end
69
+
70
+ # Check that rule is defined for policy,
71
+ # otherwise fallback to :manage? rule.
72
+ def authorization_rule_for(policy, rule)
73
+ policy.resolve_rule(rule)
74
+ end
75
+
76
+ def lookup_authorization_policy(record, **options) # :nodoc:
77
+ record = implicit_authorization_target! if record == :__undef__
78
+ raise ArgumentError, "Record must be specified" if record.nil?
79
+
80
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
81
+
82
+ policy_for(record: record, **options)
83
+ end
84
+
85
+ module ClassMethods # :nodoc:
86
+ # Configure authorization context.
87
+ #
88
+ # For example:
89
+ #
90
+ # class ApplicationController < ActionController::Base
91
+ # # Pass the value of `current_user` to authorization as `user`
92
+ # authorize :user, through: :current_user
93
+ # end
94
+ #
95
+ # # Assuming that in your ApplicationPolicy
96
+ # class ApplicationPolicy < ActionPolicy::Base
97
+ # authorize :user
98
+ # end
99
+ def authorize(key, through: nil)
100
+ meth = through || key
101
+ authorization_targets[key] = meth
102
+ end
103
+
104
+ def authorization_targets
105
+ return @authorization_targets if instance_variable_defined?(:@authorization_targets)
106
+
107
+ @authorization_targets = if superclass.respond_to?(:authorization_targets)
108
+ superclass.authorization_targets.dup
109
+ else
110
+ {}
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,62 @@
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: authorization_context, allow_nil: false, default: default_authorization_policy_class)
12
+ policy_class = with || ::ActionPolicy.lookup(
13
+ record,
14
+ **{namespace: namespace, context: context, allow_nil: allow_nil, default: default}
15
+ )
16
+ policy_class&.new(record, **context)
17
+ end
18
+
19
+ def authorization_context
20
+ raise NotImplementedError, "Please, define `authorization_context` method!"
21
+ end
22
+
23
+ def authorization_namespace
24
+ # override to provide specific authorization namespace
25
+ end
26
+
27
+ def default_authorization_policy_class
28
+ # override to provide a policy class use when no policy found
29
+ end
30
+
31
+ # Override this method to provide implicit authorization target
32
+ # that would be used in case `record` is not specified in
33
+ # `authorize!` and `allowed_to?` call.
34
+ #
35
+ # It is also used to infer a policy for scoping (in `authorized_scope` method).
36
+ def implicit_authorization_target
37
+ # no-op
38
+ end
39
+
40
+ # Return implicit authorization target or raises an exception if it's nil
41
+ def implicit_authorization_target!
42
+ implicit_authorization_target || raise(
43
+ NotFound,
44
+ [
45
+ self,
46
+ "Couldn't find implicit authorization target " \
47
+ "for #{self.class}. " \
48
+ "Please, provide policy class explicitly using `with` option or " \
49
+ "define the `implicit_authorization_target` method."
50
+ ]
51
+ )
52
+ end
53
+
54
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
55
+ record_key = record._policy_cache_key(use_object_id: true)
56
+ context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
57
+
58
+ "#{namespace}/#{with}/#{context_key}/#{record_key}"
59
+ end
60
+ end
61
+ end
62
+ end