action_policy 0.6.7 → 0.6.8

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 (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