action_policy 0.7.1 → 0.7.3

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