action_policy 0.5.0 → 0.5.1

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