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