pundit 1.1.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +29 -52
- data/.travis.yml +25 -10
- data/CHANGELOG.md +88 -0
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/README.md +304 -87
- data/Rakefile +2 -1
- data/config/rubocop-rspec.yml +5 -0
- data/lib/generators/pundit/install/install_generator.rb +4 -2
- data/lib/generators/pundit/install/templates/application_policy.rb +8 -8
- data/lib/generators/pundit/policy/policy_generator.rb +4 -2
- data/lib/generators/pundit/policy/templates/policy.rb +4 -3
- data/lib/generators/rspec/policy_generator.rb +4 -2
- data/lib/generators/rspec/templates/policy_spec.rb +1 -2
- data/lib/generators/test_unit/policy_generator.rb +4 -2
- data/lib/generators/test_unit/templates/policy_test.rb +0 -1
- data/lib/pundit/authorization.rb +168 -0
- data/lib/pundit/policy_finder.rb +28 -32
- data/lib/pundit/rspec.rb +13 -17
- data/lib/pundit/version.rb +3 -1
- data/lib/pundit.rb +76 -190
- data/pundit.gemspec +12 -9
- data/spec/authorization_spec.rb +258 -0
- data/spec/generators_spec.rb +43 -0
- data/spec/policies/post_policy_spec.rb +3 -1
- data/spec/policy_finder_spec.rb +187 -0
- data/spec/pundit_spec.rb +147 -196
- data/spec/spec_helper.rb +110 -30
- metadata +59 -25
data/README.md
CHANGED
@@ -1,37 +1,38 @@
|
|
1
1
|
# Pundit
|
2
2
|
|
3
|
-
[![Build Status](https://secure.travis-ci.org/
|
4
|
-
[![Code Climate](https://codeclimate.com/github/
|
5
|
-
[![Inline docs](http://inch-ci.org/github/
|
3
|
+
[![Build Status](https://secure.travis-ci.org/varvet/pundit.svg?branch=master)](https://travis-ci.org/varvet/pundit)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/varvet/pundit.svg)](https://codeclimate.com/github/varvet/pundit)
|
5
|
+
[![Inline docs](http://inch-ci.org/github/varvet/pundit.svg?branch=master)](http://inch-ci.org/github/varvet/pundit)
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/pundit.svg)](http://badge.fury.io/rb/pundit)
|
7
7
|
|
8
8
|
Pundit provides a set of helpers which guide you in leveraging regular Ruby
|
9
9
|
classes and object oriented design patterns to build a simple, robust and
|
10
|
-
|
10
|
+
scalable authorization system.
|
11
11
|
|
12
12
|
Links:
|
13
13
|
|
14
|
-
- [API documentation](http://www.rubydoc.info/gems/pundit)
|
15
|
-
- [Source Code](https://github.com/
|
16
|
-
- [Contributing](https://github.com/
|
17
|
-
- [Code of Conduct](https://github.com/
|
14
|
+
- [API documentation for the most recent version](http://www.rubydoc.info/gems/pundit)
|
15
|
+
- [Source Code](https://github.com/varvet/pundit)
|
16
|
+
- [Contributing](https://github.com/varvet/pundit/blob/master/CONTRIBUTING.md)
|
17
|
+
- [Code of Conduct](https://github.com/varvet/pundit/blob/master/CODE_OF_CONDUCT.md)
|
18
18
|
|
19
19
|
Sponsored by:
|
20
20
|
|
21
|
-
[<img src="
|
21
|
+
[<img src="https://www.varvet.com/images/wordmark-red.svg" alt="Varvet" height="50px"/>](https://www.varvet.com)
|
22
22
|
|
23
23
|
## Installation
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
> **Please note** that the README on GitHub is accurate with the _latest code on GitHub_. You are most likely using a released version of Pundit, so please refer to the [documentation for the latest released version of Pundit](https://www.rubydoc.info/gems/pundit).
|
26
|
+
|
27
|
+
``` sh
|
28
|
+
bundle add pundit
|
27
29
|
```
|
28
30
|
|
29
|
-
Include Pundit in your application controller:
|
31
|
+
Include `Pundit::Authorization` in your application controller:
|
30
32
|
|
31
33
|
``` ruby
|
32
34
|
class ApplicationController < ActionController::Base
|
33
|
-
include Pundit
|
34
|
-
protect_from_forgery
|
35
|
+
include Pundit::Authorization
|
35
36
|
end
|
36
37
|
```
|
37
38
|
|
@@ -61,7 +62,7 @@ class PostPolicy
|
|
61
62
|
end
|
62
63
|
|
63
64
|
def update?
|
64
|
-
user.admin?
|
65
|
+
user.admin? || !post.published?
|
65
66
|
end
|
66
67
|
end
|
67
68
|
```
|
@@ -116,7 +117,9 @@ and the given record. It then infers from the action name, that it should call
|
|
116
117
|
`authorize` would have done something like this:
|
117
118
|
|
118
119
|
``` ruby
|
119
|
-
|
120
|
+
unless PostPolicy.new(current_user, @post).update?
|
121
|
+
raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
|
122
|
+
end
|
120
123
|
```
|
121
124
|
|
122
125
|
You can pass a second argument to `authorize` if the name of the permission you
|
@@ -131,6 +134,18 @@ def publish
|
|
131
134
|
end
|
132
135
|
```
|
133
136
|
|
137
|
+
You can pass an argument to override the policy class if necessary. For example:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
def create
|
141
|
+
@publication = find_publication # assume this method returns any model that behaves like a publication
|
142
|
+
# @publication.class => Post
|
143
|
+
authorize @publication, policy_class: PublicationPolicy
|
144
|
+
@publication.publish!
|
145
|
+
redirect_to @publication
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
134
149
|
If you don't have an instance for the first argument to `authorize`, then you can pass
|
135
150
|
the class. For example:
|
136
151
|
|
@@ -151,6 +166,20 @@ def admin_list
|
|
151
166
|
end
|
152
167
|
```
|
153
168
|
|
169
|
+
`authorize` returns the instance passed to it, so you can chain it like this:
|
170
|
+
|
171
|
+
Controller:
|
172
|
+
```ruby
|
173
|
+
def show
|
174
|
+
@user = authorize User.find(params[:id])
|
175
|
+
end
|
176
|
+
|
177
|
+
# return the record even for namespaced policies
|
178
|
+
def show
|
179
|
+
@user = authorize [:admin, User.find(params[:id])]
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
154
183
|
You can easily get a hold of an instance of the policy through the `policy`
|
155
184
|
method in both the view and controller. This is especially useful for
|
156
185
|
conditionally showing links or buttons in the view:
|
@@ -167,14 +196,30 @@ you can retrieve it by passing a symbol.
|
|
167
196
|
|
168
197
|
```ruby
|
169
198
|
# app/policies/dashboard_policy.rb
|
170
|
-
class DashboardPolicy
|
171
|
-
|
199
|
+
class DashboardPolicy
|
200
|
+
attr_reader :user
|
201
|
+
|
202
|
+
# _record in this example will just be :dashboard
|
203
|
+
def initialize(user, _record)
|
204
|
+
@user = user
|
205
|
+
end
|
206
|
+
|
207
|
+
def show?
|
208
|
+
user.admin?
|
209
|
+
end
|
172
210
|
end
|
173
211
|
```
|
174
212
|
|
213
|
+
Note that the headless policy still needs to accept two arguments. The
|
214
|
+
second argument will just be the symbol `:dashboard` in this case which
|
215
|
+
is what is passed as the record to `authorize` below.
|
216
|
+
|
175
217
|
```ruby
|
176
218
|
# In controllers
|
177
|
-
|
219
|
+
def show
|
220
|
+
authorize :dashboard, :show?
|
221
|
+
...
|
222
|
+
end
|
178
223
|
```
|
179
224
|
|
180
225
|
```erb
|
@@ -184,54 +229,6 @@ authorize :dashboard, :show?
|
|
184
229
|
<% end %>
|
185
230
|
```
|
186
231
|
|
187
|
-
## Ensuring policies are used
|
188
|
-
|
189
|
-
Pundit adds a method called `verify_authorized` to your controllers. This
|
190
|
-
method will raise an exception if `authorize` has not yet been called. You
|
191
|
-
should run this method in an `after_action` to ensure that you haven't
|
192
|
-
forgotten to authorize the action. For example:
|
193
|
-
|
194
|
-
``` ruby
|
195
|
-
class ApplicationController < ActionController::Base
|
196
|
-
after_action :verify_authorized
|
197
|
-
end
|
198
|
-
```
|
199
|
-
|
200
|
-
Likewise, Pundit also adds `verify_policy_scoped` to your controller. This
|
201
|
-
will raise an exception in the vein of `verify_authorized`. However, it tracks
|
202
|
-
if `policy_scope` is used instead of `authorize`. This is mostly useful for
|
203
|
-
controller actions like `index` which find collections with a scope and don't
|
204
|
-
authorize individual instances.
|
205
|
-
|
206
|
-
``` ruby
|
207
|
-
class ApplicationController < ActionController::Base
|
208
|
-
after_action :verify_authorized, except: :index
|
209
|
-
after_action :verify_policy_scoped, only: :index
|
210
|
-
end
|
211
|
-
```
|
212
|
-
|
213
|
-
If you're using `verify_authorized` in your controllers but need to
|
214
|
-
conditionally bypass verification, you can use `skip_authorization`. For
|
215
|
-
bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful
|
216
|
-
in circumstances where you don't want to disable verification for the
|
217
|
-
entire action, but have some cases where you intend to not authorize.
|
218
|
-
|
219
|
-
```ruby
|
220
|
-
def show
|
221
|
-
record = Record.find_by(attribute: "value")
|
222
|
-
if record.present?
|
223
|
-
authorize record
|
224
|
-
else
|
225
|
-
skip_authorization
|
226
|
-
end
|
227
|
-
end
|
228
|
-
```
|
229
|
-
|
230
|
-
If you need to perform some more sophisticated logic or you want to raise a custom
|
231
|
-
exception you can use the two lower level methods `pundit_policy_authorized?`
|
232
|
-
and `pundit_policy_scoped?` which return `true` or `false` depending on whether
|
233
|
-
`authorize` or `policy_scope` have been called, respectively.
|
234
|
-
|
235
232
|
## Scopes
|
236
233
|
|
237
234
|
Often, you will want to have some kind of view listing records which a
|
@@ -241,8 +238,6 @@ define a class called a policy scope. It can look something like this:
|
|
241
238
|
``` ruby
|
242
239
|
class PostPolicy < ApplicationPolicy
|
243
240
|
class Scope
|
244
|
-
attr_reader :user, :scope
|
245
|
-
|
246
241
|
def initialize(user, scope)
|
247
242
|
@user = user
|
248
243
|
@scope = scope
|
@@ -255,10 +250,14 @@ class PostPolicy < ApplicationPolicy
|
|
255
250
|
scope.where(published: true)
|
256
251
|
end
|
257
252
|
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
attr_reader :user, :scope
|
258
257
|
end
|
259
258
|
|
260
259
|
def update?
|
261
|
-
user.admin? or not
|
260
|
+
user.admin? or not record.published?
|
262
261
|
end
|
263
262
|
end
|
264
263
|
```
|
@@ -291,7 +290,7 @@ class PostPolicy < ApplicationPolicy
|
|
291
290
|
end
|
292
291
|
|
293
292
|
def update?
|
294
|
-
user.admin? or not
|
293
|
+
user.admin? or not record.published?
|
295
294
|
end
|
296
295
|
end
|
297
296
|
```
|
@@ -302,15 +301,26 @@ You can now use this class from your controller via the `policy_scope` method:
|
|
302
301
|
def index
|
303
302
|
@posts = policy_scope(Post)
|
304
303
|
end
|
304
|
+
|
305
|
+
def show
|
306
|
+
@post = policy_scope(Post).find(params[:id])
|
307
|
+
end
|
308
|
+
```
|
309
|
+
|
310
|
+
Like with the authorize method, you can also override the policy scope class:
|
311
|
+
|
312
|
+
``` ruby
|
313
|
+
def index
|
314
|
+
# publication_class => Post
|
315
|
+
@publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
|
316
|
+
end
|
305
317
|
```
|
306
318
|
|
307
|
-
|
308
|
-
the `PostPolicy::Scope` class, it will instantiate this class and call
|
309
|
-
`resolve` on the instance. In this case it is a shortcut for doing:
|
319
|
+
In this case it is a shortcut for doing:
|
310
320
|
|
311
321
|
``` ruby
|
312
322
|
def index
|
313
|
-
@
|
323
|
+
@publications = PublicationPolicy::Scope.new(current_user, Post).resolve
|
314
324
|
end
|
315
325
|
```
|
316
326
|
|
@@ -322,6 +332,70 @@ You can, and are encouraged to, use this method in views:
|
|
322
332
|
<% end %>
|
323
333
|
```
|
324
334
|
|
335
|
+
## Ensuring policies and scopes are used
|
336
|
+
|
337
|
+
When you are developing an application with Pundit it can be easy to forget to
|
338
|
+
authorize some action. People are forgetful after all. Since Pundit encourages
|
339
|
+
you to add the `authorize` call manually to each controller action, it's really
|
340
|
+
easy to miss one.
|
341
|
+
|
342
|
+
Thankfully, Pundit has a handy feature which reminds you in case you forget.
|
343
|
+
Pundit tracks whether you have called `authorize` anywhere in your controller
|
344
|
+
action. Pundit also adds a method to your controllers called
|
345
|
+
`verify_authorized`. This method will raise an exception if `authorize` has not
|
346
|
+
yet been called. You should run this method in an `after_action` hook to ensure
|
347
|
+
that you haven't forgotten to authorize the action. For example:
|
348
|
+
|
349
|
+
``` ruby
|
350
|
+
class ApplicationController < ActionController::Base
|
351
|
+
include Pundit::Authorization
|
352
|
+
after_action :verify_authorized
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
Likewise, Pundit also adds `verify_policy_scoped` to your controller. This
|
357
|
+
will raise an exception similar to `verify_authorized`. However, it tracks
|
358
|
+
if `policy_scope` is used instead of `authorize`. This is mostly useful for
|
359
|
+
controller actions like `index` which find collections with a scope and don't
|
360
|
+
authorize individual instances.
|
361
|
+
|
362
|
+
``` ruby
|
363
|
+
class ApplicationController < ActionController::Base
|
364
|
+
include Pundit::Authorization
|
365
|
+
after_action :verify_authorized, except: :index
|
366
|
+
after_action :verify_policy_scoped, only: :index
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
**This verification mechanism only exists to aid you while developing your
|
371
|
+
application, so you don't forget to call `authorize`. It is not some kind of
|
372
|
+
failsafe mechanism or authorization mechanism. You should be able to remove
|
373
|
+
these filters without affecting how your app works in any way.**
|
374
|
+
|
375
|
+
Some people have found this feature confusing, while many others
|
376
|
+
find it extremely helpful. If you fall into the category of people who find it
|
377
|
+
confusing then you do not need to use it. Pundit will work just fine without
|
378
|
+
using `verify_authorized` and `verify_policy_scoped`.
|
379
|
+
|
380
|
+
### Conditional verification
|
381
|
+
|
382
|
+
If you're using `verify_authorized` in your controllers but need to
|
383
|
+
conditionally bypass verification, you can use `skip_authorization`. For
|
384
|
+
bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful
|
385
|
+
in circumstances where you don't want to disable verification for the
|
386
|
+
entire action, but have some cases where you intend to not authorize.
|
387
|
+
|
388
|
+
```ruby
|
389
|
+
def show
|
390
|
+
record = Record.find_by(attribute: "value")
|
391
|
+
if record.present?
|
392
|
+
authorize record
|
393
|
+
else
|
394
|
+
skip_authorization
|
395
|
+
end
|
396
|
+
end
|
397
|
+
```
|
398
|
+
|
325
399
|
## Manually specifying policy classes
|
326
400
|
|
327
401
|
Sometimes you might want to explicitly declare which policy to use for a given
|
@@ -335,6 +409,16 @@ class Post
|
|
335
409
|
end
|
336
410
|
```
|
337
411
|
|
412
|
+
Alternatively, you can declare an instance method:
|
413
|
+
|
414
|
+
``` ruby
|
415
|
+
class Post
|
416
|
+
def policy_class
|
417
|
+
PostablePolicy
|
418
|
+
end
|
419
|
+
end
|
420
|
+
```
|
421
|
+
|
338
422
|
## Just plain old Ruby
|
339
423
|
|
340
424
|
As you can see, Pundit doesn't do anything you couldn't have easily done
|
@@ -362,7 +446,8 @@ rails g pundit:policy post
|
|
362
446
|
|
363
447
|
In many applications, only logged in users are really able to do anything. If
|
364
448
|
you're building such a system, it can be kind of cumbersome to check that the
|
365
|
-
user in a policy isn't `nil` for every single permission.
|
449
|
+
user in a policy isn't `nil` for every single permission. Aside from policies,
|
450
|
+
you can add this check to the base class for scopes.
|
366
451
|
|
367
452
|
We suggest that you define a filter that redirects unauthenticated users to the
|
368
453
|
login page. As a secondary defence, if you've defined an ApplicationPolicy, it
|
@@ -376,6 +461,37 @@ class ApplicationPolicy
|
|
376
461
|
@user = user
|
377
462
|
@record = record
|
378
463
|
end
|
464
|
+
|
465
|
+
class Scope
|
466
|
+
attr_reader :user, :scope
|
467
|
+
|
468
|
+
def initialize(user, scope)
|
469
|
+
raise Pundit::NotAuthorizedError, "must be logged in" unless user
|
470
|
+
@user = user
|
471
|
+
@scope = scope
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
```
|
476
|
+
|
477
|
+
## NilClassPolicy
|
478
|
+
|
479
|
+
To support a [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern)
|
480
|
+
you may find that you want to implement a `NilClassPolicy`. This might be useful
|
481
|
+
where you want to extend your ApplicationPolicy to allow some tolerance of, for
|
482
|
+
example, associations which might be `nil`.
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
class NilClassPolicy < ApplicationPolicy
|
486
|
+
class Scope < Scope
|
487
|
+
def resolve
|
488
|
+
raise Pundit::NotDefinedError, "Cannot scope NilClass"
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def show?
|
493
|
+
false # Nobody can see nothing
|
494
|
+
end
|
379
495
|
end
|
380
496
|
```
|
381
497
|
|
@@ -388,8 +504,7 @@ method in every controller.
|
|
388
504
|
|
389
505
|
```ruby
|
390
506
|
class ApplicationController < ActionController::Base
|
391
|
-
|
392
|
-
include Pundit
|
507
|
+
include Pundit::Authorization
|
393
508
|
|
394
509
|
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
395
510
|
|
@@ -397,11 +512,15 @@ class ApplicationController < ActionController::Base
|
|
397
512
|
|
398
513
|
def user_not_authorized
|
399
514
|
flash[:alert] = "You are not authorized to perform this action."
|
400
|
-
|
515
|
+
redirect_back(fallback_location: root_path)
|
401
516
|
end
|
402
517
|
end
|
403
518
|
```
|
404
519
|
|
520
|
+
Alternatively, you can globally handle Pundit::NotAuthorizedError's by having rails handle them as a 403 error and serving a 403 error page. Add the following to application.rb:
|
521
|
+
|
522
|
+
```config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden```
|
523
|
+
|
405
524
|
## Creating custom error messages
|
406
525
|
|
407
526
|
`NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
|
@@ -422,7 +541,7 @@ class ApplicationController < ActionController::Base
|
|
422
541
|
policy_name = exception.policy.class.to_s.underscore
|
423
542
|
|
424
543
|
flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
|
425
|
-
|
544
|
+
redirect_back(fallback_url: root_path)
|
426
545
|
end
|
427
546
|
end
|
428
547
|
```
|
@@ -469,6 +588,47 @@ def pundit_user
|
|
469
588
|
end
|
470
589
|
```
|
471
590
|
|
591
|
+
## Policy Namespacing
|
592
|
+
In some cases it might be helpful to have multiple policies that serve different contexts for a
|
593
|
+
resource. A prime example of this is the case where User policies differ from Admin policies. To
|
594
|
+
authorize with a namespaced policy, pass the namespace into the `authorize` helper in an array:
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
authorize(post) # => will look for a PostPolicy
|
598
|
+
authorize([:admin, post]) # => will look for an Admin::PostPolicy
|
599
|
+
authorize([:foo, :bar, post]) # => will look for a Foo::Bar::PostPolicy
|
600
|
+
|
601
|
+
policy_scope(Post) # => will look for a PostPolicy::Scope
|
602
|
+
policy_scope([:admin, Post]) # => will look for an Admin::PostPolicy::Scope
|
603
|
+
policy_scope([:foo, :bar, Post]) # => will look for a Foo::Bar::PostPolicy::Scope
|
604
|
+
```
|
605
|
+
|
606
|
+
If you are using namespaced policies for something like Admin views, it can be useful to
|
607
|
+
override the `policy_scope` and `authorize` helpers in your `AdminController` to automatically
|
608
|
+
apply the namespacing:
|
609
|
+
|
610
|
+
```ruby
|
611
|
+
class AdminController < ApplicationController
|
612
|
+
def policy_scope(scope)
|
613
|
+
super([:admin, scope])
|
614
|
+
end
|
615
|
+
|
616
|
+
def authorize(record, query = nil)
|
617
|
+
super([:admin, record], query)
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
class Admin::PostController < AdminController
|
622
|
+
def index
|
623
|
+
policy_scope(Post)
|
624
|
+
end
|
625
|
+
|
626
|
+
def show
|
627
|
+
post = authorize Post.find(params[:id])
|
628
|
+
end
|
629
|
+
end
|
630
|
+
```
|
631
|
+
|
472
632
|
## Additional context
|
473
633
|
|
474
634
|
Pundit strongly encourages you to model your application in such a way that the
|
@@ -497,7 +657,7 @@ class UserContext
|
|
497
657
|
end
|
498
658
|
|
499
659
|
class ApplicationController
|
500
|
-
include Pundit
|
660
|
+
include Pundit::Authorization
|
501
661
|
|
502
662
|
def pundit_user
|
503
663
|
UserContext.new(current_user, request.ip)
|
@@ -507,9 +667,8 @@ end
|
|
507
667
|
|
508
668
|
## Strong parameters
|
509
669
|
|
510
|
-
In Rails
|
511
|
-
|
512
|
-
mass-assignment protection is handled in the controller. With Pundit you can
|
670
|
+
In Rails,
|
671
|
+
mass-assignment protection is handled in the controller. With Pundit you can
|
513
672
|
control which attributes a user has access to update via your policies. You can
|
514
673
|
set up a `permitted_attributes` method in your policy like this:
|
515
674
|
|
@@ -533,7 +692,7 @@ You can now retrieve these attributes from the policy:
|
|
533
692
|
class PostsController < ApplicationController
|
534
693
|
def update
|
535
694
|
@post = Post.find(params[:id])
|
536
|
-
if @post.
|
695
|
+
if @post.update(post_params)
|
537
696
|
redirect_to @post
|
538
697
|
else
|
539
698
|
render :edit
|
@@ -555,7 +714,7 @@ However, this is a bit cumbersome, so Pundit provides a convenient helper method
|
|
555
714
|
class PostsController < ApplicationController
|
556
715
|
def update
|
557
716
|
@post = Post.find(params[:id])
|
558
|
-
if @post.
|
717
|
+
if @post.update(permitted_attributes(@post))
|
559
718
|
redirect_to @post
|
560
719
|
else
|
561
720
|
render :edit
|
@@ -564,6 +723,45 @@ class PostsController < ApplicationController
|
|
564
723
|
end
|
565
724
|
```
|
566
725
|
|
726
|
+
If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy:
|
727
|
+
|
728
|
+
```ruby
|
729
|
+
# app/policies/post_policy.rb
|
730
|
+
class PostPolicy < ApplicationPolicy
|
731
|
+
def permitted_attributes_for_create
|
732
|
+
[:title, :body]
|
733
|
+
end
|
734
|
+
|
735
|
+
def permitted_attributes_for_edit
|
736
|
+
[:body]
|
737
|
+
end
|
738
|
+
end
|
739
|
+
```
|
740
|
+
|
741
|
+
If you have defined an action-specific method on your policy for the current action, the `permitted_attributes` helper will call it instead of calling `permitted_attributes` on your controller.
|
742
|
+
|
743
|
+
If you need to fetch parameters based on namespaces different from the suggested one, override the below method, in your controller, and return an instance of `ActionController::Parameters`.
|
744
|
+
|
745
|
+
```ruby
|
746
|
+
def pundit_params_for(record)
|
747
|
+
params.require(PolicyFinder.new(record).param_key)
|
748
|
+
end
|
749
|
+
```
|
750
|
+
|
751
|
+
For example:
|
752
|
+
|
753
|
+
```ruby
|
754
|
+
# If you don't want to use require
|
755
|
+
def pundit_params_for(record)
|
756
|
+
params.fetch(PolicyFinder.new(record).param_key, {})
|
757
|
+
end
|
758
|
+
|
759
|
+
# If you are using something like the JSON API spec
|
760
|
+
def pundit_params_for(_record)
|
761
|
+
params.fetch(:data, {}).fetch(:attributes, {})
|
762
|
+
end
|
763
|
+
```
|
764
|
+
|
567
765
|
## RSpec
|
568
766
|
|
569
767
|
### Policy Specs
|
@@ -600,14 +798,33 @@ end
|
|
600
798
|
An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
|
601
799
|
[excellent post](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) and implemented in the third party [pundit-matchers](https://github.com/chrisalley/pundit-matchers) gem.
|
602
800
|
|
801
|
+
### Scope Specs
|
802
|
+
|
803
|
+
Pundit does not provide a DSL for testing scopes. Just test it like a regular Ruby class!
|
804
|
+
|
805
|
+
### Linting with RuboCop RSpec
|
806
|
+
|
807
|
+
When you lint your RSpec spec files with `rubocop-rspec`, it will fail to properly detect RSpec constructs that Pundit defines, `permissions`.
|
808
|
+
Make sure to use `rubocop-rspec` 2.0 or newer and add the following to your `.rubocop.yml`:
|
809
|
+
|
810
|
+
```yaml
|
811
|
+
inherit_gem:
|
812
|
+
pundit: config/rubocop-rspec.yml
|
813
|
+
```
|
814
|
+
|
603
815
|
# External Resources
|
604
816
|
|
605
817
|
- [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
|
606
818
|
- [Migrating to Pundit from CanCan](http://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
|
607
819
|
- [Testing Pundit Policies with RSpec](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
|
608
|
-
- [
|
820
|
+
- [Testing Pundit with Minitest](https://github.com/varvet/pundit/issues/204#issuecomment-60166450)
|
821
|
+
- [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136)
|
609
822
|
- [Straightforward Rails Authorization with Pundit](http://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
|
610
823
|
|
824
|
+
## Other implementations
|
825
|
+
|
826
|
+
- [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](http://flask.pocoo.org/) extension "heavily inspired by" Pundit
|
827
|
+
|
611
828
|
# License
|
612
829
|
|
613
830
|
Licensed under the MIT license, see the separate LICENSE.txt file.
|
data/Rakefile
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Pundit
|
2
4
|
module Generators
|
3
5
|
class InstallGenerator < ::Rails::Generators::Base
|
4
|
-
source_root File.expand_path(
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
5
7
|
|
6
8
|
def copy_application_policy
|
7
|
-
template
|
9
|
+
template "application_policy.rb", "app/policies/application_policy.rb"
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class ApplicationPolicy
|
2
4
|
attr_reader :user, :record
|
3
5
|
|
@@ -11,7 +13,7 @@ class ApplicationPolicy
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def show?
|
14
|
-
|
16
|
+
false
|
15
17
|
end
|
16
18
|
|
17
19
|
def create?
|
@@ -34,20 +36,18 @@ class ApplicationPolicy
|
|
34
36
|
false
|
35
37
|
end
|
36
38
|
|
37
|
-
def scope
|
38
|
-
Pundit.policy_scope!(user, record.class)
|
39
|
-
end
|
40
|
-
|
41
39
|
class Scope
|
42
|
-
attr_reader :user, :scope
|
43
|
-
|
44
40
|
def initialize(user, scope)
|
45
41
|
@user = user
|
46
42
|
@scope = scope
|
47
43
|
end
|
48
44
|
|
49
45
|
def resolve
|
50
|
-
|
46
|
+
raise NotImplementedError, "You must define #resolve in #{self.class}"
|
51
47
|
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_reader :user, :scope
|
52
52
|
end
|
53
53
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Pundit
|
2
4
|
module Generators
|
3
5
|
class PolicyGenerator < ::Rails::Generators::NamedBase
|
4
|
-
source_root File.expand_path(
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
5
7
|
|
6
8
|
def create_policy
|
7
|
-
template
|
9
|
+
template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb")
|
8
10
|
end
|
9
11
|
|
10
12
|
hook_for :test_framework
|