/
pundit.rb
311 lines (266 loc) · 11.6 KB
/
pundit.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# frozen_string_literal: true
require "pundit/version"
require "pundit/policy_finder"
require "active_support/concern"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/module/introspection"
require "active_support/dependencies/autoload"
# @api public
module Pundit
SUFFIX = "Policy".freeze
# @api private
module Generators; end
# @api private
class Error < StandardError; end
# Error that will be raised when authorization has failed
class NotAuthorizedError < Error
attr_reader :query, :record, :policy
def initialize(options = {})
if options.is_a? String
message = options
else
@query = options[:query]
@record = options[:record]
@policy = options[:policy]
message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" }
end
super(message)
end
end
# Error that will be raised if a policy or policy scope constructor is not called correctly.
class InvalidConstructorError < Error; end
# Error that will be raised if a controller action has not called the
# `authorize` or `skip_authorization` methods.
class AuthorizationNotPerformedError < Error; end
# Error that will be raised if a controller action has not called the
# `policy_scope` or `skip_policy_scope` methods.
class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
# Error that will be raised if a policy or policy scope is not defined.
class NotDefinedError < Error; end
extend ActiveSupport::Concern
class << self
# Retrieves the policy for the given record, initializing it with the
# record and user and finally throwing an error if the user is not
# authorized to perform the given action.
#
# @param user [Object] the user that initiated the action
# @param record [Object] the object we're checking permissions of
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
# @param policy_class [Class] the policy class we want to force use of
# @raise [NotAuthorizedError] if the given query method returned false
# @return [Object] Always returns the passed object record
def authorize(user, record, query, policy_class: nil)
policy = policy_class ? policy_class.new(user, record) : policy!(user, record)
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
record
end
# Retrieves the policy scope for the given record.
#
# @see https://github.com/elabs/pundit#scopes
# @param user [Object] the user that initiated the action
# @param scope [Object] the object we're retrieving the policy scope for
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
# @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
def policy_scope(user, scope)
policy_scope = PolicyFinder.new(scope).scope
policy_scope.new(user, pundit_model(scope)).resolve if policy_scope
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
end
# Retrieves the policy scope for the given record.
#
# @see https://github.com/elabs/pundit#scopes
# @param user [Object] the user that initiated the action
# @param scope [Object] the object we're retrieving the policy scope for
# @raise [NotDefinedError] if the policy scope cannot be found
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
# @return [Scope{#resolve}] instance of scope class which can resolve to a scope
def policy_scope!(user, scope)
policy_scope = PolicyFinder.new(scope).scope!
policy_scope.new(user, pundit_model(scope)).resolve
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
end
# Retrieves the policy for the given record.
#
# @see https://github.com/elabs/pundit#policies
# @param user [Object] the user that initiated the action
# @param record [Object] the object we're retrieving the policy for
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
# @return [Object, nil] instance of policy class with query methods
def policy(user, record)
policy = PolicyFinder.new(record).policy
policy.new(user, pundit_model(record)) if policy
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
end
# Retrieves the policy for the given record.
#
# @see https://github.com/elabs/pundit#policies
# @param user [Object] the user that initiated the action
# @param record [Object] the object we're retrieving the policy for
# @raise [NotDefinedError] if the policy cannot be found
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
# @return [Object] instance of policy class with query methods
def policy!(user, record)
policy = PolicyFinder.new(record).policy!
policy.new(user, pundit_model(record))
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
end
private
def pundit_model(record)
record.is_a?(Array) ? record.last : record
end
end
# @api private
module Helper
def policy_scope(scope)
pundit_policy_scope(scope)
end
end
included do
helper Helper if respond_to?(:helper)
if respond_to?(:helper_method)
helper_method :policy
helper_method :pundit_policy_scope
helper_method :pundit_user
end
end
protected
# @return [Boolean] whether authorization has been performed, i.e. whether
# one {#authorize} or {#skip_authorization} has been called
def pundit_policy_authorized?
!!@_pundit_policy_authorized
end
# @return [Boolean] whether policy scoping has been performed, i.e. whether
# one {#policy_scope} or {#skip_policy_scope} has been called
def pundit_policy_scoped?
!!@_pundit_policy_scoped
end
# Raises an error if authorization has not been performed, usually used as an
# `after_action` filter to prevent programmer error in forgetting to call
# {#authorize} or {#skip_authorization}.
#
# @see https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
# @raise [AuthorizationNotPerformedError] if authorization has not been performed
# @return [void]
def verify_authorized
raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
end
# Raises an error if policy scoping has not been performed, usually used as an
# `after_action` filter to prevent programmer error in forgetting to call
# {#policy_scope} or {#skip_policy_scope} in index actions.
#
# @see https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
# @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
# @return [void]
def verify_policy_scoped
raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
end
# Retrieves the policy for the given record, initializing it with the record
# and current user and finally throwing an error if the user is not
# authorized to perform the given action.
#
# @param record [Object] the object we're checking permissions of
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
# If omitted then this defaults to the Rails controller action name.
# @param policy_class [Class] the policy class we want to force use of
# @raise [NotAuthorizedError] if the given query method returned false
# @return [Object] Always returns the passed object record
def authorize(record, query = nil, policy_class: nil)
query ||= "#{action_name}?"
@_pundit_policy_authorized = true
policy = policy_class ? policy_class.new(pundit_user, record) : policy(record)
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
record
end
# Allow this action not to perform authorization.
#
# @see https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
# @return [void]
def skip_authorization
@_pundit_policy_authorized = true
end
# Allow this action not to perform policy scoping.
#
# @see https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
# @return [void]
def skip_policy_scope
@_pundit_policy_scoped = true
end
# Retrieves the policy scope for the given record.
#
# @see https://github.com/elabs/pundit#scopes
# @param scope [Object] the object we're retrieving the policy scope for
# @param policy_scope_class [Class] the policy scope class we want to force use of
# @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
def policy_scope(scope, policy_scope_class: nil)
@_pundit_policy_scoped = true
policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
end
# Retrieves the policy for the given record.
#
# @see https://github.com/elabs/pundit#policies
# @param record [Object] the object we're retrieving the policy for
# @return [Object, nil] instance of policy class with query methods
def policy(record)
policies[record] ||= Pundit.policy!(pundit_user, record)
end
# Retrieves a set of permitted attributes from the policy by instantiating
# the policy class for the given record and calling `permitted_attributes` on
# it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers
# what key the record should have in the params hash and retrieves the
# permitted attributes from the params hash under that key.
#
# @see https://github.com/elabs/pundit#strong-parameters
# @param record [Object] the object we're retrieving permitted attributes for
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
# If omitted then this defaults to the Rails controller action name.
# @return [Hash{String => Object}] the permitted attributes
def permitted_attributes(record, action = action_name)
policy = policy(record)
method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
"permitted_attributes_for_#{action}"
else
"permitted_attributes"
end
pundit_params_for(record).permit(*policy.public_send(method_name))
end
# Retrieves the params for the given record.
#
# @param record [Object] the object we're retrieving params for
# @return [ActionController::Parameters] the params
def pundit_params_for(record)
params.require(PolicyFinder.new(record).param_key)
end
# Cache of policies. You should not rely on this method.
#
# @api private
# rubocop:disable Naming/MemoizedInstanceVariableName
def policies
@_pundit_policies ||= {}
end
# rubocop:enable Naming/MemoizedInstanceVariableName
# Cache of policy scope. You should not rely on this method.
#
# @api private
# rubocop:disable Naming/MemoizedInstanceVariableName
def policy_scopes
@_pundit_policy_scopes ||= {}
end
# rubocop:enable Naming/MemoizedInstanceVariableName
# Hook method which allows customizing which user is passed to policies and
# scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
#
# @see https://github.com/elabs/pundit#customize-pundit-user
# @return [Object] the user object to be used with pundit
def pundit_user
current_user
end
private
def pundit_policy_scope(scope)
policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
end
end