6.9 KiB
Pundit
Pundit isn't really a library, as much as a set of helpers which simplify writing authorization systems for Ruby on Rails according to a pattern of using pure Ruby classes and object oriented design patterns.
Installation
gem "pundit"
Optionally, you can run the generator, which will set up an application policy with some useful default for you:
rails g pundit:install
Policies
Pundit is focused around the notion of policy classes. We suggest that you put
these classes in app/policies
. This is a simple example:
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def create?
user.admin? or not post.published?
end
end
As you can see, this is just a plain Ruby class. As a convenience, we can inherit from Struct:
class PostPolicy < Struct.new(:user, :post)
def create?
user.admin? or not post.published?
end
end
Pundit makes the following assumptions about this class:
- The class has the same name as some kind of model class, only suffixed with the word "Policy".
- The first argument is a user. In your controller, Pundit will call the
current_user
method to retrieve what to send into this argument - The second argument is some kind of model object, whose authorization you want to check. This does not need to be an ActiveRecord or even an ActiveModel object, it can be anything really.
- The class implements some kind of query method, in this case
create?
. Usually, this will map to the name of a particular controller action.
That's it really.
Supposing that you have an instance of class Post
, Pundit now lets you do
this in your controller:
def create
@post = Post.new(params[:post])
authorize @post
if @post.save
redirect_to @post
else
render :new
end
end
The authorize method automatically infers that Post
will have a matching
PostPolicy
class, and instantiates this class, handing in the current user
and the given record. It then infers from the action name, that it should call
create?
on this instance of the policy. In this case, you can imagine that
authorize
would have done something like this:
raise "not authorized" unless PostPolicy.new(current_user, @post).create?
You can easily get a hold of an instance of the policy through the policy
method in both the view and controller. This is especially useful for
conditionally showing links or buttons in the view:
<% if policy(@post).create? %>
<%= link_to "New post", new_post_path %>
<% end %>
Ensuring policies are used
Pundit adds a method called verify_authorized
to your controllers. This
method will raise an exception if authorize
has not yet been called. You
should run this method in an after_filter
to ensure that you haven't
forgotten to authorize the action. For example:
class ApplicationController < ActionController::Base
after_filter :verify_authorized, :except => :index
end
Scopes
Often, you will want to have some kind of view listing records which a particular user has access to. When using Pundit, you are expected to define a class called a policy scope. It can look something like this:
class PostPolicy < Struct.new(:user, :post)
class Scope < Struct.new(:user, :scope)
def resolve
if user.admin?
scope
else
scope.where(:published => true)
end
end
end
def create?
user.admin? or not post.published?
end
end
Pundit makes the following assumptions about this class:
- The class has the name
Scope
and is nested under the policy class. - The first argument is a user. In your controller, Pundit will call the
current_user
method to retrieve what to send into this argument. - The second argument is a scope of some kind on which to perform some kind of
query. It will usually be an ActiveRecord class or a
ActiveRecord::Relation
, but it could be something else entirely. - Instances of this class respond to the method
resolve
, which should return some kind of result which can be iterated over. For ActiveRecord classes, this would usually be anActiveRecord::Relation
.
You can now use this class from your controller via the policy_scope
method:
def index
@posts = policy_scope(Post)
end
Just as with your policy, this will automatically infer that you want to use
the PostPolicy::Scope
class, it will instantiate this class and call
resolve
on the instance. In this case it is a shortcut for doing:
def index
@posts = PostPolicy::Scope.new(current_user, Post).resolve
end
You can, and are encouraged to, use this method in views:
<% policy_scope(@user.posts).each do |post| %>
<p><% link_to @post.title, post_path(post) %></p>
<% end %>
Conclusion
As you can see, Pundit doesn't do anything you couldn't have easily done yourself. It's a very small library, it just provides a few neat helpers. Together these give you the power of building a well structured, fully working authorization system without using any special DSLs or funky syntax or anything.
Just plain old Ruby
Remember that all of the policy and scope classes are just plain Ruby classes,
which means you can use the same mechanisms you always use to DRY things up.
Encapsulate a set of permissions into a module and include them in multiple
policies. Use alias_method
to make some permissions behave the same as
others. Inherit from a base set of permissions. Use metaprogramming if you
really have to. The options are endless.
Generator
Use the supplied generator to generate policies:
rails g pundit:policy post
Closed systems
In many applications, only logged in users are really able to do anything. If
you're building such a system, it can be kind of cumbersome to check that the
user in a policy isn't nil
for every single permission.
We suggest that you define a filter that redirects unauthenticated users to the login page. As a secondary defence, if you've defined an ApplicationPolicy, it might be a good idea to raise an exception if somehow an unauthenticated user got through. This way you can fail more gracefully.
class ApplicationPolicy < Pundit::Policy
def initialize(user, record)
raise Pundit::NotAuthorized, "must be logged in" unless user
super
end
end
Manually retrieving policies and scopes
Sometimes you want to retrieve a policy for a record outside the controller or view. For example when you delegate permissions from one policy to another.
You can easily retrieve policies and scopes like this:
Pundit.policy!(user, post)
Pundit.policy(user, post)
Pundit.policy_scope!(user, Post)
Pundit.policy_scope(user, Post)
The bang methods will raise an exception if the policy does not exist, whereas those without the bang will return nil.
License
Licensed under the MIT license, see the separate LICENSE.txt file.