pundit 2.2.0 → 2.5.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -29
  3. data/CONTRIBUTING.md +3 -5
  4. data/README.md +125 -54
  5. data/SECURITY.md +19 -0
  6. data/config/rubocop-rspec.yml +5 -0
  7. data/lib/generators/pundit/install/install_generator.rb +3 -1
  8. data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
  9. data/lib/generators/pundit/policy/policy_generator.rb +3 -1
  10. data/lib/generators/pundit/policy/templates/policy.rb.tt +16 -0
  11. data/lib/generators/rspec/policy_generator.rb +4 -1
  12. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +1 -1
  13. data/lib/generators/test_unit/policy_generator.rb +4 -1
  14. data/lib/pundit/authorization.rb +176 -75
  15. data/lib/pundit/cache_store/legacy_store.rb +27 -0
  16. data/lib/pundit/cache_store/null_store.rb +30 -0
  17. data/lib/pundit/cache_store.rb +24 -0
  18. data/lib/pundit/context.rb +190 -0
  19. data/lib/pundit/error.rb +71 -0
  20. data/lib/pundit/helper.rb +16 -0
  21. data/lib/pundit/policy_finder.rb +34 -2
  22. data/lib/pundit/railtie.rb +20 -0
  23. data/lib/pundit/rspec.rb +92 -7
  24. data/lib/pundit/version.rb +2 -1
  25. data/lib/pundit.rb +45 -140
  26. metadata +25 -170
  27. data/.gitignore +0 -19
  28. data/.rubocop.yml +0 -72
  29. data/.travis.yml +0 -26
  30. data/.yardopts +0 -1
  31. data/CODE_OF_CONDUCT.md +0 -28
  32. data/Gemfile +0 -7
  33. data/Rakefile +0 -20
  34. data/lib/generators/pundit/policy/templates/policy.rb +0 -10
  35. data/pundit.gemspec +0 -33
  36. data/spec/authorization_spec.rb +0 -258
  37. data/spec/generators_spec.rb +0 -43
  38. data/spec/policies/post_policy_spec.rb +0 -22
  39. data/spec/policy_finder_spec.rb +0 -187
  40. data/spec/pundit_spec.rb +0 -427
  41. data/spec/spec_helper.rb +0 -275
  42. /data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @private
3
4
  module Rspec
5
+ # @private
4
6
  module Generators
7
+ # @private
5
8
  class PolicyGenerator < ::Rails::Generators::NamedBase
6
9
  source_root File.expand_path("templates", __dir__)
7
10
 
8
11
  def create_policy_spec
9
- template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
12
+ template "policy_spec.rb.tt", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
10
13
  end
11
14
  end
12
15
  end
@@ -1,4 +1,4 @@
1
- require '<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
1
+ require '<%= File.exist?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
2
2
 
3
3
  RSpec.describe <%= class_name %>Policy, type: :policy do
4
4
  let(:user) { User.new }
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @private
3
4
  module TestUnit
5
+ # @private
4
6
  module Generators
7
+ # @private
5
8
  class PolicyGenerator < ::Rails::Generators::NamedBase
6
9
  source_root File.expand_path("templates", __dir__)
7
10
 
8
11
  def create_policy_test
9
- template "policy_test.rb", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
12
+ template "policy_test.rb.tt", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
10
13
  end
11
14
  end
12
15
  end
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
+ # Pundit DSL to include in your controllers to provide authorization helpers.
5
+ #
6
+ # @example
7
+ # class ApplicationController < ActionController::Base
8
+ # include Pundit::Authorization
9
+ # end
10
+ # @see #pundit
11
+ # @api public
12
+ # @since v2.2.0
4
13
  module Authorization
5
14
  extend ActiveSupport::Concern
6
15
 
@@ -15,40 +24,55 @@ module Pundit
15
24
 
16
25
  protected
17
26
 
18
- # @return [Boolean] whether authorization has been performed, i.e. whether
19
- # one {#authorize} or {#skip_authorization} has been called
20
- def pundit_policy_authorized?
21
- !!@_pundit_policy_authorized
22
- end
23
-
24
- # @return [Boolean] whether policy scoping has been performed, i.e. whether
25
- # one {#policy_scope} or {#skip_policy_scope} has been called
26
- def pundit_policy_scoped?
27
- !!@_pundit_policy_scoped
27
+ # An instance of {Pundit::Context} initialized with the current user.
28
+ #
29
+ # @note this method is memoized and will return the same instance during the request.
30
+ # @api public
31
+ # @return [Pundit::Context]
32
+ # @see #pundit_user
33
+ # @see #policies
34
+ # @since v2.3.2
35
+ def pundit
36
+ @pundit ||= Pundit::Context.new(
37
+ user: pundit_user,
38
+ policy_cache: Pundit::CacheStore::LegacyStore.new(policies)
39
+ )
28
40
  end
29
41
 
30
- # Raises an error if authorization has not been performed, usually used as an
31
- # `after_action` filter to prevent programmer error in forgetting to call
32
- # {#authorize} or {#skip_authorization}.
42
+ # Hook method which allows customizing which user is passed to policies and
43
+ # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
33
44
  #
34
- # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
35
- # @raise [AuthorizationNotPerformedError] if authorization has not been performed
36
- # @return [void]
37
- def verify_authorized
38
- raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
45
+ # @note Make sure to call `pundit_reset!` if this changes during a request.
46
+ # @see https://github.com/varvet/pundit#customize-pundit-user
47
+ # @see #pundit
48
+ # @see #pundit_reset!
49
+ # @return [Object] the user object to be used with pundit
50
+ # @since v0.2.2
51
+ def pundit_user
52
+ current_user
39
53
  end
40
54
 
41
- # Raises an error if policy scoping has not been performed, usually used as an
42
- # `after_action` filter to prevent programmer error in forgetting to call
43
- # {#policy_scope} or {#skip_policy_scope} in index actions.
55
+ # Clears the cached Pundit authorization data.
56
+ #
57
+ # This method should be called when the pundit_user is changed,
58
+ # such as during user switching, to ensure that stale authorization
59
+ # data is not used. Pundit caches authorization policies and scopes
60
+ # for the pundit_user, so calling this method will reset those
61
+ # caches and ensure that the next authorization checks are performed
62
+ # with the correct context for the new pundit_user.
44
63
  #
45
- # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
46
- # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
47
64
  # @return [void]
48
- def verify_policy_scoped
49
- raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
65
+ # @since v2.5.0
66
+ def pundit_reset!
67
+ @pundit = nil
68
+ @_pundit_policies = nil
69
+ @_pundit_policy_scopes = nil
70
+ @_pundit_policy_authorized = nil
71
+ @_pundit_policy_scoped = nil
50
72
  end
51
73
 
74
+ # @!group Policies
75
+
52
76
  # Retrieves the policy for the given record, initializing it with the record
53
77
  # and current user and finally throwing an error if the user is not
54
78
  # authorized to perform the given action.
@@ -58,62 +82,169 @@ module Pundit
58
82
  # If omitted then this defaults to the Rails controller action name.
59
83
  # @param policy_class [Class] the policy class we want to force use of
60
84
  # @raise [NotAuthorizedError] if the given query method returned false
61
- # @return [Object] Always returns the passed object record
85
+ # @return [record] Always returns the passed object record
86
+ # @see Pundit::Context#authorize
87
+ # @see #verify_authorized
88
+ # @since v0.1.0
62
89
  def authorize(record, query = nil, policy_class: nil)
63
90
  query ||= "#{action_name}?"
64
91
 
65
92
  @_pundit_policy_authorized = true
66
93
 
67
- Pundit.authorize(pundit_user, record, query, policy_class: policy_class, cache: policies)
94
+ pundit.authorize(record, query: query, policy_class: policy_class)
68
95
  end
69
96
 
70
97
  # Allow this action not to perform authorization.
71
98
  #
72
99
  # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
73
100
  # @return [void]
101
+ # @see #verify_authorized
102
+ # @since v1.0.0
74
103
  def skip_authorization
75
104
  @_pundit_policy_authorized = :skipped
76
105
  end
77
106
 
78
- # Allow this action not to perform policy scoping.
107
+ # @return [Boolean] wether or not authorization has been performed
108
+ # @see #authorize
109
+ # @see #skip_authorization
110
+ # @since v1.0.0
111
+ def pundit_policy_authorized?
112
+ !!@_pundit_policy_authorized
113
+ end
114
+
115
+ # Raises an error if authorization has not been performed.
116
+ #
117
+ # Usually used as an `after_action` filter to prevent programmer error in
118
+ # forgetting to call {#authorize} or {#skip_authorization}.
79
119
  #
80
120
  # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
121
+ # @raise [AuthorizationNotPerformedError] if authorization has not been performed
81
122
  # @return [void]
82
- def skip_policy_scope
83
- @_pundit_policy_scoped = :skipped
123
+ # @see #authorize
124
+ # @see #skip_authorization
125
+ # @since v0.1.0
126
+ def verify_authorized
127
+ raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
128
+ end
129
+
130
+ # rubocop:disable Naming/MemoizedInstanceVariableName
131
+
132
+ # Cache of policies. You should not rely on this method.
133
+ #
134
+ # @api private
135
+ # @since v1.0.0
136
+ def policies
137
+ @_pundit_policies ||= {}
138
+ end
139
+
140
+ # rubocop:enable Naming/MemoizedInstanceVariableName
141
+
142
+ # @!endgroup
143
+
144
+ # Retrieves the policy for the given record.
145
+ #
146
+ # @see https://github.com/varvet/pundit#policies
147
+ # @param record [Object] the object we're retrieving the policy for
148
+ # @return [Object] instance of policy class with query methods
149
+ # @since v0.1.0
150
+ def policy(record)
151
+ pundit.policy!(record)
84
152
  end
85
153
 
154
+ # @!group Policy Scopes
155
+
86
156
  # Retrieves the policy scope for the given record.
87
157
  #
88
158
  # @see https://github.com/varvet/pundit#scopes
89
159
  # @param scope [Object] the object we're retrieving the policy scope for
90
- # @param policy_scope_class [Class] the policy scope class we want to force use of
91
- # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
160
+ # @param policy_scope_class [#resolve] the policy scope class we want to force use of
161
+ # @return [#resolve, nil] instance of scope class which can resolve to a scope
162
+ # @since v0.1.0
92
163
  def policy_scope(scope, policy_scope_class: nil)
93
164
  @_pundit_policy_scoped = true
94
165
  policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
95
166
  end
96
167
 
97
- # Retrieves the policy for the given record.
168
+ # Allow this action not to perform policy scoping.
98
169
  #
99
- # @see https://github.com/varvet/pundit#policies
100
- # @param record [Object] the object we're retrieving the policy for
101
- # @return [Object, nil] instance of policy class with query methods
102
- def policy(record)
103
- policies[record] ||= Pundit.policy!(pundit_user, record)
170
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
171
+ # @return [void]
172
+ # @see #verify_policy_scoped
173
+ # @since v1.0.0
174
+ def skip_policy_scope
175
+ @_pundit_policy_scoped = :skipped
104
176
  end
105
177
 
106
- # Retrieves a set of permitted attributes from the policy by instantiating
107
- # the policy class for the given record and calling `permitted_attributes` on
108
- # it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers
109
- # what key the record should have in the params hash and retrieves the
110
- # permitted attributes from the params hash under that key.
178
+ # @return [Boolean] wether or not policy scoping has been performed
179
+ # @see #policy_scope
180
+ # @see #skip_policy_scope
181
+ # @since v1.0.0
182
+ def pundit_policy_scoped?
183
+ !!@_pundit_policy_scoped
184
+ end
185
+
186
+ # Raises an error if policy scoping has not been performed.
187
+ #
188
+ # Usually used as an `after_action` filter to prevent programmer error in
189
+ # forgetting to call {#policy_scope} or {#skip_policy_scope} in index
190
+ # actions.
191
+ #
192
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
193
+ # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
194
+ # @return [void]
195
+ # @see #policy_scope
196
+ # @see #skip_policy_scope
197
+ # @since v0.2.1
198
+ def verify_policy_scoped
199
+ raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
200
+ end
201
+
202
+ # rubocop:disable Naming/MemoizedInstanceVariableName
203
+
204
+ # Cache of policy scope. You should not rely on this method.
205
+ #
206
+ # @api private
207
+ # @since v1.0.0
208
+ def policy_scopes
209
+ @_pundit_policy_scopes ||= {}
210
+ end
211
+
212
+ # rubocop:enable Naming/MemoizedInstanceVariableName
213
+
214
+ # This was added to allow calling `policy_scope!` without flipping the
215
+ # `pundit_policy_scoped?` flag.
216
+ #
217
+ # It's used internally by `policy_scope`, as well as from the views
218
+ # when they call `policy_scope`. It works because views get their helper
219
+ # from {Pundit::Helper}.
220
+ #
221
+ # @note This also memoizes the instance with `scope` as the key.
222
+ # @see Pundit::Helper#policy_scope
223
+ # @api private
224
+ # @since v1.0.0
225
+ def pundit_policy_scope(scope)
226
+ policy_scopes[scope] ||= pundit.policy_scope!(scope)
227
+ end
228
+ private :pundit_policy_scope
229
+
230
+ # @!endgroup
231
+
232
+ # @!group Strong Parameters
233
+
234
+ # Retrieves a set of permitted attributes from the policy.
235
+ #
236
+ # Done by instantiating the policy class for the given record and calling
237
+ # `permitted_attributes` on it, or `permitted_attributes_for_{action}` if
238
+ # `action` is defined. It then infers what key the record should have in the
239
+ # params hash and retrieves the permitted attributes from the params hash
240
+ # under that key.
111
241
  #
112
242
  # @see https://github.com/varvet/pundit#strong-parameters
113
243
  # @param record [Object] the object we're retrieving permitted attributes for
114
244
  # @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
115
245
  # If omitted then this defaults to the Rails controller action name.
116
246
  # @return [Hash{String => Object}] the permitted attributes
247
+ # @since v1.0.0
117
248
  def permitted_attributes(record, action = action_name)
118
249
  policy = policy(record)
119
250
  method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
@@ -128,41 +259,11 @@ module Pundit
128
259
  #
129
260
  # @param record [Object] the object we're retrieving params for
130
261
  # @return [ActionController::Parameters] the params
262
+ # @since v2.0.0
131
263
  def pundit_params_for(record)
132
264
  params.require(PolicyFinder.new(record).param_key)
133
265
  end
134
266
 
135
- # Cache of policies. You should not rely on this method.
136
- #
137
- # @api private
138
- # rubocop:disable Naming/MemoizedInstanceVariableName
139
- def policies
140
- @_pundit_policies ||= {}
141
- end
142
- # rubocop:enable Naming/MemoizedInstanceVariableName
143
-
144
- # Cache of policy scope. You should not rely on this method.
145
- #
146
- # @api private
147
- # rubocop:disable Naming/MemoizedInstanceVariableName
148
- def policy_scopes
149
- @_pundit_policy_scopes ||= {}
150
- end
151
- # rubocop:enable Naming/MemoizedInstanceVariableName
152
-
153
- # Hook method which allows customizing which user is passed to policies and
154
- # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
155
- #
156
- # @see https://github.com/varvet/pundit#customize-pundit-user
157
- # @return [Object] the user object to be used with pundit
158
- def pundit_user
159
- current_user
160
- end
161
-
162
- private
163
-
164
- def pundit_policy_scope(scope)
165
- policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
166
- end
267
+ # @!endgroup
167
268
  end
168
269
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module CacheStore
5
+ # A cache store that uses only the record as a cache key, and ignores the user.
6
+ #
7
+ # The original cache mechanism used by Pundit.
8
+ #
9
+ # @api private
10
+ # @since v2.3.2
11
+ class LegacyStore
12
+ # @since v2.3.2
13
+ def initialize(hash = {})
14
+ @store = hash
15
+ end
16
+
17
+ # A cache store that uses only the record as a cache key, and ignores the user.
18
+ #
19
+ # @note `nil` results are not cached.
20
+ # @since v2.3.2
21
+ def fetch(user:, record:)
22
+ _ = user
23
+ @store[record] ||= yield
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module CacheStore
5
+ # A cache store that does not cache anything.
6
+ #
7
+ # Use `NullStore.instance` to get the singleton instance, it is thread-safe.
8
+ #
9
+ # @see Pundit::Context#initialize
10
+ # @api private
11
+ # @since v2.3.2
12
+ class NullStore
13
+ @instance = new
14
+
15
+ class << self
16
+ # @since v2.3.2
17
+ # @return [NullStore] the singleton instance
18
+ attr_reader :instance
19
+ end
20
+
21
+ # Always yields, does not cache anything.
22
+ # @yield
23
+ # @return [any] whatever the block returns.
24
+ # @since v2.3.2
25
+ def fetch(*, **)
26
+ yield
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ # Namespace for cache store implementations.
5
+ #
6
+ # Cache stores are used to cache policy lookups, so you get the same policy
7
+ # instance for the same record.
8
+ # @since v2.3.2
9
+ module CacheStore
10
+ # @!group Cache Store Interface
11
+
12
+ # @!method fetch(user:, record:, &block)
13
+ # Looks up a stored policy or generate a new one.
14
+ #
15
+ # @since v2.3.2
16
+ # @note This is a method template, but the method does not exist in this module.
17
+ # @param user [Object] the user that initiated the action
18
+ # @param record [Object] the object being accessed
19
+ # @param block [Proc] the block to execute if missing
20
+ # @return [Object] the policy
21
+
22
+ # @!endgroup
23
+ end
24
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ # {Pundit::Context} is intended to be created once per request and user, and
5
+ # it is then used to perform authorization checks throughout the request.
6
+ #
7
+ # @example Using Sinatra
8
+ # helpers do
9
+ # def current_user = ...
10
+ #
11
+ # def pundit
12
+ # @pundit ||= Pundit::Context.new(user: current_user)
13
+ # end
14
+ # end
15
+ #
16
+ # get "/posts/:id" do |id|
17
+ # pundit.authorize(Post.find(id), query: :show?)
18
+ # end
19
+ #
20
+ # @example Using [Roda](https://roda.jeremyevans.net/index.html)
21
+ # route do |r|
22
+ # context = Pundit::Context.new(user:)
23
+ #
24
+ # r.get "posts", Integer do |id|
25
+ # context.authorize(Post.find(id), query: :show?)
26
+ # end
27
+ # end
28
+ #
29
+ # @since v2.3.2
30
+ class Context
31
+ # @see Pundit::Authorization#pundit
32
+ # @param user later passed to policies and scopes
33
+ # @param policy_cache [#fetch] cache store for policies (see e.g. {CacheStore::NullStore})
34
+ # @since v2.3.2
35
+ def initialize(user:, policy_cache: CacheStore::NullStore.instance)
36
+ @user = user
37
+ @policy_cache = policy_cache
38
+ end
39
+
40
+ # @api public
41
+ # @see #initialize
42
+ # @since v2.3.2
43
+ attr_reader :user
44
+
45
+ # @api private
46
+ # @see #initialize
47
+ # @since v2.3.2
48
+ attr_reader :policy_cache
49
+
50
+ # @!group Policies
51
+
52
+ # Retrieves the policy for the given record, initializing it with the
53
+ # record and user and finally throwing an error if the user is not
54
+ # authorized to perform the given action.
55
+ #
56
+ # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
57
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
58
+ # @param policy_class [Class] the policy class we want to force use of
59
+ # @raise [NotAuthorizedError] if the given query method returned false
60
+ # @return [Object] Always returns the passed object record
61
+ # @since v2.3.2
62
+ def authorize(possibly_namespaced_record, query:, policy_class:)
63
+ record = pundit_model(possibly_namespaced_record)
64
+ policy = if policy_class
65
+ policy_class.new(user, record)
66
+ else
67
+ policy!(possibly_namespaced_record)
68
+ end
69
+
70
+ raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
71
+
72
+ record
73
+ end
74
+
75
+ # Retrieves the policy for the given record.
76
+ #
77
+ # @see https://github.com/varvet/pundit#policies
78
+ # @param record [Object] the object we're retrieving the policy for
79
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
80
+ # @return [Object, nil] instance of policy class with query methods
81
+ # @since v2.3.2
82
+ def policy(record)
83
+ cached_find(record, &:policy)
84
+ end
85
+
86
+ # Retrieves the policy for the given record, or raises if not found.
87
+ #
88
+ # @see https://github.com/varvet/pundit#policies
89
+ # @param record [Object] the object we're retrieving the policy for
90
+ # @raise [NotDefinedError] if the policy cannot be found
91
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
92
+ # @return [Object] instance of policy class with query methods
93
+ # @since v2.3.2
94
+ def policy!(record)
95
+ cached_find(record, &:policy!)
96
+ end
97
+
98
+ # @!endgroup
99
+
100
+ # @!group Scopes
101
+
102
+ # Retrieves the policy scope for the given record.
103
+ #
104
+ # @see https://github.com/varvet/pundit#scopes
105
+ # @param scope [Object] the object we're retrieving the policy scope for
106
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
107
+ # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
108
+ # @since v2.3.2
109
+ def policy_scope(scope)
110
+ policy_scope_class = policy_finder(scope).scope
111
+ return unless policy_scope_class
112
+
113
+ begin
114
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
115
+ rescue ArgumentError
116
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
117
+ end
118
+
119
+ policy_scope.resolve
120
+ end
121
+
122
+ # Retrieves the policy scope for the given record. Raises if not found.
123
+ #
124
+ # @see https://github.com/varvet/pundit#scopes
125
+ # @param scope [Object] the object we're retrieving the policy scope for
126
+ # @raise [NotDefinedError] if the policy scope cannot be found
127
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
128
+ # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
129
+ # @since v2.3.2
130
+ def policy_scope!(scope)
131
+ policy_scope_class = policy_finder(scope).scope!
132
+
133
+ begin
134
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
135
+ rescue ArgumentError
136
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
137
+ end
138
+
139
+ policy_scope.resolve
140
+ end
141
+
142
+ # @!endgroup
143
+
144
+ private
145
+
146
+ # @!group Private Helpers
147
+
148
+ # Finds a cached policy for the given record, or yields to find one.
149
+ #
150
+ # @api private
151
+ # @param record [Object] the object we're retrieving the policy for
152
+ # @yield a policy finder if no policy was cached
153
+ # @yieldparam [PolicyFinder] policy_finder
154
+ # @yieldreturn [#new(user, model)]
155
+ # @return [Policy, nil] an instantiated policy
156
+ # @raise [InvalidConstructorError] if policy can't be instantated
157
+ # @since v2.3.2
158
+ def cached_find(record)
159
+ policy_cache.fetch(user: user, record: record) do
160
+ klass = yield policy_finder(record)
161
+ next unless klass
162
+
163
+ model = pundit_model(record)
164
+
165
+ begin
166
+ klass.new(user, model)
167
+ rescue ArgumentError
168
+ raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
169
+ end
170
+ end
171
+ end
172
+
173
+ # Return a policy finder for the given record.
174
+ #
175
+ # @api private
176
+ # @return [PolicyFinder]
177
+ # @since v2.3.2
178
+ def policy_finder(record)
179
+ PolicyFinder.new(record)
180
+ end
181
+
182
+ # Given a possibly namespaced record, return the actual record.
183
+ #
184
+ # @api private
185
+ # @since v2.3.2
186
+ def pundit_model(record)
187
+ record.is_a?(Array) ? record.last : record
188
+ end
189
+ end
190
+ end