action_policy 0.6.7 → 0.6.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE.txt +1 -1
  4. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +1 -1
  5. data/lib/.rbnext/2.7/action_policy/rails/scope_matchers/action_controller_params.rb +5 -3
  6. data/lib/.rbnext/2.7/action_policy/rails/scope_matchers/active_record.rb +13 -11
  7. data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +1 -1
  8. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +1 -1
  9. data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +10 -10
  10. data/lib/.rbnext/3.0/action_policy/policy/core.rb +1 -1
  11. data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +1 -1
  12. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +1 -1
  13. data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +1 -1
  14. data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +1 -1
  15. data/lib/.rbnext/3.1/action_policy/ext/policy_cache_key.rb +10 -10
  16. data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +68 -0
  17. data/lib/.rbnext/3.2/action_policy/ext/policy_cache_key.rb +72 -0
  18. data/lib/.rbnext/3.2/action_policy/lookup_chain.rb +145 -0
  19. data/lib/.rbnext/3.2/action_policy/policy/core.rb +168 -0
  20. data/lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb +96 -0
  21. data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +124 -0
  22. data/lib/.rbnext/3.2/action_policy/utils/suggest_message.rb +19 -0
  23. data/lib/action_policy/rails/scope_matchers/action_controller_params.rb +5 -3
  24. data/lib/action_policy/rails/scope_matchers/active_record.rb +13 -11
  25. data/lib/action_policy/railtie.rb +4 -15
  26. data/lib/action_policy/version.rb +1 -1
  27. metadata +12 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df57cf926c648dc59ed1a1afc44b352eb998f5bf03a9405127105611e641ffdb
4
- data.tar.gz: 51a8e81c6479bf06894da3a2f8fa83c98ed25da678e73bf2462f029a75c55ec4
3
+ metadata.gz: 1e78f24a8faa881e8cad62f6bb4fc58921668fb7d41ac933c7fb1934117acc7d
4
+ data.tar.gz: 39f55de2e98b9a1d67519df40452349a2007e8d7168039438d2c8469cc7779e7
5
5
  SHA512:
6
- metadata.gz: 6c55e23ffeff3a1d9800512759105e509abb6b51dabee1a984c56f04daa04e661ef37e056e2fee64b86a17bff8fab8167b557d8f5fe6bd9118febf0b116f7b3a
7
- data.tar.gz: 8c1459872bb8daa8bca216c1ee342b9c8c4b4b6fb5654301765ec4d2c7f8ff6612ab784269a89920c29f575ee9ecd79b2a470a415f92246db70c0b9d7a3631b1
6
+ metadata.gz: 66b0de36683bc63ce349b0fba097585b7fcb4d7d65145da53c26cd5d6f0c798ef76c447d75a987c1f16110d3d4bb379153407ef79b531613cc6cfa8e55171570
7
+ data.tar.gz: b7eadbbd0020e60addce888bf8287722c8bafd32a8b35ff2c66d8ea51eaa3b9c6f406e5214fdcddda4e7d4d527d12f08be3f86eac64d28db3b62b7976de28f17
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.6.8 (2024-01-17)
6
+
7
+ - Do not preload Rails base classes, use load hooks everywhere. ([@palkan][])
8
+
5
9
  ## 0.6.7 (2023-09-13)
6
10
 
7
11
  - Fix loading Rails extensions during eager load. ([@palkan][])
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018-2023 Vladimir Dementyev
3
+ Copyright (c) 2018-2024 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -57,7 +57,7 @@ module ActionPolicy
57
57
  )
58
58
  end
59
59
 
60
- def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
60
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
61
61
  record_key = record._policy_cache_key(use_object_id: true)
62
62
  context_key = context.values.map { |_1| _1._policy_cache_key(use_object_id: true) }.join(".")
63
63
 
@@ -12,8 +12,10 @@ module ActionPolicy
12
12
  end
13
13
  end
14
14
 
15
- # Register params scope matcher
16
- ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
17
-
18
15
  # Add alias to base policy
19
16
  ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActionControllerParams
17
+
18
+ ActiveSupport.on_load(:action_controller) do
19
+ # Register params scope matcher
20
+ ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
21
+ end
@@ -12,18 +12,20 @@ module ActionPolicy
12
12
  end
13
13
  end
14
14
 
15
- # Register relation scope matcher
16
- ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
17
-
18
15
  # Add alias to base policy
19
16
  ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActiveRecord
20
17
 
21
- ActiveRecord::Relation.include(Module.new do
22
- def policy_name
23
- if model.respond_to?(:policy_name)
24
- model.policy_name.to_s
25
- else
26
- "#{model}Policy"
18
+ ActiveSupport.on_load(:active_record) do
19
+ # Register relation scope matcher
20
+ ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
21
+
22
+ ActiveRecord::Relation.include(Module.new do
23
+ def policy_name
24
+ if model.respond_to?(:policy_name)
25
+ model.policy_name.to_s
26
+ else
27
+ "#{model}Policy"
28
+ end
27
29
  end
28
- end
29
- end)
30
+ end)
31
+ end
@@ -54,7 +54,7 @@ module ActionPolicy
54
54
  actual_calls.any? { |_1| _1.matches?(policy, rule, target, context) }
55
55
  end
56
56
 
57
- def does_not_match?(*)
57
+ def does_not_match?(*__rest__)
58
58
  raise "This matcher doesn't support negation"
59
59
  end
60
60
 
@@ -71,7 +71,7 @@ module ActionPolicy
71
71
  true
72
72
  end
73
73
 
74
- def does_not_match?(*)
74
+ def does_not_match?(*__rest__)
75
75
  raise "This matcher doesn't support negation"
76
76
  end
77
77
 
@@ -27,45 +27,45 @@ module ActionPolicy
27
27
  end
28
28
 
29
29
  refine NilClass do
30
- def _policy_cache_key(*) ; ""; end
30
+ def _policy_cache_key(*__rest__) ; ""; end
31
31
  end
32
32
 
33
33
  refine TrueClass do
34
- def _policy_cache_key(*) ; "t"; end
34
+ def _policy_cache_key(*__rest__) ; "t"; end
35
35
  end
36
36
 
37
37
  refine FalseClass do
38
- def _policy_cache_key(*) ; "f"; end
38
+ def _policy_cache_key(*__rest__) ; "f"; end
39
39
  end
40
40
 
41
41
  refine String do
42
- def _policy_cache_key(*) ; self; end
42
+ def _policy_cache_key(*__rest__) ; self; end
43
43
  end
44
44
 
45
45
  refine Symbol do
46
- def _policy_cache_key(*) ; to_s; end
46
+ def _policy_cache_key(*__rest__) ; to_s; end
47
47
  end
48
48
 
49
49
  if RUBY_PLATFORM.match?(/java/i)
50
50
  refine Integer do
51
- def _policy_cache_key(*) ; to_s; end
51
+ def _policy_cache_key(*__rest__) ; to_s; end
52
52
  end
53
53
 
54
54
  refine Float do
55
- def _policy_cache_key(*) ; to_s; end
55
+ def _policy_cache_key(*__rest__) ; to_s; end
56
56
  end
57
57
  else
58
58
  refine Numeric do
59
- def _policy_cache_key(*) ; to_s; end
59
+ def _policy_cache_key(*__rest__) ; to_s; end
60
60
  end
61
61
  end
62
62
 
63
63
  refine Time do
64
- def _policy_cache_key(*) ; to_s; end
64
+ def _policy_cache_key(*__rest__) ; to_s; end
65
65
  end
66
66
 
67
67
  refine Module do
68
- def _policy_cache_key(*) ; name; end
68
+ def _policy_cache_key(*__rest__) ; name; end
69
69
  end
70
70
  end
71
71
  end
@@ -75,7 +75,7 @@ module ActionPolicy
75
75
  attr_reader :record, :result
76
76
 
77
77
  # NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
78
- def initialize(record = nil, *)
78
+ def initialize(record = nil, *__rest__)
79
79
  @record = record
80
80
  end
81
81
 
@@ -54,7 +54,7 @@ module ActionPolicy
54
54
  actual_calls.any? { _1.matches?(policy, rule, target, context) }
55
55
  end
56
56
 
57
- def does_not_match?(*)
57
+ def does_not_match?(*__rest__)
58
58
  raise "This matcher doesn't support negation"
59
59
  end
60
60
 
@@ -71,7 +71,7 @@ module ActionPolicy
71
71
  true
72
72
  end
73
73
 
74
- def does_not_match?(*)
74
+ def does_not_match?(*__rest__)
75
75
  raise "This matcher doesn't support negation"
76
76
  end
77
77
 
@@ -13,7 +13,7 @@ module ActionPolicy
13
13
  suggestion ? "\nDid you mean? #{suggestion}" : ""
14
14
  end
15
15
  else
16
- def suggest(*) ; ""; end
16
+ def suggest(*__rest__) ; ""; end
17
17
  end
18
18
  end
19
19
  end
@@ -57,7 +57,7 @@ module ActionPolicy
57
57
  )
58
58
  end
59
59
 
60
- def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
60
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
61
61
  record_key = record._policy_cache_key(use_object_id: true)
62
62
  context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
63
63
 
@@ -27,45 +27,45 @@ module ActionPolicy
27
27
  end
28
28
 
29
29
  refine NilClass do
30
- def _policy_cache_key(*) = ""
30
+ def _policy_cache_key(*__rest__) = ""
31
31
  end
32
32
 
33
33
  refine TrueClass do
34
- def _policy_cache_key(*) = "t"
34
+ def _policy_cache_key(*__rest__) = "t"
35
35
  end
36
36
 
37
37
  refine FalseClass do
38
- def _policy_cache_key(*) = "f"
38
+ def _policy_cache_key(*__rest__) = "f"
39
39
  end
40
40
 
41
41
  refine String do
42
- def _policy_cache_key(*) = self
42
+ def _policy_cache_key(*__rest__) = self
43
43
  end
44
44
 
45
45
  refine Symbol do
46
- def _policy_cache_key(*) = to_s
46
+ def _policy_cache_key(*__rest__) = to_s
47
47
  end
48
48
 
49
49
  if RUBY_PLATFORM.match?(/java/i)
50
50
  refine Integer do
51
- def _policy_cache_key(*) = to_s
51
+ def _policy_cache_key(*__rest__) = to_s
52
52
  end
53
53
 
54
54
  refine Float do
55
- def _policy_cache_key(*) = to_s
55
+ def _policy_cache_key(*__rest__) = to_s
56
56
  end
57
57
  else
58
58
  refine Numeric do
59
- def _policy_cache_key(*) = to_s
59
+ def _policy_cache_key(*__rest__) = to_s
60
60
  end
61
61
  end
62
62
 
63
63
  refine Time do
64
- def _policy_cache_key(*) = to_s
64
+ def _policy_cache_key(*__rest__) = to_s
65
65
  end
66
66
 
67
67
  refine Module do
68
- def _policy_cache_key(*) = name
68
+ def _policy_cache_key(*__rest__) = name
69
69
  end
70
70
  end
71
71
  end
@@ -0,0 +1,68 @@
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 ? authorization_context.merge(context) : authorization_context
13
+
14
+ policy_class = with || ::ActionPolicy.lookup(
15
+ record,
16
+ namespace:, context:, allow_nil:, default:, strict_namespace:
17
+ )
18
+ policy_class&.new(record, **context)
19
+ end
20
+
21
+ def authorization_context
22
+ Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
23
+ end
24
+
25
+ def authorization_namespace
26
+ # override to provide specific authorization namespace
27
+ end
28
+
29
+ def default_authorization_policy_class
30
+ # override to provide a policy class use when no policy found
31
+ end
32
+
33
+ def authorization_strict_namespace
34
+ # override to provide strict namespace lookup option
35
+ end
36
+
37
+ # Override this method to provide implicit authorization target
38
+ # that would be used in case `record` is not specified in
39
+ # `authorize!` and `allowed_to?` call.
40
+ #
41
+ # It is also used to infer a policy for scoping (in `authorized_scope` method).
42
+ def implicit_authorization_target
43
+ # no-op
44
+ end
45
+
46
+ # Return implicit authorization target or raises an exception if it's nil
47
+ def implicit_authorization_target!
48
+ implicit_authorization_target || Kernel.raise(
49
+ NotFound,
50
+ [
51
+ self,
52
+ "Couldn't find implicit authorization target " \
53
+ "for #{self.class}. " \
54
+ "Please, provide policy class explicitly using `with` option or " \
55
+ "define the `implicit_authorization_target` method."
56
+ ]
57
+ )
58
+ end
59
+
60
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
61
+ record_key = record._policy_cache_key(use_object_id: true)
62
+ context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
63
+
64
+ "#{namespace}/#{with}/#{context_key}/#{record_key}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Ext
5
+ # Adds #_policy_cache_key method to Object,
6
+ # which just call #policy_cache_key or #cache_key
7
+ # or #object_id (if `use_object_id` parameter is set to true).
8
+ #
9
+ # For other core classes returns string representation.
10
+ #
11
+ # Raises ArgumentError otherwise.
12
+ module PolicyCacheKey # :nodoc: all
13
+ module ObjectExt
14
+ def _policy_cache_key(use_object_id: false)
15
+ return policy_cache_key if respond_to?(:policy_cache_key)
16
+ return cache_key_with_version if respond_to?(:cache_key_with_version)
17
+ return cache_key if respond_to?(:cache_key)
18
+
19
+ return object_id.to_s if use_object_id == true
20
+
21
+ raise ArgumentError, "object is not cacheable"
22
+ end
23
+ end
24
+
25
+ refine Object do
26
+ import_methods ObjectExt
27
+ end
28
+
29
+ refine NilClass do
30
+ def _policy_cache_key(*__rest__) = ""
31
+ end
32
+
33
+ refine TrueClass do
34
+ def _policy_cache_key(*__rest__) = "t"
35
+ end
36
+
37
+ refine FalseClass do
38
+ def _policy_cache_key(*__rest__) = "f"
39
+ end
40
+
41
+ refine String do
42
+ def _policy_cache_key(*__rest__) = self
43
+ end
44
+
45
+ refine Symbol do
46
+ def _policy_cache_key(*__rest__) = to_s
47
+ end
48
+
49
+ if RUBY_PLATFORM.match?(/java/i)
50
+ refine Integer do
51
+ def _policy_cache_key(*__rest__) = to_s
52
+ end
53
+
54
+ refine Float do
55
+ def _policy_cache_key(*__rest__) = to_s
56
+ end
57
+ else
58
+ refine Numeric do
59
+ def _policy_cache_key(*__rest__) = to_s
60
+ end
61
+ end
62
+
63
+ refine Time do
64
+ def _policy_cache_key(*__rest__) = to_s
65
+ end
66
+
67
+ refine Module do
68
+ def _policy_cache_key(*__rest__) = name
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ # LookupChain contains _resolvers_ to determine a policy
5
+ # for a record (with additional options).
6
+ #
7
+ # You can modify the `LookupChain.chain` (for example, to add
8
+ # custom resolvers).
9
+ module LookupChain
10
+ unless "".respond_to?(:safe_constantize)
11
+ require "action_policy/ext/string_constantize"
12
+ using ActionPolicy::Ext::StringConstantize
13
+ end
14
+
15
+ require "action_policy/ext/symbol_camelize"
16
+ using ActionPolicy::Ext::SymbolCamelize
17
+
18
+ require "action_policy/ext/module_namespace"
19
+ using ActionPolicy::Ext::ModuleNamespace
20
+
21
+ # Cache namespace resolving result for policies.
22
+ # @see benchmarks/namespaced_lookup_cache.rb
23
+ class NamespaceCache
24
+ class << self
25
+ attr_reader :store
26
+
27
+ def put_if_absent(scope, namespace, policy)
28
+ local_store = store[scope][namespace]
29
+ return local_store[policy] if local_store[policy]
30
+ local_store[policy] ||= yield
31
+ end
32
+
33
+ def fetch(namespace, policy, strict:, &block)
34
+ return yield unless LookupChain.namespace_cache_enabled?
35
+
36
+ if strict
37
+ put_if_absent(:strict, namespace, policy, &block)
38
+ else
39
+ put_if_absent(:flexible, namespace, policy, &block)
40
+ end
41
+ end
42
+
43
+ def clear
44
+ hash = Hash.new { |h, k| h[k] = {} }
45
+ @store = {strict: hash, flexible: hash.clone}
46
+ end
47
+ end
48
+
49
+ clear
50
+ end
51
+
52
+ class << self
53
+ attr_accessor :chain, :namespace_cache_enabled
54
+
55
+ alias_method :namespace_cache_enabled?, :namespace_cache_enabled
56
+
57
+ def call(record, **opts)
58
+ chain.each do |probe|
59
+ val = probe.call(record, **opts)
60
+ return val unless val.nil?
61
+ end
62
+ nil
63
+ end
64
+
65
+ private
66
+
67
+ def lookup_within_namespace(policy_name, namespace, strict: false)
68
+ NamespaceCache.fetch(namespace&.name || "Kernel", policy_name, strict: strict) do
69
+ mod = namespace
70
+ policy_class = nil
71
+
72
+ loop do
73
+ policy_class = [mod&.name, policy_name].compact.join("::").safe_constantize
74
+ break policy_class if policy_class || mod.nil?
75
+
76
+ mod = mod.namespace
77
+ end
78
+
79
+ next policy_class if !strict || namespace.nil? || policy_class.nil?
80
+
81
+ # If we're in the strict mode and the namespace boundary is provided,
82
+ # we must check that the found policy satisfies it
83
+ policy_class if policy_class.name.start_with?("#{namespace.name}::")
84
+ end
85
+ end
86
+
87
+ def policy_class_name_for(record)
88
+ return record.policy_name.to_s if record.respond_to?(:policy_name)
89
+
90
+ record_class = record.is_a?(Module) ? record : record.class
91
+
92
+ if record_class.respond_to?(:policy_name)
93
+ record_class.policy_name.to_s
94
+ else
95
+ "#{record_class}Policy"
96
+ end
97
+ end
98
+ end
99
+
100
+ # Enable namespace cache by default or
101
+ # if RACK_ENV provided and equal to "production"
102
+ self.namespace_cache_enabled =
103
+ (!ENV["RACK_ENV"].nil?) ? ENV["RACK_ENV"] == "production" : true
104
+
105
+ # By self `policy_class` method
106
+ INSTANCE_POLICY_CLASS = ->(record, **__kwrest__) {
107
+ record.policy_class if record.respond_to?(:policy_class)
108
+ }
109
+
110
+ # By record's class `policy_class` method
111
+ CLASS_POLICY_CLASS = ->(record, **__kwrest__) {
112
+ record.class.policy_class if record.class.respond_to?(:policy_class)
113
+ }
114
+
115
+ # Infer from class name
116
+ INFER_FROM_CLASS = ->(record, namespace: nil, strict_namespace: false, **__kwrest__) {
117
+ policy_name = policy_class_name_for(record)
118
+ lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
119
+ }
120
+
121
+ # Infer from passed symbol
122
+ SYMBOL_LOOKUP = ->(record, namespace: nil, strict_namespace: false, **__kwrest__) {
123
+ next unless record.is_a?(Symbol)
124
+
125
+ policy_name = "#{record.camelize}Policy"
126
+ lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
127
+ }
128
+
129
+ # (Optional) Infer using String#classify if available
130
+ CLASSIFY_SYMBOL_LOOKUP = ->(record, namespace: nil, strict_namespace: false, **__kwrest__) {
131
+ next unless record.is_a?(Symbol)
132
+
133
+ policy_name = "#{record.to_s.classify}Policy"
134
+ lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
135
+ }
136
+
137
+ self.chain = [
138
+ SYMBOL_LOOKUP,
139
+ (CLASSIFY_SYMBOL_LOOKUP if String.method_defined?(:classify)),
140
+ INSTANCE_POLICY_CLASS,
141
+ CLASS_POLICY_CLASS,
142
+ INFER_FROM_CLASS
143
+ ].compact
144
+ end
145
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/behaviours/policy_for"
4
+ require "action_policy/policy/execution_result"
5
+ require "action_policy/utils/suggest_message"
6
+ require "action_policy/utils/pretty_print"
7
+
8
+ unless "".respond_to?(:underscore)
9
+ require "action_policy/ext/string_underscore"
10
+ using ActionPolicy::Ext::StringUnderscore
11
+ end
12
+
13
+ module ActionPolicy
14
+ using RubyNext
15
+
16
+ # Raised when `resolve_rule` failed to find an approriate
17
+ # policy rule method for the activity
18
+ class UnknownRule < Error
19
+ include ActionPolicy::SuggestMessage
20
+
21
+ attr_reader :policy, :rule, :message
22
+
23
+ def initialize(policy, rule)
24
+ @policy = policy.class
25
+ @rule = rule
26
+ @message = "Couldn't find rule '#{@rule}' for #{@policy}" \
27
+ "#{suggest(@rule, @policy.instance_methods - Object.instance_methods)}"
28
+ end
29
+ end
30
+
31
+ class NonPredicateRule < UnknownRule
32
+ def initialize(policy, rule)
33
+ @policy = policy.class
34
+ @rule = rule
35
+ @message = "The rule '#{@rule}' of '#{@policy}' must ends with ? (question mark)\nDid you mean? #{@rule}?"
36
+ end
37
+ end
38
+
39
+ module Policy
40
+ # Core policy API
41
+ module Core
42
+ class << self
43
+ def included(base)
44
+ base.extend ClassMethods
45
+
46
+ # Generate a new class for each _policy chain_
47
+ # in order to extend it independently
48
+ base.module_eval do
49
+ @result_class = Class.new(ExecutionResult)
50
+
51
+ # we need to make this class _named_,
52
+ # 'cause anonymous classes couldn't be marshalled
53
+ base.const_set(:APR, @result_class)
54
+ end
55
+ end
56
+ end
57
+
58
+ module ClassMethods # :nodoc:
59
+ attr_writer :identifier
60
+
61
+ def result_class
62
+ return @result_class if instance_variable_defined?(:@result_class)
63
+ @result_class = superclass.result_class
64
+ end
65
+
66
+ def identifier
67
+ return @identifier if instance_variable_defined?(:@identifier)
68
+
69
+ @identifier = name.sub(/Policy$/, "").underscore.to_sym
70
+ end
71
+ end
72
+
73
+ include ActionPolicy::Behaviours::PolicyFor
74
+
75
+ attr_reader :record, :result
76
+
77
+ # NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
78
+ def initialize(record = nil, *__rest__)
79
+ @record = record
80
+ end
81
+
82
+ # Returns a result of applying the specified rule (true of false).
83
+ # Unlike simply calling a predicate rule (`policy.manage?`),
84
+ # `apply` also calls pre-checks.
85
+ def apply(rule)
86
+ @result = self.class.result_class.new(self.class, rule)
87
+
88
+ catch :policy_fulfilled do
89
+ result.load __apply__(resolve_rule(rule))
90
+ end
91
+
92
+ result.value
93
+ end
94
+
95
+ def deny!
96
+ result&.load false
97
+ throw :policy_fulfilled
98
+ end
99
+
100
+ def allow!
101
+ result&.load true
102
+ throw :policy_fulfilled
103
+ end
104
+
105
+ # This method performs the rule call.
106
+ # Override or extend it to provide custom functionality
107
+ # (such as caching, pre checks, etc.)
108
+ def __apply__(rule) = public_send(rule)
109
+
110
+ # Wrap code that could modify result
111
+ # to prevent the current result modification
112
+ def with_clean_result # :nodoc:
113
+ was_result = @result
114
+ yield
115
+ @result
116
+ ensure
117
+ @result = was_result
118
+ end
119
+
120
+ # Returns a result of applying the specified rule to the specified record.
121
+ # Under the hood a policy class for record is resolved
122
+ # (unless it's explicitly set through `with` option).
123
+ #
124
+ # If record is `nil` then we uses the current policy.
125
+ def allowed_to?(rule, record = :__undef__, **options)
126
+ if (record == :__undef__ || record == self.record) && options.empty?
127
+ __apply__(resolve_rule(rule))
128
+ else
129
+ policy_for(record: record, **options).then do |policy|
130
+ policy.apply(policy.resolve_rule(rule))
131
+ end
132
+ end
133
+ end
134
+
135
+ # An alias for readability purposes
136
+ def check?(*args, **hargs) = allowed_to?(*args, **hargs)
137
+
138
+ # Returns a rule name (policy method name) for activity.
139
+ #
140
+ # By default, rule name is equal to activity name.
141
+ #
142
+ # Raises ActionPolicy::UnknownRule when rule is not found in policy.
143
+ def resolve_rule(activity)
144
+ raise UnknownRule.new(self, activity) unless
145
+ respond_to?(activity)
146
+ activity
147
+ end
148
+
149
+ # Return annotated source code for the rule
150
+ # NOTE: require "method_source" and "unparser" gems to be installed.
151
+ # Otherwise returns empty string.
152
+ def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
153
+
154
+ # Helper for printing the annotated rule source.
155
+ # Useful for debugging: type `pp :show?` within the context of the policy
156
+ # to preview the rule.
157
+ def pp(rule)
158
+ with_clean_result do
159
+ # We need result to exist for `allowed_to?` to work correctly
160
+ @result = self.class.result_class.new(self.class, rule)
161
+ header = "#{self.class.name}##{rule}"
162
+ source = inspect_rule(rule)
163
+ $stdout.puts "#{header}\n#{source}"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -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? { _1.matches?(policy, rule, target, context) }
55
+ end
56
+
57
+ def does_not_match?(*__rest__)
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
+ "#{context ? "and context #{context.inspect}, " : ""}" \
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
81
+ " - #{_1.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,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?(*__rest__)
75
+ raise "This matcher doesn't support negation"
76
+ end
77
+
78
+ def supports_block_expectations?() = true
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,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(*__rest__) = ""
17
+ end
18
+ end
19
+ end
@@ -12,8 +12,10 @@ module ActionPolicy
12
12
  end
13
13
  end
14
14
 
15
- # Register params scope matcher
16
- ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
17
-
18
15
  # Add alias to base policy
19
16
  ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActionControllerParams
17
+
18
+ ActiveSupport.on_load(:action_controller) do
19
+ # Register params scope matcher
20
+ ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
21
+ end
@@ -12,18 +12,20 @@ module ActionPolicy
12
12
  end
13
13
  end
14
14
 
15
- # Register relation scope matcher
16
- ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
17
-
18
15
  # Add alias to base policy
19
16
  ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActiveRecord
20
17
 
21
- ActiveRecord::Relation.include(Module.new do
22
- def policy_name
23
- if model.respond_to?(:policy_name)
24
- model.policy_name.to_s
25
- else
26
- "#{model}Policy"
18
+ ActiveSupport.on_load(:active_record) do
19
+ # Register relation scope matcher
20
+ ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
21
+
22
+ ActiveRecord::Relation.include(Module.new do
23
+ def policy_name
24
+ if model.respond_to?(:policy_name)
25
+ model.policy_name.to_s
26
+ else
27
+ "#{model}Policy"
28
+ end
27
29
  end
28
- end
29
- end)
30
+ end)
31
+ end
@@ -74,8 +74,6 @@ module ActionPolicy # :nodoc:
74
74
  app.config.action_policy.namespace_cache_enabled
75
75
 
76
76
  ActiveSupport.on_load(:action_controller) do
77
- require "action_policy/rails/scope_matchers/action_controller_params"
78
-
79
77
  next unless app.config.action_policy.auto_inject_into_controller
80
78
 
81
79
  ActionController::Base.include ActionPolicy::Controller
@@ -95,21 +93,12 @@ module ActionPolicy # :nodoc:
95
93
  ActionCable::Channel::Base.authorize :user, through: :current_user
96
94
  end
97
95
 
96
+ # Scope matchers
97
+ require "action_policy/rails/scope_matchers/action_controller_params"
98
+ require "action_policy/rails/scope_matchers/active_record"
99
+
98
100
  ActiveSupport.on_load(:active_record) do
99
101
  require "action_policy/rails/ext/active_record"
100
- require "action_policy/rails/scope_matchers/active_record"
101
- end
102
-
103
- # Trigger load hooks of the components that extend ActionPolicy itself
104
- # (e.g., scope matchers)
105
- begin
106
- ::ActionController::Base
107
- rescue NameError
108
- end
109
-
110
- begin
111
- ::ActiveRecord::Base
112
- rescue NameError
113
102
  end
114
103
  end
115
104
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.6.7"
4
+ VERSION = "0.6.8"
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.7
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-14 00:00:00.000000000 Z
11
+ date: 2024-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.14.0
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.14.0
26
+ version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: ammeter
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -162,6 +162,13 @@ files:
162
162
  - lib/.rbnext/3.1/action_policy/ext/module_namespace.rb
163
163
  - lib/.rbnext/3.1/action_policy/ext/policy_cache_key.rb
164
164
  - lib/.rbnext/3.1/action_policy/policy/authorization.rb
165
+ - lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb
166
+ - lib/.rbnext/3.2/action_policy/ext/policy_cache_key.rb
167
+ - lib/.rbnext/3.2/action_policy/lookup_chain.rb
168
+ - lib/.rbnext/3.2/action_policy/policy/core.rb
169
+ - lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb
170
+ - lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb
171
+ - lib/.rbnext/3.2/action_policy/utils/suggest_message.rb
165
172
  - lib/action_policy.rb
166
173
  - lib/action_policy/authorizer.rb
167
174
  - lib/action_policy/base.rb
@@ -242,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
249
  - !ruby/object:Gem::Version
243
250
  version: '0'
244
251
  requirements: []
245
- rubygems_version: 3.4.8
252
+ rubygems_version: 3.4.20
246
253
  signing_key:
247
254
  specification_version: 4
248
255
  summary: Authorization framework for Ruby/Rails application