action_policy 0.7.1 → 0.7.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a2cec9201751bbc94bfa0875d8b4ed69500cdeea4ab4a1d9717a1786b3adba8
4
- data.tar.gz: 4b0be37589400d6491bf4e7b5c7a6e2e46b5e71629f9c6b93d28c59f58b0d4ec
3
+ metadata.gz: 21363e4337fbe0caea24309750d79d94bb9fa6c4ae0a9d0697b0e686519fa763
4
+ data.tar.gz: 8ac572534b621240640723ea5c6ec942a73b91e14f2d40e534889c2dc7ba9251
5
5
  SHA512:
6
- metadata.gz: 3751003181c14a8f2f71889525597d7b53cd2553cc52e955bbd668891f27ae34ec684fce36b1118b57c1527f78cdf6dca98b90b1dec7efc6cd20e59dc9f18fde
7
- data.tar.gz: c6d0b96504ac2f70264b12df798cbbf4fc953b139666522e5dad8b6b3c5fd8702d522d21ed35c6023443b9ba9872720499931b3996e64e3d5652b600eb23e2a0
6
+ metadata.gz: c1dd33d739cfdf1143b7cc2402c2e8bf7ebf1f1613fa7c650c72fddf0bd4fd4b5c924c920ff143ad582ab68524836f0d0c32641372c5999fdd95f6d9113078dc
7
+ data.tar.gz: 5fb9dbd9b1f19fb6bed19ddbeb99be9bd35495b2d0f89bd3fb0cd8fed59a0513c4a627fdd25291b56dcfc7cf9b60f64d351bb2b658d97351f9a7b5db2d1c4128
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.7.3 (2024-12-18)
6
+
7
+ - Fix keeping the result object in concurrent (Fiber-ed) execution environments. ([@palkan][])
8
+
9
+ ## 0.7.2 (2024-11-21)
10
+
11
+ - Fix missing details in deny! message interpolation. ([@palkan][])
12
+
13
+ - Fix implicit authorization target in anonymous controllers. ([@palkan][])
14
+
15
+ - Improve default `ActionPolicy::Unauthorized` error message. ([@Spone][])
16
+
17
+ Before: `Not Authorized` / After: `Not authorized: UserPolicy#create? returns false`
18
+
5
19
  ## 0.7.1 (2024-07-25)
6
20
 
7
21
  - Support passing scope options to callable scope objects. ([@palkan][])
@@ -524,3 +538,4 @@ This value is now stored in a cache (if any) instead of just the call result (`t
524
538
  [@tomdalling]: https://github.com/tomdalling
525
539
  [@matsales28]: https://github.com/matsales28
526
540
  [@killondark]: https://github.com/killondark
541
+ [@Spone]: https://github.com/Spone
@@ -50,18 +50,18 @@ module ActionPolicy # :nodoc:
50
50
  key = rule_cache_key(rule)
51
51
 
52
52
  ActionPolicy.cache_store.then do |store|
53
- @result = store.read(key)
53
+ result = store.read(key)
54
54
  unless result.nil?
55
55
  result.cached!
56
- next result.value
56
+ next result
57
+ end
58
+ yield.tap do |result|
59
+ store.write(key, result, options)
57
60
  end
58
- yield
59
- store.write(key, result, options)
60
- result.value
61
61
  end
62
62
  end
63
63
 
64
- def apply(rule)
64
+ def apply_r(rule)
65
65
  return super if ActionPolicy.cache_store.nil? ||
66
66
  !self.class.cached_rules.key?(rule)
67
67
 
@@ -50,18 +50,18 @@ module ActionPolicy # :nodoc:
50
50
  key = rule_cache_key(rule)
51
51
 
52
52
  ActionPolicy.cache_store.then do |store|
53
- @result = store.read(key)
53
+ result = store.read(key)
54
54
  unless result.nil?
55
55
  result.cached!
56
- next result.value
56
+ next result
57
+ end
58
+ yield.tap do |result|
59
+ store.write(key, result, options)
57
60
  end
58
- yield
59
- store.write(key, result, options)
60
- result.value
61
61
  end
62
62
  end
63
63
 
64
- def apply(rule)
64
+ def apply_r(rule)
65
65
  return super if ActionPolicy.cache_store.nil? ||
66
66
  !self.class.cached_rules.key?(rule)
67
67
 
@@ -72,7 +72,7 @@ module ActionPolicy
72
72
 
73
73
  include ActionPolicy::Behaviours::PolicyFor
74
74
 
75
- attr_reader :record, :result
75
+ attr_reader :record
76
76
 
77
77
  # NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
78
78
  def initialize(record = nil, *__rest__)
@@ -83,13 +83,23 @@ module ActionPolicy
83
83
  # Unlike simply calling a predicate rule (`policy.manage?`),
84
84
  # `apply` also calls pre-checks.
85
85
  def apply(rule)
86
- @result = self.class.result_class.new(self.class, rule)
86
+ res = apply_r(rule)
87
87
 
88
- catch :policy_fulfilled do
89
- result.load __apply__(resolve_rule(rule))
90
- end
88
+ # DEPRECATED (we still rely on it in tests)
89
+ @result = res
90
+
91
+ res.value
92
+ end
93
+
94
+ # NEXT_RELEASE: This is gonna be #apply in 1.0
95
+ def apply_r(rule) # :nodoc:
96
+ with_result(rule) do |result|
97
+ catch :policy_fulfilled do
98
+ result.load __apply__(resolve_rule(rule))
99
+ end
91
100
 
92
- result.value
101
+ result
102
+ end
93
103
  end
94
104
 
95
105
  def deny!
@@ -107,14 +117,17 @@ module ActionPolicy
107
117
  # (such as caching, pre checks, etc.)
108
118
  def __apply__(rule) ; public_send(rule); end
109
119
 
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
120
+ # Prepare a new result object for the next rule application.
121
+ # It's stored in the thread-local storage to be accessible from within the policy.
122
+ def with_result(rule) # :nodoc:
123
+ result = self.class.result_class.new(self.class, rule)
124
+
125
+ Thread.current[:__action_policy_result__] ||= []
126
+ Thread.current[:__action_policy_result__] << result
127
+
128
+ yield result
116
129
  ensure
117
- @result = was_result
130
+ Thread.current[:__action_policy_result__]&.pop
118
131
  end
119
132
 
120
133
  # Returns a result of applying the specified rule to the specified record.
@@ -146,6 +159,13 @@ module ActionPolicy
146
159
  activity
147
160
  end
148
161
 
162
+ # Returns the result object for the last rule application within the given
163
+ # execution context (Thread or Fiber)
164
+ def result
165
+ # FIXME: Remove ivar fallback after 1.0
166
+ Thread.current[:__action_policy_result__]&.last || @result
167
+ end
168
+
149
169
  # Return annotated source code for the rule
150
170
  # NOTE: require "method_source" and "prism" gems to be installed.
151
171
  # Otherwise returns empty string.
@@ -155,9 +175,7 @@ module ActionPolicy
155
175
  # Useful for debugging: type `pp :show?` within the context of the policy
156
176
  # to preview the rule.
157
177
  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)
178
+ with_result(rule) do
161
179
  header = "#{self.class.name}##{rule}"
162
180
  source = inspect_rule(rule)
163
181
  $stdout.puts "#{header}\n#{source}"
@@ -201,13 +201,12 @@ module ActionPolicy
201
201
  if (record == :__undef__ || record == self.record) && options.empty?
202
202
  rule = resolve_rule(rule)
203
203
  policy = self
204
- with_clean_result { apply(rule) }
204
+ apply_r(rule)
205
205
  else
206
206
  policy = policy_for(record: record, **options)
207
207
  rule = policy.resolve_rule(rule)
208
208
 
209
- policy.apply(rule)
210
- policy.result
209
+ policy.apply_r(rule)
211
210
  end
212
211
 
213
212
  if res.fail? && result&.reasons
@@ -220,7 +219,7 @@ module ActionPolicy
220
219
  end
221
220
 
222
221
  def deny!(reason = nil)
223
- result&.reasons&.add(self, reason) if reason
222
+ result&.reasons&.add(self, reason, result.details) if reason
224
223
  super()
225
224
  end
226
225
  end
@@ -72,7 +72,7 @@ module ActionPolicy
72
72
 
73
73
  include ActionPolicy::Behaviours::PolicyFor
74
74
 
75
- attr_reader :record, :result
75
+ attr_reader :record
76
76
 
77
77
  # NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
78
78
  def initialize(record = nil, *__rest__)
@@ -83,13 +83,23 @@ module ActionPolicy
83
83
  # Unlike simply calling a predicate rule (`policy.manage?`),
84
84
  # `apply` also calls pre-checks.
85
85
  def apply(rule)
86
- @result = self.class.result_class.new(self.class, rule)
86
+ res = apply_r(rule)
87
87
 
88
- catch :policy_fulfilled do
89
- result.load __apply__(resolve_rule(rule))
90
- end
88
+ # DEPRECATED (we still rely on it in tests)
89
+ @result = res
90
+
91
+ res.value
92
+ end
93
+
94
+ # NEXT_RELEASE: This is gonna be #apply in 1.0
95
+ def apply_r(rule) # :nodoc:
96
+ with_result(rule) do |result|
97
+ catch :policy_fulfilled do
98
+ result.load __apply__(resolve_rule(rule))
99
+ end
91
100
 
92
- result.value
101
+ result
102
+ end
93
103
  end
94
104
 
95
105
  def deny!
@@ -107,14 +117,17 @@ module ActionPolicy
107
117
  # (such as caching, pre checks, etc.)
108
118
  def __apply__(rule) = public_send(rule)
109
119
 
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
120
+ # Prepare a new result object for the next rule application.
121
+ # It's stored in the thread-local storage to be accessible from within the policy.
122
+ def with_result(rule) # :nodoc:
123
+ result = self.class.result_class.new(self.class, rule)
124
+
125
+ Thread.current[:__action_policy_result__] ||= []
126
+ Thread.current[:__action_policy_result__] << result
127
+
128
+ yield result
116
129
  ensure
117
- @result = was_result
130
+ Thread.current[:__action_policy_result__]&.pop
118
131
  end
119
132
 
120
133
  # Returns a result of applying the specified rule to the specified record.
@@ -146,6 +159,13 @@ module ActionPolicy
146
159
  activity
147
160
  end
148
161
 
162
+ # Returns the result object for the last rule application within the given
163
+ # execution context (Thread or Fiber)
164
+ def result
165
+ # FIXME: Remove ivar fallback after 1.0
166
+ Thread.current[:__action_policy_result__]&.last || @result
167
+ end
168
+
149
169
  # Return annotated source code for the rule
150
170
  # NOTE: require "method_source" and "prism" gems to be installed.
151
171
  # Otherwise returns empty string.
@@ -155,9 +175,7 @@ module ActionPolicy
155
175
  # Useful for debugging: type `pp :show?` within the context of the policy
156
176
  # to preview the rule.
157
177
  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)
178
+ with_result(rule) do
161
179
  header = "#{self.class.name}##{rule}"
162
180
  source = inspect_rule(rule)
163
181
  $stdout.puts "#{header}\n#{source}"
@@ -5,12 +5,13 @@ module ActionPolicy
5
5
  class Unauthorized < Error
6
6
  attr_reader :policy, :rule, :result
7
7
 
8
- def initialize(policy, rule)
8
+ # NEXT_RELEASE: remove result fallback
9
+ def initialize(policy, rule, result = policy.result)
9
10
  @policy = policy.class
10
11
  @rule = rule
11
- @result = policy.result
12
+ @result = result
12
13
 
13
- super("Not Authorized")
14
+ super("Not authorized: #{@policy}##{@rule} returns false")
14
15
  end
15
16
  end
16
17
 
@@ -20,12 +21,14 @@ module ActionPolicy
20
21
  class << self
21
22
  # Performs authorization, raises an exception when check failed.
22
23
  def call(policy, rule)
23
- authorize(policy, rule) ||
24
- raise(::ActionPolicy::Unauthorized.new(policy, rule))
24
+ res = authorize(policy, rule)
25
+ return if res.success?
26
+
27
+ raise(::ActionPolicy::Unauthorized.new(policy, rule, res))
25
28
  end
26
29
 
27
30
  def authorize(policy, rule)
28
- policy.apply(rule)
31
+ policy.apply_r(rule)
29
32
  end
30
33
 
31
34
  # Applies scope to the target
@@ -53,8 +53,7 @@ module ActionPolicy
53
53
  def allowance_to(rule, record = :__undef__, **options)
54
54
  policy = lookup_authorization_policy(record, **options)
55
55
 
56
- policy.apply(authorization_rule_for(policy, rule))
57
- policy.result
56
+ policy.apply_r(authorization_rule_for(policy, rule))
58
57
  end
59
58
 
60
59
  def authorization_context
@@ -50,18 +50,18 @@ module ActionPolicy # :nodoc:
50
50
  key = rule_cache_key(rule)
51
51
 
52
52
  ActionPolicy.cache_store.then do |store|
53
- @result = store.read(key)
53
+ result = store.read(key)
54
54
  unless result.nil?
55
55
  result.cached!
56
- next result.value
56
+ next result
57
+ end
58
+ yield.tap do |result|
59
+ store.write(key, result, options)
57
60
  end
58
- yield
59
- store.write(key, result, options)
60
- result.value
61
61
  end
62
62
  end
63
63
 
64
- def apply(rule)
64
+ def apply_r(rule)
65
65
  return super if ActionPolicy.cache_store.nil? ||
66
66
  !self.class.cached_rules.key?(rule)
67
67
 
@@ -7,19 +7,16 @@ module ActionPolicy
7
7
  # When you call `apply` twice on the same policy and for the same rule,
8
8
  # the check (and pre-checks) is only called once.
9
9
  module CachedApply
10
- def apply(rule)
10
+ def apply_r(rule)
11
11
  @__rules_cache__ ||= {}
12
12
 
13
13
  if @__rules_cache__.key?(rule)
14
- @result = @__rules_cache__[rule]
15
- return result.value
14
+ return @__rules_cache__[rule]
16
15
  end
17
16
 
18
- super
19
-
20
- @__rules_cache__[rule] = result
21
-
22
- result.value
17
+ super.tap do |result|
18
+ @__rules_cache__[rule] = result
19
+ end
23
20
  end
24
21
  end
25
22
  end
@@ -72,7 +72,7 @@ module ActionPolicy
72
72
 
73
73
  include ActionPolicy::Behaviours::PolicyFor
74
74
 
75
- attr_reader :record, :result
75
+ attr_reader :record
76
76
 
77
77
  # NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
78
78
  def initialize(record = nil, *)
@@ -83,13 +83,23 @@ module ActionPolicy
83
83
  # Unlike simply calling a predicate rule (`policy.manage?`),
84
84
  # `apply` also calls pre-checks.
85
85
  def apply(rule)
86
- @result = self.class.result_class.new(self.class, rule)
86
+ res = apply_r(rule)
87
87
 
88
- catch :policy_fulfilled do
89
- result.load __apply__(resolve_rule(rule))
90
- end
88
+ # DEPRECATED (we still rely on it in tests)
89
+ @result = res
90
+
91
+ res.value
92
+ end
93
+
94
+ # NEXT_RELEASE: This is gonna be #apply in 1.0
95
+ def apply_r(rule) # :nodoc:
96
+ with_result(rule) do |result|
97
+ catch :policy_fulfilled do
98
+ result.load __apply__(resolve_rule(rule))
99
+ end
91
100
 
92
- result.value
101
+ result
102
+ end
93
103
  end
94
104
 
95
105
  def deny!
@@ -107,14 +117,17 @@ module ActionPolicy
107
117
  # (such as caching, pre checks, etc.)
108
118
  def __apply__(rule) = public_send(rule)
109
119
 
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
120
+ # Prepare a new result object for the next rule application.
121
+ # It's stored in the thread-local storage to be accessible from within the policy.
122
+ def with_result(rule) # :nodoc:
123
+ result = self.class.result_class.new(self.class, rule)
124
+
125
+ Thread.current[:__action_policy_result__] ||= []
126
+ Thread.current[:__action_policy_result__] << result
127
+
128
+ yield result
116
129
  ensure
117
- @result = was_result
130
+ Thread.current[:__action_policy_result__]&.pop
118
131
  end
119
132
 
120
133
  # Returns a result of applying the specified rule to the specified record.
@@ -146,6 +159,13 @@ module ActionPolicy
146
159
  activity
147
160
  end
148
161
 
162
+ # Returns the result object for the last rule application within the given
163
+ # execution context (Thread or Fiber)
164
+ def result
165
+ # FIXME: Remove ivar fallback after 1.0
166
+ Thread.current[:__action_policy_result__]&.last || @result
167
+ end
168
+
149
169
  # Return annotated source code for the rule
150
170
  # NOTE: require "method_source" and "prism" gems to be installed.
151
171
  # Otherwise returns empty string.
@@ -155,9 +175,7 @@ module ActionPolicy
155
175
  # Useful for debugging: type `pp :show?` within the context of the policy
156
176
  # to preview the rule.
157
177
  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)
178
+ with_result(rule) do
161
179
  header = "#{self.class.name}##{rule}"
162
180
  source = inspect_rule(rule)
163
181
  $stdout.puts "#{header}\n#{source}"
@@ -201,13 +201,12 @@ module ActionPolicy
201
201
  if (record == :__undef__ || record == self.record) && options.empty?
202
202
  rule = resolve_rule(rule)
203
203
  policy = self
204
- with_clean_result { apply(rule) }
204
+ apply_r(rule)
205
205
  else
206
206
  policy = policy_for(record: record, **options)
207
207
  rule = policy.resolve_rule(rule)
208
208
 
209
- policy.apply(rule)
210
- policy.result
209
+ policy.apply_r(rule)
211
210
  end
212
211
 
213
212
  if res.fail? && result&.reasons
@@ -220,7 +219,7 @@ module ActionPolicy
220
219
  end
221
220
 
222
221
  def deny!(reason = nil)
223
- result&.reasons&.add(self, reason) if reason
222
+ result&.reasons&.add(self, reason, result.details) if reason
224
223
  super()
225
224
  end
226
225
  end
@@ -9,10 +9,10 @@ module ActionPolicy # :nodoc:
9
9
  def authorize(policy, rule)
10
10
  event = {policy: policy.class.name, rule: rule.to_s}
11
11
  ActiveSupport::Notifications.instrument(EVENT_NAME, event) do
12
- res = super
13
- event[:cached] = policy.result.cached?
14
- event[:value] = policy.result.value
15
- res
12
+ result = super
13
+ event[:cached] = result.cached?
14
+ event[:value] = result.value
15
+ result
16
16
  end
17
17
  end
18
18
  end
@@ -56,7 +56,7 @@ module ActionPolicy
56
56
  # Tries to infer the resource class from controller name
57
57
  # (i.e. `controller_name.classify.safe_constantize`).
58
58
  def implicit_authorization_target
59
- controller_name.classify.safe_constantize
59
+ controller_name&.classify&.safe_constantize
60
60
  end
61
61
 
62
62
  def verify_authorized
@@ -16,13 +16,13 @@ module ActionPolicy # :nodoc:
16
16
  ActiveSupport::Notifications.instrument(INIT_EVENT_NAME, event) { super }
17
17
  end
18
18
 
19
- def apply(rule)
19
+ def apply_r(rule)
20
20
  event = {policy: self.class.name, rule: rule.to_s}
21
21
  ActiveSupport::Notifications.instrument(APPLY_EVENT_NAME, event) do
22
- res = super
22
+ result = super
23
23
  event[:cached] = result.cached?
24
24
  event[:value] = result.value
25
- res
25
+ result
26
26
  end
27
27
  end
28
28
  end
@@ -39,6 +39,7 @@ module ActionPolicy
39
39
  super
40
40
  end
41
41
 
42
+ # FIXME(1.0): Update to use result object directly
42
43
  def formatted_policy(policy)
43
44
  "#{policy.result.inspect}\n#{policy.inspect_rule(policy.result.rule)}"
44
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.3"
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.7.1
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-25 00:00:00.000000000 Z
11
+ date: 2024-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core