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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -29
- data/CONTRIBUTING.md +3 -5
- data/README.md +125 -54
- data/SECURITY.md +19 -0
- data/config/rubocop-rspec.yml +5 -0
- data/lib/generators/pundit/install/install_generator.rb +3 -1
- data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
- data/lib/generators/pundit/policy/policy_generator.rb +3 -1
- data/lib/generators/pundit/policy/templates/policy.rb.tt +16 -0
- data/lib/generators/rspec/policy_generator.rb +4 -1
- data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +1 -1
- data/lib/generators/test_unit/policy_generator.rb +4 -1
- data/lib/pundit/authorization.rb +176 -75
- data/lib/pundit/cache_store/legacy_store.rb +27 -0
- data/lib/pundit/cache_store/null_store.rb +30 -0
- data/lib/pundit/cache_store.rb +24 -0
- data/lib/pundit/context.rb +190 -0
- data/lib/pundit/error.rb +71 -0
- data/lib/pundit/helper.rb +16 -0
- data/lib/pundit/policy_finder.rb +34 -2
- data/lib/pundit/railtie.rb +20 -0
- data/lib/pundit/rspec.rb +92 -7
- data/lib/pundit/version.rb +2 -1
- data/lib/pundit.rb +45 -140
- metadata +25 -170
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -72
- data/.travis.yml +0 -26
- data/.yardopts +0 -1
- data/CODE_OF_CONDUCT.md +0 -28
- data/Gemfile +0 -7
- data/Rakefile +0 -20
- data/lib/generators/pundit/policy/templates/policy.rb +0 -10
- data/pundit.gemspec +0 -33
- data/spec/authorization_spec.rb +0 -258
- data/spec/generators_spec.rb +0 -43
- data/spec/policies/post_policy_spec.rb +0 -22
- data/spec/policy_finder_spec.rb +0 -187
- data/spec/pundit_spec.rb +0 -427
- data/spec/spec_helper.rb +0 -275
- /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,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
|
data/lib/pundit/authorization.rb
CHANGED
|
@@ -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
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# @
|
|
25
|
-
#
|
|
26
|
-
def
|
|
27
|
-
|
|
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
|
-
#
|
|
31
|
-
#
|
|
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
|
-
# @
|
|
35
|
-
# @
|
|
36
|
-
# @
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
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
|
-
|
|
49
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
83
|
-
|
|
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 [
|
|
91
|
-
# @return [
|
|
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
|
-
#
|
|
168
|
+
# Allow this action not to perform policy scoping.
|
|
98
169
|
#
|
|
99
|
-
# @see https://github.com/varvet/pundit#policies
|
|
100
|
-
# @
|
|
101
|
-
# @
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
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
|
-
#
|
|
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
|