• Home
  • History
  • Annotate
  • current directory
Name Date Size #Lines LOC

..16-Aug-2019-

lib/H16-Aug-2019-

spec/H16-Aug-2019-

.gitignoreH A D16-Aug-2019158 1918

.rubocop.ymlH A D16-Aug-20191.6 KiB10474

.travis.ymlH A D16-Aug-2019397 2119

.yardoptsH A D16-Aug-201950 21

CHANGELOG.mdH A D16-Aug-20193.1 KiB8054

CODE_OF_CONDUCT.mdH A D16-Aug-20191.4 KiB2922

CONTRIBUTING.mdH A D16-Aug-20191.2 KiB3422

GemfileH A D16-Aug-2019215 1713

README.mdH A D16-Aug-201922.6 KiB791620

RakefileH A D16-Aug-2019330 1914

pundit.gemspecH A D16-Aug-20192.3 KiB3428

README.md

1# Pundit
2
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[![Gem Version](https://badge.fury.io/rb/pundit.svg)](http://badge.fury.io/rb/pundit)
7
8Pundit provides a set of helpers which guide you in leveraging regular Ruby
9classes and object oriented design patterns to build a simple, robust and
10scaleable authorization system.
11
12Links:
13
14- [API documentation](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
19Sponsored by:
20
21[<img src="https://www.varvet.com/images/wordmark-red.svg" alt="Varvet" height="50px"/>](https://www.varvet.com)
22
23## Installation
24
25``` ruby
26gem "pundit"
27```
28
29Include Pundit in your application controller:
30
31``` ruby
32class ApplicationController < ActionController::Base
33  include Pundit
34  protect_from_forgery
35end
36```
37
38Optionally, you can run the generator, which will set up an application policy
39with some useful defaults for you:
40
41``` sh
42rails g pundit:install
43```
44
45After generating your application policy, restart the Rails server so that Rails
46can pick up any classes in the new `app/policies/` directory.
47
48## Policies
49
50Pundit is focused around the notion of policy classes. We suggest that you put
51these classes in `app/policies`. This is a simple example that allows updating
52a post if the user is an admin, or if the post is unpublished:
53
54``` ruby
55class PostPolicy
56  attr_reader :user, :post
57
58  def initialize(user, post)
59    @user = user
60    @post = post
61  end
62
63  def update?
64    user.admin? or not post.published?
65  end
66end
67```
68
69As you can see, this is just a plain Ruby class. Pundit makes the following
70assumptions about this class:
71
72- The class has the same name as some kind of model class, only suffixed
73  with the word "Policy".
74- The first argument is a user. In your controller, Pundit will call the
75  `current_user` method to retrieve what to send into this argument
76- The second argument is some kind of model object, whose authorization
77  you want to check. This does not need to be an ActiveRecord or even
78  an ActiveModel object, it can be anything really.
79- The class implements some kind of query method, in this case `update?`.
80  Usually, this will map to the name of a particular controller action.
81
82That's it really.
83
84Usually you'll want to inherit from the application policy created by the
85generator, or set up your own base class to inherit from:
86
87``` ruby
88class PostPolicy < ApplicationPolicy
89  def update?
90    user.admin? or not record.published?
91  end
92end
93```
94
95In the generated `ApplicationPolicy`, the model object is called `record`.
96
97Supposing that you have an instance of class `Post`, Pundit now lets you do
98this in your controller:
99
100``` ruby
101def update
102  @post = Post.find(params[:id])
103  authorize @post
104  if @post.update(post_params)
105    redirect_to @post
106  else
107    render :edit
108  end
109end
110```
111
112The authorize method automatically infers that `Post` will have a matching
113`PostPolicy` class, and instantiates this class, handing in the current user
114and the given record. It then infers from the action name, that it should call
115`update?` on this instance of the policy. In this case, you can imagine that
116`authorize` would have done something like this:
117
118``` ruby
119unless PostPolicy.new(current_user, @post).update?
120  raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
121end
122```
123
124You can pass a second argument to `authorize` if the name of the permission you
125want to check doesn't match the action name. For example:
126
127``` ruby
128def publish
129  @post = Post.find(params[:id])
130  authorize @post, :update?
131  @post.publish!
132  redirect_to @post
133end
134```
135
136You can pass an argument to override the policy class if necessary. For example:
137
138```ruby
139def create
140  @publication = find_publication # assume this method returns any model that behaves like a publication
141  # @publication.class => Post
142  authorize @publication, policy_class: PublicationPolicy
143  @publication.publish!
144  redirect_to @publication
145end
146```
147
148If you don't have an instance for the first argument to `authorize`, then you can pass
149the class. For example:
150
151Policy:
152```ruby
153class PostPolicy < ApplicationPolicy
154  def admin_list?
155    user.admin?
156  end
157end
158```
159
160Controller:
161```ruby
162def admin_list
163  authorize Post # we don't have a particular post to authorize
164  # Rest of controller action
165end
166```
167
168`authorize` returns the object passed to it, so you can chain it like this:
169
170Controller:
171```ruby
172def show
173  @user = authorize User.find(params[:id])
174end
175```
176
177You can easily get a hold of an instance of the policy through the `policy`
178method in both the view and controller. This is especially useful for
179conditionally showing links or buttons in the view:
180
181``` erb
182<% if policy(@post).update? %>
183  <%= link_to "Edit post", edit_post_path(@post) %>
184<% end %>
185```
186## Headless policies
187
188Given there is a policy without a corresponding model / ruby class,
189you can retrieve it by passing a symbol.
190
191```ruby
192# app/policies/dashboard_policy.rb
193class DashboardPolicy < Struct.new(:user, :dashboard)
194  # ...
195end
196```
197
198Note that the headless policy still needs to accept two arguments. The
199second argument will just be the symbol `:dashboard` in this case which
200is what is passed as the record to `authorize` below.
201
202```ruby
203# In controllers
204authorize :dashboard, :show?
205```
206
207```erb
208# In views
209<% if policy(:dashboard).show? %>
210  <%= link_to 'Dashboard', dashboard_path %>
211<% end %>
212```
213
214## Scopes
215
216Often, you will want to have some kind of view listing records which a
217particular user has access to. When using Pundit, you are expected to
218define a class called a policy scope. It can look something like this:
219
220``` ruby
221class PostPolicy < ApplicationPolicy
222  class Scope
223    attr_reader :user, :scope
224
225    def initialize(user, scope)
226      @user  = user
227      @scope = scope
228    end
229
230    def resolve
231      if user.admin?
232        scope.all
233      else
234        scope.where(published: true)
235      end
236    end
237  end
238
239  def update?
240    user.admin? or not record.published?
241  end
242end
243```
244
245Pundit makes the following assumptions about this class:
246
247- The class has the name `Scope` and is nested under the policy class.
248- The first argument is a user. In your controller, Pundit will call the
249  `current_user` method to retrieve what to send into this argument.
250- The second argument is a scope of some kind on which to perform some kind of
251  query. It will usually be an ActiveRecord class or a
252  `ActiveRecord::Relation`, but it could be something else entirely.
253- Instances of this class respond to the method `resolve`, which should return
254  some kind of result which can be iterated over. For ActiveRecord classes,
255  this would usually be an `ActiveRecord::Relation`.
256
257You'll probably want to inherit from the application policy scope generated by the
258generator, or create your own base class to inherit from:
259
260``` ruby
261class PostPolicy < ApplicationPolicy
262  class Scope < Scope
263    def resolve
264      if user.admin?
265        scope.all
266      else
267        scope.where(published: true)
268      end
269    end
270  end
271
272  def update?
273    user.admin? or not record.published?
274  end
275end
276```
277
278You can now use this class from your controller via the `policy_scope` method:
279
280``` ruby
281def index
282  @posts = policy_scope(Post)
283end
284
285def show
286  @post = policy_scope(Post).find(params[:id])
287end
288```
289
290Like with the authorize method, you can also override the policy scope class:
291
292``` ruby
293def index
294  # publication_class => Post
295  @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
296end
297```
298
299Just as with your policy, this will automatically infer that you want to use
300the `PostPolicy::Scope` class, it will instantiate this class and call
301`resolve` on the instance. In this case it is a shortcut for doing:
302
303``` ruby
304def index
305  @posts = PostPolicy::Scope.new(current_user, Post).resolve
306end
307```
308
309You can, and are encouraged to, use this method in views:
310
311``` erb
312<% policy_scope(@user.posts).each do |post| %>
313  <p><%= link_to post.title, post_path(post) %></p>
314<% end %>
315```
316
317## Ensuring policies and scopes are used
318
319When you are developing an application with Pundit it can be easy to forget to
320authorize some action. People are forgetful after all. Since Pundit encourages
321you to add the `authorize` call manually to each controller action, it's really
322easy to miss one.
323
324Thankfully, Pundit has a handy feature which reminds you in case you forget.
325Pundit tracks whether you have called `authorize` anywhere in your controller
326action. Pundit also adds a method to your controllers called
327`verify_authorized`. This method will raise an exception if `authorize` has not
328yet been called. You should run this method in an `after_action` hook to ensure
329that you haven't forgotten to authorize the action. For example:
330
331``` ruby
332class ApplicationController < ActionController::Base
333  include Pundit
334  after_action :verify_authorized
335end
336```
337
338Likewise, Pundit also adds `verify_policy_scoped` to your controller. This
339will raise an exception similar to `verify_authorized`. However, it tracks
340if `policy_scope` is used instead of `authorize`. This is mostly useful for
341controller actions like `index` which find collections with a scope and don't
342authorize individual instances.
343
344``` ruby
345class ApplicationController < ActionController::Base
346  include Pundit
347  after_action :verify_authorized, except: :index
348  after_action :verify_policy_scoped, only: :index
349end
350```
351
352**This verification mechanism only exists to aid you while developing your
353application, so you don't forget to call `authorize`. It is not some kind of
354failsafe mechanism or authorization mechanism. You should be able to remove
355these filters without affecting how your app works in any way.**
356
357Some people have found this feature confusing, while many others
358find it extremely helpful. If you fall into the category of people who find it
359confusing then you do not need to use it. Pundit will work just fine without
360using `verify_authorized` and `verify_policy_scoped`.
361
362### Conditional verification
363
364If you're using `verify_authorized` in your controllers but need to
365conditionally bypass verification, you can use `skip_authorization`. For
366bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful
367in circumstances where you don't want to disable verification for the
368entire action, but have some cases where you intend to not authorize.
369
370```ruby
371def show
372  record = Record.find_by(attribute: "value")
373  if record.present?
374    authorize record
375  else
376    skip_authorization
377  end
378end
379```
380
381## Manually specifying policy classes
382
383Sometimes you might want to explicitly declare which policy to use for a given
384class, instead of letting Pundit infer it. This can be done like so:
385
386``` ruby
387class Post
388  def self.policy_class
389    PostablePolicy
390  end
391end
392```
393
394## Just plain old Ruby
395
396As you can see, Pundit doesn't do anything you couldn't have easily done
397yourself.  It's a very small library, it just provides a few neat helpers.
398Together these give you the power of building a well structured, fully working
399authorization system without using any special DSLs or funky syntax or
400anything.
401
402Remember that all of the policy and scope classes are just plain Ruby classes,
403which means you can use the same mechanisms you always use to DRY things up.
404Encapsulate a set of permissions into a module and include them in multiple
405policies. Use `alias_method` to make some permissions behave the same as
406others. Inherit from a base set of permissions. Use metaprogramming if you
407really have to.
408
409## Generator
410
411Use the supplied generator to generate policies:
412
413``` sh
414rails g pundit:policy post
415```
416
417## Closed systems
418
419In many applications, only logged in users are really able to do anything. If
420you're building such a system, it can be kind of cumbersome to check that the
421user in a policy isn't `nil` for every single permission. Aside from policies,
422you can add this check to the base class for scopes.
423
424We suggest that you define a filter that redirects unauthenticated users to the
425login page. As a secondary defence, if you've defined an ApplicationPolicy, it
426might be a good idea to raise an exception if somehow an unauthenticated user
427got through. This way you can fail more gracefully.
428
429``` ruby
430class ApplicationPolicy
431  def initialize(user, record)
432    raise Pundit::NotAuthorizedError, "must be logged in" unless user
433    @user   = user
434    @record = record
435  end
436
437  class Scope
438    attr_reader :user, :scope
439
440    def initialize(user, scope)
441      raise Pundit::NotAuthorizedError, "must be logged in" unless user
442      @user = user
443      @scope = scope
444    end
445  end
446end
447```
448
449## NilClassPolicy
450
451To support a [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern)
452you may find that you want to implement a `NilClassPolicy`. This might be useful
453where you want to extend your ApplicationPolicy to allow some tolerance of, for
454example, associations which might be `nil`.
455
456```ruby
457class NilClassPolicy < ApplicationPolicy
458  class Scope < Scope
459    def resolve
460      raise Pundit::NotDefinedError, "Cannot scope NilClass"
461    end
462  end
463
464  def show?
465    false # Nobody can see nothing
466  end
467end
468```
469
470## Rescuing a denied Authorization in Rails
471
472Pundit raises a `Pundit::NotAuthorizedError` you can
473[rescue_from](http://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
474in your `ApplicationController`. You can customize the `user_not_authorized`
475method in every controller.
476
477```ruby
478class ApplicationController < ActionController::Base
479  protect_from_forgery
480  include Pundit
481
482  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
483
484  private
485
486  def user_not_authorized
487    flash[:alert] = "You are not authorized to perform this action."
488    redirect_to(request.referrer || root_path)
489  end
490end
491```
492
493Alternatively, 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:
494
495```config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden```
496
497## Creating custom error messages
498
499`NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
500record (e.g. an instance of `Post`), and what policy (e.g. an instance of
501`PostPolicy`) caused the error to be raised.
502
503One way to use these `query`, `record`, and `policy` properties is to connect
504them with `I18n` to generate error messages. Here's how you might go about doing
505that.
506
507```ruby
508class ApplicationController < ActionController::Base
509 rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
510
511 private
512
513 def user_not_authorized(exception)
514   policy_name = exception.policy.class.to_s.underscore
515
516   flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
517   redirect_to(request.referrer || root_path)
518 end
519end
520```
521
522```yaml
523en:
524 pundit:
525   default: 'You cannot perform this action.'
526   post_policy:
527     update?: 'You cannot edit this post!'
528     create?: 'You cannot create posts!'
529```
530
531Of course, this is just an example. Pundit is agnostic as to how you implement
532your error messaging.
533
534## Manually retrieving policies and scopes
535
536Sometimes you want to retrieve a policy for a record outside the controller or
537view. For example when you delegate permissions from one policy to another.
538
539You can easily retrieve policies and scopes like this:
540
541``` ruby
542Pundit.policy!(user, post)
543Pundit.policy(user, post)
544
545Pundit.policy_scope!(user, Post)
546Pundit.policy_scope(user, Post)
547```
548
549The bang methods will raise an exception if the policy does not exist, whereas
550those without the bang will return nil.
551
552## Customize Pundit user
553
554In some cases your controller might not have access to `current_user`, or your
555`current_user` is not the method that should be invoked by Pundit. Simply
556define a method in your controller called `pundit_user`.
557
558```ruby
559def pundit_user
560  User.find_by_other_means
561end
562```
563
564## Policy Namespacing
565In some cases it might be helpful to have multiple policies that serve different contexts for a
566resource. A prime example of this is the case where User policies differ from Admin policies. To
567authorize with a namespaced policy, pass the namespace into the `authorize` helper in an array:
568
569```ruby
570authorize(post)                   # => will look for a PostPolicy
571authorize([:admin, post])         # => will look for an Admin::PostPolicy
572authorize([:foo, :bar, post])     # => will look for a Foo::Bar::PostPolicy
573
574policy_scope(Post)                # => will look for a PostPolicy::Scope
575policy_scope([:admin, Post])      # => will look for an Admin::PostPolicy::Scope
576policy_scope([:foo, :bar, Post])  # => will look for a Foo::Bar::PostPolicy::Scope
577```
578
579If you are using namespaced policies for something like Admin views, it can be useful to
580override the `policy_scope` and `authorize` helpers in your `AdminController` to automatically
581apply the namespacing:
582
583```ruby
584class AdminController < ApplicationController
585  def policy_scope(scope)
586    super([:admin, scope])
587  end
588
589  def authorize(record, query = nil)
590    super([:admin, record], query)
591  end
592end
593
594class Admin::PostController < AdminController
595  def index
596    policy_scope(Post)
597  end
598
599  def show
600    post = Post.find(params[:id])
601    authorize(post)
602  end
603end
604```
605
606## Additional context
607
608Pundit strongly encourages you to model your application in such a way that the
609only context you need for authorization is a user object and a domain model that
610you want to check authorization for. If you find yourself needing more context than
611that, consider whether you are authorizing the right domain model, maybe another
612domain model (or a wrapper around multiple domain models) can provide the context
613you need.
614
615Pundit does not allow you to pass additional arguments to policies for precisely
616this reason.
617
618However, in very rare cases, you might need to authorize based on more context than just
619the currently authenticated user. Suppose for example that authorization is dependent
620on IP address in addition to the authenticated user. In that case, one option is to
621create a special class which wraps up both user and IP and passes it to the policy.
622
623``` ruby
624class UserContext
625  attr_reader :user, :ip
626
627  def initialize(user, ip)
628    @user = user
629    @ip   = ip
630  end
631end
632
633class ApplicationController
634  include Pundit
635
636  def pundit_user
637    UserContext.new(current_user, request.ip)
638  end
639end
640```
641
642## Strong parameters
643
644In Rails 4 (or Rails 3.2 with the
645[strong_parameters](https://github.com/rails/strong_parameters) gem),
646mass-assignment protection is handled in the controller.  With Pundit you can
647control which attributes a user has access to update via your policies. You can
648set up a `permitted_attributes` method in your policy like this:
649
650```ruby
651# app/policies/post_policy.rb
652class PostPolicy < ApplicationPolicy
653  def permitted_attributes
654    if user.admin? || user.owner_of?(post)
655      [:title, :body, :tag_list]
656    else
657      [:tag_list]
658    end
659  end
660end
661```
662
663You can now retrieve these attributes from the policy:
664
665```ruby
666# app/controllers/posts_controller.rb
667class PostsController < ApplicationController
668  def update
669    @post = Post.find(params[:id])
670    if @post.update_attributes(post_params)
671      redirect_to @post
672    else
673      render :edit
674    end
675  end
676
677  private
678
679  def post_params
680    params.require(:post).permit(policy(@post).permitted_attributes)
681  end
682end
683```
684
685However, this is a bit cumbersome, so Pundit provides a convenient helper method:
686
687```ruby
688# app/controllers/posts_controller.rb
689class PostsController < ApplicationController
690  def update
691    @post = Post.find(params[:id])
692    if @post.update_attributes(permitted_attributes(@post))
693      redirect_to @post
694    else
695      render :edit
696    end
697  end
698end
699```
700
701If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy:
702
703```ruby
704# app/policies/post_policy.rb
705class PostPolicy < ApplicationPolicy
706  def permitted_attributes_for_create
707    [:title, :body]
708  end
709
710  def permitted_attributes_for_edit
711    [:body]
712  end
713end
714```
715
716If 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.
717
718If 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`.
719
720```ruby
721def pundit_params_for(record)
722  params.require(PolicyFinder.new(record).param_key)
723end
724```
725
726For example:
727
728```ruby
729# If you don't want to use require
730def pundit_params_for(record)
731  params.fetch(PolicyFinder.new(record).param_key, {})
732end
733
734# If you are using something like the JSON API spec
735def pundit_params_for(_record)
736  params.fetch(:data, {}).fetch(:attributes, {})
737end
738```
739
740## RSpec
741
742### Policy Specs
743
744Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec.
745Require `pundit/rspec` in your `spec_helper.rb`:
746
747``` ruby
748require "pundit/rspec"
749```
750
751Then put your policy specs in `spec/policies`, and make them look somewhat like this:
752
753``` ruby
754describe PostPolicy do
755  subject { described_class }
756
757  permissions :update?, :edit? do
758    it "denies access if post is published" do
759      expect(subject).not_to permit(User.new(admin: false), Post.new(published: true))
760    end
761
762    it "grants access if post is published and user is an admin" do
763      expect(subject).to permit(User.new(admin: true), Post.new(published: true))
764    end
765
766    it "grants access if post is unpublished" do
767      expect(subject).to permit(User.new(admin: false), Post.new(published: false))
768    end
769  end
770end
771```
772
773An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
774[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.
775
776### Scope Specs
777
778Pundit does not provide a DSL for testing scopes. Just test it like a regular Ruby class!
779
780# External Resources
781
782- [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
783- [Migrating to Pundit from CanCan](http://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
784- [Testing Pundit Policies with RSpec](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
785- [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136)
786- [Straightforward Rails Authorization with Pundit](http://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
787
788# License
789
790Licensed under the MIT license, see the separate LICENSE.txt file.
791