pundit 1.1.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rubygems"
2
4
  require "bundler/gem_tasks"
3
5
  require "rspec/core/rake_task"
@@ -16,4 +18,3 @@ YARD::Rake::YardocTask.new do |t|
16
18
  end
17
19
 
18
20
  task default: :spec
19
- task default: :rubocop unless RUBY_ENGINE == "rbx"
@@ -1,7 +1,7 @@
1
1
  module Pundit
2
2
  module Generators
3
3
  class InstallGenerator < ::Rails::Generators::Base
4
- source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
4
+ source_root File.expand_path('templates', __dir__)
5
5
 
6
6
  def copy_application_policy
7
7
  template 'application_policy.rb', 'app/policies/application_policy.rb'
@@ -11,7 +11,7 @@ class ApplicationPolicy
11
11
  end
12
12
 
13
13
  def show?
14
- scope.where(:id => record.id).exists?
14
+ false
15
15
  end
16
16
 
17
17
  def create?
@@ -34,10 +34,6 @@ class ApplicationPolicy
34
34
  false
35
35
  end
36
36
 
37
- def scope
38
- Pundit.policy_scope!(user, record.class)
39
- end
40
-
41
37
  class Scope
42
38
  attr_reader :user, :scope
43
39
 
@@ -47,7 +43,7 @@ class ApplicationPolicy
47
43
  end
48
44
 
49
45
  def resolve
50
- scope
46
+ scope.all
51
47
  end
52
48
  end
53
49
  end
@@ -1,7 +1,7 @@
1
1
  module Pundit
2
2
  module Generators
3
3
  class PolicyGenerator < ::Rails::Generators::NamedBase
4
- source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
4
+ source_root File.expand_path('templates', __dir__)
5
5
 
6
6
  def create_policy
7
7
  template 'policy.rb', File.join('app/policies', class_path, "#{file_name}_policy.rb")
@@ -2,7 +2,7 @@
2
2
  class <%= class_name %>Policy < ApplicationPolicy
3
3
  class Scope < Scope
4
4
  def resolve
5
- scope
5
+ scope.all
6
6
  end
7
7
  end
8
8
  end
@@ -1,7 +1,7 @@
1
1
  module Rspec
2
2
  module Generators
3
3
  class PolicyGenerator < ::Rails::Generators::NamedBase
4
- source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
4
+ source_root File.expand_path('templates', __dir__)
5
5
 
6
6
  def create_policy_spec
7
7
  template 'policy_spec.rb', File.join('spec/policies', class_path, "#{file_name}_policy_spec.rb")
@@ -1,7 +1,6 @@
1
1
  require '<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
2
2
 
3
- RSpec.describe <%= class_name %>Policy do
4
-
3
+ RSpec.describe <%= class_name %>Policy, type: :policy do
5
4
  let(:user) { User.new }
6
5
 
7
6
  subject { described_class }
@@ -1,7 +1,7 @@
1
1
  module TestUnit
2
2
  module Generators
3
3
  class PolicyGenerator < ::Rails::Generators::NamedBase
4
- source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
4
+ source_root File.expand_path('templates', __dir__)
5
5
 
6
6
  def create_policy_test
7
7
  template 'policy_test.rb', File.join('test/policies', class_path, "#{file_name}_policy_test.rb")
@@ -1,7 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class <%= class_name %>PolicyTest < ActiveSupport::TestCase
4
-
5
4
  def test_scope
6
5
  end
7
6
 
data/lib/pundit.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pundit/version"
2
4
  require "pundit/policy_finder"
3
5
  require "active_support/concern"
@@ -6,17 +8,19 @@ require "active_support/core_ext/object/blank"
6
8
  require "active_support/core_ext/module/introspection"
7
9
  require "active_support/dependencies/autoload"
8
10
 
11
+ # @api private
12
+ # To avoid name clashes with common Error naming when mixing in Pundit,
13
+ # keep it here with compact class style definition.
14
+ class Pundit::Error < StandardError; end # rubocop:disable Style/ClassAndModuleChildren
15
+
9
16
  # @api public
10
17
  module Pundit
11
- SUFFIX = "Policy"
18
+ SUFFIX = "Policy".freeze
12
19
 
13
20
  # @api private
14
21
  module Generators; end
15
22
 
16
- # @api private
17
- class Error < StandardError; end
18
-
19
- # Error that will be raiser when authorization has failed
23
+ # Error that will be raised when authorization has failed
20
24
  class NotAuthorizedError < Error
21
25
  attr_reader :query, :record, :policy
22
26
 
@@ -28,13 +32,16 @@ module Pundit
28
32
  @record = options[:record]
29
33
  @policy = options[:policy]
30
34
 
31
- message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" }
35
+ message = options.fetch(:message) { "not allowed to #{query} this #{record.class}" }
32
36
  end
33
37
 
34
38
  super(message)
35
39
  end
36
40
  end
37
41
 
42
+ # Error that will be raised if a policy or policy scope constructor is not called correctly.
43
+ class InvalidConstructorError < Error; end
44
+
38
45
  # Error that will be raised if a controller action has not called the
39
46
  # `authorize` or `skip_authorization` methods.
40
47
  class AuthorizationNotPerformedError < Error; end
@@ -55,61 +62,92 @@ module Pundit
55
62
  #
56
63
  # @param user [Object] the user that initiated the action
57
64
  # @param record [Object] the object we're checking permissions of
58
- # @param record [Symbol] the query method to check on the policy (e.g. `:show?`)
65
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
66
+ # @param policy_class [Class] the policy class we want to force use of
59
67
  # @raise [NotAuthorizedError] if the given query method returned false
60
- # @return [true] Always returns true
61
- def authorize(user, record, query)
62
- policy = policy!(user, record)
68
+ # @return [Object] Always returns the passed object record
69
+ def authorize(user, record, query, policy_class: nil)
70
+ policy = policy_class ? policy_class.new(user, record) : policy!(user, record)
63
71
 
64
- unless policy.public_send(query)
65
- raise NotAuthorizedError, query: query, record: record, policy: policy
66
- end
72
+ raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
67
73
 
68
- true
74
+ record
69
75
  end
70
76
 
71
77
  # Retrieves the policy scope for the given record.
72
78
  #
73
- # @see https://github.com/elabs/pundit#scopes
79
+ # @see https://github.com/varvet/pundit#scopes
74
80
  # @param user [Object] the user that initiated the action
75
- # @param record [Object] the object we're retrieving the policy scope for
81
+ # @param scope [Object] the object we're retrieving the policy scope for
82
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
76
83
  # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
77
84
  def policy_scope(user, scope)
78
- policy_scope = PolicyFinder.new(scope).scope
79
- policy_scope.new(user, scope).resolve if policy_scope
85
+ policy_scope_class = PolicyFinder.new(scope).scope
86
+ return unless policy_scope_class
87
+
88
+ begin
89
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
90
+ rescue ArgumentError
91
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
92
+ end
93
+
94
+ policy_scope.resolve
80
95
  end
81
96
 
82
97
  # Retrieves the policy scope for the given record.
83
98
  #
84
- # @see https://github.com/elabs/pundit#scopes
99
+ # @see https://github.com/varvet/pundit#scopes
85
100
  # @param user [Object] the user that initiated the action
86
- # @param record [Object] the object we're retrieving the policy scope for
101
+ # @param scope [Object] the object we're retrieving the policy scope for
87
102
  # @raise [NotDefinedError] if the policy scope cannot be found
103
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
88
104
  # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
89
105
  def policy_scope!(user, scope)
90
- PolicyFinder.new(scope).scope!.new(user, scope).resolve
106
+ policy_scope_class = PolicyFinder.new(scope).scope!
107
+ return unless policy_scope_class
108
+
109
+ begin
110
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
111
+ rescue ArgumentError
112
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
113
+ end
114
+
115
+ policy_scope.resolve
91
116
  end
92
117
 
93
118
  # Retrieves the policy for the given record.
94
119
  #
95
- # @see https://github.com/elabs/pundit#policies
120
+ # @see https://github.com/varvet/pundit#policies
96
121
  # @param user [Object] the user that initiated the action
97
122
  # @param record [Object] the object we're retrieving the policy for
123
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
98
124
  # @return [Object, nil] instance of policy class with query methods
99
125
  def policy(user, record)
100
126
  policy = PolicyFinder.new(record).policy
101
- policy.new(user, record) if policy
127
+ policy.new(user, pundit_model(record)) if policy
128
+ rescue ArgumentError
129
+ raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
102
130
  end
103
131
 
104
132
  # Retrieves the policy for the given record.
105
133
  #
106
- # @see https://github.com/elabs/pundit#policies
134
+ # @see https://github.com/varvet/pundit#policies
107
135
  # @param user [Object] the user that initiated the action
108
136
  # @param record [Object] the object we're retrieving the policy for
109
137
  # @raise [NotDefinedError] if the policy cannot be found
138
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
110
139
  # @return [Object] instance of policy class with query methods
111
140
  def policy!(user, record)
112
- PolicyFinder.new(record).policy!.new(user, record)
141
+ policy = PolicyFinder.new(record).policy!
142
+ policy.new(user, pundit_model(record))
143
+ rescue ArgumentError
144
+ raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
145
+ end
146
+
147
+ private
148
+
149
+ def pundit_model(record)
150
+ record.is_a?(Array) ? record.last : record
113
151
  end
114
152
  end
115
153
 
@@ -127,23 +165,10 @@ module Pundit
127
165
  helper_method :pundit_policy_scope
128
166
  helper_method :pundit_user
129
167
  end
130
- if respond_to?(:hide_action)
131
- hide_action :policy
132
- hide_action :policy_scope
133
- hide_action :policies
134
- hide_action :policy_scopes
135
- hide_action :authorize
136
- hide_action :verify_authorized
137
- hide_action :verify_policy_scoped
138
- hide_action :permitted_attributes
139
- hide_action :pundit_user
140
- hide_action :skip_authorization
141
- hide_action :skip_policy_scope
142
- hide_action :pundit_policy_authorized?
143
- hide_action :pundit_policy_scoped?
144
- end
145
168
  end
146
169
 
170
+ protected
171
+
147
172
  # @return [Boolean] whether authorization has been performed, i.e. whether
148
173
  # one {#authorize} or {#skip_authorization} has been called
149
174
  def pundit_policy_authorized?
@@ -160,7 +185,7 @@ module Pundit
160
185
  # `after_action` filter to prevent programmer error in forgetting to call
161
186
  # {#authorize} or {#skip_authorization}.
162
187
  #
163
- # @see https://github.com/elabs/pundit#ensuring-policies-are-used
188
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
164
189
  # @raise [AuthorizationNotPerformedError] if authorization has not been performed
165
190
  # @return [void]
166
191
  def verify_authorized
@@ -171,7 +196,7 @@ module Pundit
171
196
  # `after_action` filter to prevent programmer error in forgetting to call
172
197
  # {#policy_scope} or {#skip_policy_scope} in index actions.
173
198
  #
174
- # @see https://github.com/elabs/pundit#ensuring-policies-are-used
199
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
175
200
  # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
176
201
  # @return [void]
177
202
  def verify_policy_scoped
@@ -183,26 +208,26 @@ module Pundit
183
208
  # authorized to perform the given action.
184
209
  #
185
210
  # @param record [Object] the object we're checking permissions of
186
- # @param record [Symbol, nil] the query method to check on the policy (e.g. `:show?`)
211
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
212
+ # If omitted then this defaults to the Rails controller action name.
213
+ # @param policy_class [Class] the policy class we want to force use of
187
214
  # @raise [NotAuthorizedError] if the given query method returned false
188
- # @return [true] Always returns true
189
- def authorize(record, query = nil)
190
- query ||= params[:action].to_s + "?"
215
+ # @return [Object] Always returns the passed object record
216
+ def authorize(record, query = nil, policy_class: nil)
217
+ query ||= "#{action_name}?"
191
218
 
192
219
  @_pundit_policy_authorized = true
193
220
 
194
- policy = policy(record)
221
+ policy = policy_class ? policy_class.new(pundit_user, record) : policy(record)
195
222
 
196
- unless policy.public_send(query)
197
- raise NotAuthorizedError, query: query, record: record, policy: policy
198
- end
223
+ raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
199
224
 
200
- true
225
+ record
201
226
  end
202
227
 
203
228
  # Allow this action not to perform authorization.
204
229
  #
205
- # @see https://github.com/elabs/pundit#ensuring-policies-are-used
230
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
206
231
  # @return [void]
207
232
  def skip_authorization
208
233
  @_pundit_policy_authorized = true
@@ -210,7 +235,7 @@ module Pundit
210
235
 
211
236
  # Allow this action not to perform policy scoping.
212
237
  #
213
- # @see https://github.com/elabs/pundit#ensuring-policies-are-used
238
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
214
239
  # @return [void]
215
240
  def skip_policy_scope
216
241
  @_pundit_policy_scoped = true
@@ -218,17 +243,18 @@ module Pundit
218
243
 
219
244
  # Retrieves the policy scope for the given record.
220
245
  #
221
- # @see https://github.com/elabs/pundit#scopes
222
- # @param record [Object] the object we're retrieving the policy scope for
246
+ # @see https://github.com/varvet/pundit#scopes
247
+ # @param scope [Object] the object we're retrieving the policy scope for
248
+ # @param policy_scope_class [Class] the policy scope class we want to force use of
223
249
  # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
224
- def policy_scope(scope)
250
+ def policy_scope(scope, policy_scope_class: nil)
225
251
  @_pundit_policy_scoped = true
226
- pundit_policy_scope(scope)
252
+ policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
227
253
  end
228
254
 
229
255
  # Retrieves the policy for the given record.
230
256
  #
231
- # @see https://github.com/elabs/pundit#policies
257
+ # @see https://github.com/varvet/pundit#policies
232
258
  # @param record [Object] the object we're retrieving the policy for
233
259
  # @return [Object, nil] instance of policy class with query methods
234
260
  def policy(record)
@@ -237,42 +263,55 @@ module Pundit
237
263
 
238
264
  # Retrieves a set of permitted attributes from the policy by instantiating
239
265
  # the policy class for the given record and calling `permitted_attributes` on
240
- # it, or `permitted_attributes_for_{action}` if it is defined. It then infers
266
+ # it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers
241
267
  # what key the record should have in the params hash and retrieves the
242
268
  # permitted attributes from the params hash under that key.
243
269
  #
244
- # @see https://github.com/elabs/pundit#strong-parameters
270
+ # @see https://github.com/varvet/pundit#strong-parameters
245
271
  # @param record [Object] the object we're retrieving permitted attributes for
272
+ # @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
273
+ # If omitted then this defaults to the Rails controller action name.
246
274
  # @return [Hash{String => Object}] the permitted attributes
247
- def permitted_attributes(record, action = params[:action])
248
- param_key = PolicyFinder.new(record).param_key
275
+ def permitted_attributes(record, action = action_name)
249
276
  policy = policy(record)
250
277
  method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
251
278
  "permitted_attributes_for_#{action}"
252
279
  else
253
280
  "permitted_attributes"
254
281
  end
255
- params.require(param_key).permit(policy.public_send(method_name))
282
+ pundit_params_for(record).permit(*policy.public_send(method_name))
283
+ end
284
+
285
+ # Retrieves the params for the given record.
286
+ #
287
+ # @param record [Object] the object we're retrieving params for
288
+ # @return [ActionController::Parameters] the params
289
+ def pundit_params_for(record)
290
+ params.require(PolicyFinder.new(record).param_key)
256
291
  end
257
292
 
258
293
  # Cache of policies. You should not rely on this method.
259
294
  #
260
295
  # @api private
296
+ # rubocop:disable Naming/MemoizedInstanceVariableName
261
297
  def policies
262
298
  @_pundit_policies ||= {}
263
299
  end
300
+ # rubocop:enable Naming/MemoizedInstanceVariableName
264
301
 
265
302
  # Cache of policy scope. You should not rely on this method.
266
303
  #
267
304
  # @api private
305
+ # rubocop:disable Naming/MemoizedInstanceVariableName
268
306
  def policy_scopes
269
307
  @_pundit_policy_scopes ||= {}
270
308
  end
309
+ # rubocop:enable Naming/MemoizedInstanceVariableName
271
310
 
272
311
  # Hook method which allows customizing which user is passed to policies and
273
312
  # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
274
313
  #
275
- # @see https://github.com/elabs/pundit#customize-pundit-user
314
+ # @see https://github.com/varvet/pundit#customize-pundit-user
276
315
  # @return [Object] the user object to be used with pundit
277
316
  def pundit_user
278
317
  current_user
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pundit
2
4
  # Finds policy and scope classes for given object.
3
5
  # @api public
@@ -17,75 +19,69 @@ module Pundit
17
19
  end
18
20
 
19
21
  # @return [nil, Scope{#resolve}] scope class which can resolve to a scope
20
- # @see https://github.com/elabs/pundit#scopes
22
+ # @see https://github.com/varvet/pundit#scopes
21
23
  # @example
22
24
  # scope = finder.scope #=> UserPolicy::Scope
23
25
  # scope.resolve #=> <#ActiveRecord::Relation ...>
24
26
  #
25
27
  def scope
26
- policy::Scope if policy
27
- rescue NameError
28
- nil
28
+ "#{policy}::Scope".safe_constantize
29
29
  end
30
30
 
31
31
  # @return [nil, Class] policy class with query methods
32
- # @see https://github.com/elabs/pundit#policies
32
+ # @see https://github.com/varvet/pundit#policies
33
33
  # @example
34
34
  # policy = finder.policy #=> UserPolicy
35
35
  # policy.show? #=> true
36
36
  # policy.update? #=> false
37
37
  #
38
38
  def policy
39
- klass = find
40
- klass = klass.constantize if klass.is_a?(String)
41
- klass
42
- rescue NameError
43
- nil
39
+ klass = find(object)
40
+ klass.is_a?(String) ? klass.safe_constantize : klass
44
41
  end
45
42
 
46
43
  # @return [Scope{#resolve}] scope class which can resolve to a scope
47
44
  # @raise [NotDefinedError] if scope could not be determined
48
45
  #
49
46
  def scope!
50
- raise NotDefinedError, "unable to find policy scope of nil" if object.nil?
51
- scope or raise NotDefinedError, "unable to find scope `#{find}::Scope` for `#{object.inspect}`"
47
+ scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`"
52
48
  end
53
49
 
54
50
  # @return [Class] policy class with query methods
55
51
  # @raise [NotDefinedError] if policy could not be determined
56
52
  #
57
53
  def policy!
58
- raise NotDefinedError, "unable to find policy of nil" if object.nil?
59
- policy or raise NotDefinedError, "unable to find policy `#{find}` for `#{object.inspect}`"
54
+ policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
60
55
  end
61
56
 
62
57
  # @return [String] the name of the key this object would have in a params hash
63
58
  #
64
59
  def param_key
65
- if object.respond_to?(:model_name)
66
- object.model_name.param_key.to_s
67
- elsif object.is_a?(Class)
68
- object.to_s.demodulize.underscore
60
+ model = object.is_a?(Array) ? object.last : object
61
+
62
+ if model.respond_to?(:model_name)
63
+ model.model_name.param_key.to_s
64
+ elsif model.is_a?(Class)
65
+ model.to_s.demodulize.underscore
69
66
  else
70
- object.class.to_s.demodulize.underscore
67
+ model.class.to_s.demodulize.underscore
71
68
  end
72
69
  end
73
70
 
74
71
  private
75
72
 
76
- def find
77
- if object.nil?
78
- nil
79
- elsif object.respond_to?(:policy_class)
80
- object.policy_class
81
- elsif object.class.respond_to?(:policy_class)
82
- object.class.policy_class
73
+ def find(subject)
74
+ if subject.is_a?(Array)
75
+ modules = subject.dup
76
+ last = modules.pop
77
+ context = modules.map { |x| find_class_name(x) }.join("::")
78
+ [context, find(last)].join("::")
79
+ elsif subject.respond_to?(:policy_class)
80
+ subject.policy_class
81
+ elsif subject.class.respond_to?(:policy_class)
82
+ subject.class.policy_class
83
83
  else
84
- klass = if object.is_a?(Array)
85
- object.map { |x| find_class_name(x) }.join("::")
86
- else
87
- find_class_name(object)
88
- end
84
+ klass = find_class_name(subject)
89
85
  "#{klass}#{SUFFIX}"
90
86
  end
91
87
  end