Pundit for authorization with Rspec Rails
Authorization is one of the important feature of any web app.
With rails you can leverage the power of all those wonderful open source gems that are available to you or you can code your own authorization module.
The most commonly used gems for authorization are Pundit
and Cancan
(now cancancan
, since the community took over the development of the gem. One reason for which I love the Ruby community the most :))
Now since we have those 2 popular gem, the next big question is which to chose from
Pundit or Cancan ?
Cancan
- Popular among the two.
- More star gazers in github.
- Now being maintained by the community.
- Custom DSL
Pundit
- Plain ruby classes and Object Oriented code design patterns.
- No DSL to master
- PORO.
Basic usage
The setup and install instructions are documented in detailed in the gem's wiki itself.
The main principle is to extract the authorization rules into policy files, which are POROs:
For eg:, an Article plolicy might look like:
class ArticlePolicy
attr_reader :user, :article
def initialize(user, article)
@user = user
@article = article
end
def new?
user.has_roles?('author')
end
alias_method :create?, :new?
def edit?
user.has_roles?('author') && article.is_draft?
end
alias_method :update?, :edit?
end
And your corresponding controller will be something like :
class ArticleController < ApplicationController
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
...
def edit
@article = Article.find(params[:id])
authorize @article
...
end
...
private
def user_not_authorized
flash[:error] = 'You are not authorized to perform this action.'
redirect_to(request.referrer || root_path)
end
end
Testing with Rspec.
Pundit works well with rspec. Thunderboltlabs has a nice article written on testing pundit policies with rspec.
Going ahead and adding the matchers for pundit:
RSpec::Matchers.define :permit 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
to spec/pundit_matcher.rb
and including that in our spec_helper
using Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
.
Once we have the custom matchers and the support files loaded into spec helper we can go ahead and write our test cases our article policy.
So our rspec test will be having a following pattern:
require 'spec_helper'
describe ArticlePolicy do
subject { ArticlePolicy.new(user, article) }
let(:article) { FactoryGirl.create(:article) }
context 'for a visitor' do
let(:user) { nil }
it { should_not permit(:create) }
it { should_not permit(:new) }
it { should_not permit(:update) }
it { should_not permit(:edit) }
end
context "for an author" do
let(:user) { FactoryGirl.create(:user, role: 'author') }
it { should permit(:create) }
it { should permit(:new) }
it { should permit(:update) }
it { should permit(:edit) }
end
end
Tweaks with shoulda matchers.
In case your application is using shoulda matchers there is a chance that the above wont work, as a result of conflicting namespace. You can read more about the same here.
So the work around would be to tweak your support file. Instead of using the conflicting permit
we just rename it to permitted
a little bit to the following:
RSpec::Matchers.define :permitted_to 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
Updating our test case for the above workaround,
require 'spec_helper'
describe ArticlePolicy do
subject { ArticlePolicy.new(user, article) }
let(:article) { FactoryGirl.create(:article) }
context 'for a visitor' do
let(:user) { nil }
it { should_not permitted_to(:create) }
it { should_not permitted_to(:new) }
it { should_not permitted_to(:update) }
it { should_not permitted_to(:edit) }
end
context "for an author" do
let(:user) { FactoryGirl.create(:user, role: 'author') }
it { should permitted_to(:create) }
it { should permitted_to(:new) }
it { should permitted_to(:update) }
it { should permitted_to(:edit) }
end
end
Another possible work around is not to load the shoulda matchers in your policies specs. You can restrict this by loading them only to your models, controllers etc.
Hope you find this helpful.