Who are you?) and Authorization (are you supposed to be here?). Authentication verifies the user’s identity while authorization verifies whether that user has access rights on certain resources to perform actions on them. Two popular gems for authorization in the rails world are CanCanCan and Pundit, we at Red Panthers prefers pundit over CanCanCan we get to write Pure Ruby Objects and keep the logic for each part separate. The gem CanCanCan isolates (encourages) all authorization logic into a single class. It is a drawback when the complexity of application increases. Pundit gem provides object oriented design patterns to build our own authorization system that meets project’s requirements. It enables us to keep the models and controllers free from authorization code and allows to keep the resource logic separately. This flexibility and simplicity of Pundit gem help to use it with ease.
To start with PunditAdd Pundit gem to your gem file and run bundle install.
gem 'pundit'Integrate Pundit to Rails application by adding the following line to ApplicationController.
include PunditIf you run the Pundit’s generator as below, it will generate app/policies folder which contains application_policy.rb by default.
rails g pundit:install
#app/policies/application_policy.rb class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end #............ def destroy? false end def scope Pundit.policy_scope!(user, record.class) end class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve scope end end end
Create policiesPolicy classes are the core of Pundit. In the app/policies folder, we can write our own policies. Each policy is a Ruby class. Each policy class should be named after a model they belong to, followed by the word Policy. For example, use CollectionPolicy for Collection model. Pundit can also be used without an associated model. Pundit uses the current_user method to get the first argument in initialize method in ApplicationPolicy. But if current_user is not the method that should be invoked by Pundit, simply define a method in your controller.
def pundit_user User.find_by_other_means endLogically, Pundit can be used outside controllers, for example, in custom services or in views. Consider the following example. In User model,
class User < ApplicationRecord has_many :collections, dependent: :destroy endIn Collection model,
class Collection < ApplicationRecord belongs_to :user endA collection should be able to be deleted only by the user who created it. So, let’s start by creating a new file collection_policy.rb in app/policies that will store our policies that are specific to collections. In this file, we define a class that inherits from the ApplicationPolicy class and we will integrate delete method for managing permissions for the delete action .
#app/policies/collection_policy.rb class CollectionPolicy < ApplicationPolicy def destroy? record.user == user end endIn our CollectionPolicy class we are overriding the delete? method originally declared in the ApplicationPolicy. There it simply returns false. We can override the methods in ApplicationPolicy class with our unique requirements since it’s intended to give a structure only.
record.user == userThis states that, the only user that should be able to delete a collection is the user that created it. We can refactor this into their own method since it’s a better approach if other authorized users are needed to be added in future so that changes can be made easily in a single method instead of having to make the same changes in multiple places. This is very explicit, clearly describing the intent of the program flow. Definitely, we won’t be wondering, from where these record and user attributes are coming from. In ApplicationPolicy class we can see that they are set as read only attributes representing the object that we are adding authorization to, such as collection in our app and then the user. This is an instance of Pundit providing easy access to the items that we want to add authorization. If we add another policy class like CollectionPolicy then also we will be able to use very similar code like we are working with collections. Now let’s move to CollectionsController . By using Pundit policies, a proper permission structure can be integrated to the delete action, which earlier had no protection from unauthorized HTTP requests.
class CollectionsController < ApplicationController protect_from_forgery def create Collections::Create.call(collection_params, current_user) redirect_to root_url end def destroy collection = Collection.find(params[:id]) authorize(collection, :destroy?) Collections::Delete.call(collection, current_user) redirect_to root_url end private def collection_params params.require(:collection).permit(:name) end endThe authorize method automatically assumes that Collection will have a corresponding CollectionPolicy class, and instantiates this class. It should call destroy? method on this instance of the policy. Passing a second argument to authorize method is optional here. It infers from the action name that it should call destroy? method on this instance of the policy. But second argument should be passed if it doesn’t match the action name.
Policy without a corresponding modelWe can create ‘headless’ policies that are not tied to any specific model. Such policies can be retrieved by passing a symbol.
# app/policies/website_policy.rb class WebsitePolicy < Struct.new(:user, :website) # ... end
# In controllers authorize :website, :show?
# In views <% if policy(:website).show? %> <%= link_to 'Website', website_path %> <% end %>Here a model or class named Website doesn’t exist. So WebsitePolicy is retrieved by passing a symbol. This is a headless policy.
Pundit scopesIn application_policy.rb, there is a scope class defined. It implements a method called resolve for filtering. We can inherit it from a base class and implement our own resolve method. In this example a scope is setup to allow users to view websites only if they have a link through collections. Let’s consider three models: User, Website, and Collection Collection belongs to User. In WebsitePolicy,
class Scope < Scope def resolve if user.admin? scope.all else scope.where(:company_id => user.collections.select(:website_id)) end end endIn the websites_controller,
def index @websites = policy_scope(Website.includes(:company).all) authorize @websites endNow we see only the websites where we have a collection.
Exception handlingBy default, Pundit raises an exception when users attempt to access that which they are not authorized to. This situation can be handled in ApplicationController.
class ApplicationController < ActionController::Base include Pundit protect_from_forgery with: :exception rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized def user_not_authorized flash[:alert] = 'You are not authorized to perform this action.' redirect_back(fallback_location: root_path) end endWe need to rescue the Pundit::NotAuthorizedError exception with a suitable method that tells how to handle it.
- Well suited to the service oriented architecture that’s popular for large Rails applications
- Keep controllers skinny
- Pundit policy objects are lightweight, adding authorization logic without as much overhead as CanCanCan
- Emphasizes object-oriented design with discrete Ruby objects providing specialized services