Hegwin.Me

In silence I feel full; With speech I sense emptiness.

Rails Authorization with Pundit

Rails权限验证工具Pundit

After people started using Rails 4, cancan was criticized for its complexity and lack of compatibility fixes, and people turned to a new tool Pundit.

Pundit is a pure ruby gem for permission verification.

Basic idea

Pundit, for objects that need to be authorized, according to the user's actions, will go to the corresponding policy of this object to find and execute the authorization method, and then implement the authorization.

In other words, for any Ruby Class, if you need to verify that the user has permission to operate one of its instances, just write the authorization rules in the corresponding policy.

Installation and Initialization

# Gemfile
gem 'pundit'

$ bundle install

$ rails g pundit:install Generate the default policy file with path app/policies/application_policy.rb.

In the ApplicationController add:

class ApplicationController < ActionController::Base
  # ...
  include Pundit
  # ...
end

Add authorization

If you want to authorize an instance of the Article model, go ahead and execute:

$ rails generate pundit:policy article => Generate app/policies/article_policy.rb.

``ruby

app/policies/article_policy.rb

class ArticlePolicy < ApplicationPolicy class Scope < Struct.new(:user, :scope) def resolve scope end end end ```

Suppose we have the following in articles_controller:

def update
  @article = Article.find(params[:id])
  # We need to verify here that the current_user has access to this @atilce
  @article.update_atttributes(article_arrtibutes)
end

We add a method to ArticlePolicy to verify that the user has this permission. The method naming convention is to end with a question mark.

# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
  class Scope < Struct.new(:user, :scope)
    def resolve
      scope
    end
  end

  def update?
     record.creator_id == user.id
  end
end

where uesr and record are from ApplicationPolicy.

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end
end

Then add authorization to ArticlesContoller:

def update
  @article = Article.find(params[:id])
  authorize @article, :update?
  @article.update_atttributes(article_arrtibutes)
end

Since action_name is the same as the name of the authorization method, it can simply be written as

def update
  @article = Article.find(params[:id])
  authorize @article
  @article.update_atttributes(article_arrtibutes)
end

At this point, we have permission check and rails will throw an error when the user doesn't have permission; but we're not done yet, we need to catch this error and handle it accordingly.

Our application_controller should look like this:

class ApplicationController < ActionController::Base
  # ...
  include Pundit
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    redirect_to root_url, :alert => "You don't have permission to those resources."
  end

end

This will redirect the user to root_url and give the appropriate alert if the user does not have permission.

Testing

Refer here: Testing Pundit Policies with RSpec

Next, we will test the code we just added, using rspec as an example:

First, add to spec_helper:

require 'pundit/rspec'

Then, generate the test file and place it in spec/policies/article_policy_spec.rb:

require 'spec_helper'

describe ArticlePolicy do
  subject { ArticlePolicy.new(user, article) }

  let(:article) { create :article }

  context 'for a guest' do
    let(:user) { nil }

    it { should_not allow(:update) }
  end

  context 'for the write ' do
    let(:user) { article.creator }

    it { should allow(:index) }
  end

  context 'for an other writer' do
    let(:user) { create :user }

    it { should_not_allow(:update) }
  end
end

Note: The allow method here is not provided by Pundit, but is our custom matcher.

# spec/support/pundit_matcher.rb

RSpec::Matchers.define :allow do |action|
  match do |policy|
    policy.public_send("#{action}?")
  end

  failure_message_for_should do |policy|
    "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}."
  end

  failure_message_for_should_not_do |policy|
    "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}."
  end
end
< Back