Authorization with Pundit

What isĀ Pundit?

Authorization strategies

User-based

class Ability
  include CanCan::Ability
 
  def initialize(user)
    # Anonymous users don't have access to anything
    return if user.nil?
 
    case user.role
    when :admin
      can :manage, :all
    when :supervisor
      # Between 1-50 can/cannot statements
    when :doctor
      # Between 1-50 can/cannot statements
    when :patient
      # Between 1-50 can/cannot statements
    else
      raise Ability::UnknownRoleError
    end
  end
 
end

Role-based

class PostPolicy < ApplicationPolicy

  def show?
    true
  end

  def create?
    user.admin?
  end

  def update?
    user.admin? or not post.published?
  end

  def destroy?
    update?
  end

end

First policy

# app/policies/post_policy.rb
class PostPolicy
  attr_reader :user, :post

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

  def create?
    true
  end

  def update?
    user.admin? || user.owner_of?(post)
  end
end

Using in controller

# app/controllers/posts_controller.rb
class PostsController < ApplicationController


  def create
    raise unless PostPolicy.new(current_user, nil).create?
    # [...]
  end

  def update
    @post = Post.find(params[:id])
    raise unless PostPolicy.new(current_user, @post).update?
    # [...]
  end

end

Using in controller

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  include Pundit

  def create
    authorize Post, :create?
    # [...]
  end

  def update
    @post = Post.find(params[:id])
    authorize @post, :update?
    # [...]
  end

end

Using in controller

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  include Pundit

  def create
    authorize Post
    # [...]
  end

  def update
    @post = Post.find(params[:id])
    authorize @post
    # [...]
  end

end

Using in view

# app/views/posts/show.html.erb
<% if policy(@post).update? %>
  <%= link_to 'Edit post', edit_post_path(@post) %>
<% end %>

Testing policy

# spec/policies/post_policy_spec.rb
describe PostPolicy do
  subject { described_class }
  let(:post) { Post.new }

  context "#update?" do
    it "should disallow for other user" do
      user = User.create
      policy = subject.new(user, post)
      expect(policy.update?).to be_false
    end

    it "should allow for admin" do
      user = User.create(admin: true)
      policy = subject.new(user, post)
      expect(policy.update?).to be_true
    end
  end
end

Testing policy

# spec/policies/post_policy_spec.rb
describe PostPolicy do
  subject { described_class }
  let(:post) { Post.new }

  permissions :update? do
    it "should disallow for other user" do
      user = User.new

      expect(subject).not_to permit(user, post)
    end

    it "should allow for admin" do
      user = User.new(admin: true)

      expect(subject).to permit(user, post)
    end
  end
end

Scopes

# app/policies/post_policy.rb
class PostPolicy
  # [...]
  class Scope < Struct.new(:user, :scope)

    def resolve
      if user.admin?
        scope.all
      else
        scope.where(user_id: user.id)
      end
    end

  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = PostPolicy::Scope.new(current_user, Post).resolve
  end
end

Scopes

# app/policies/post_policy.rb
class PostPolicy
  # [...]
  class Scope < Struct.new(:user, :scope)

    def resolve
      if user.admin?
        scope.all
      else
        scope.where(user_id: user.id)
      end
    end

  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = policy_scope(Post)
  end
end

Scopes

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = policy_scope(Post)
  end

  def show
    @post = policy_scope(Post).find(params[:id])
  end
end

# app/views/posts/index.html.erb
<% policy_scope(@user.posts).each do |post| %>
  <p><%= link_to post.title, post_path(post) %></p>
<% end %>

Strong params

# app/policies/post_policy.rb
class PostPolicy

  def permitted_attributes
    if user.admin? || user.owner_of?(post)
      [:title, :body, :tag_list]
    else
      [:tag_list]
    end
  end

end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def post_params
    params.require(:post).permit(policy(@post).permitted_attributes)
  end

end

Strong params

# app/policies/post_policy.rb
class PostPolicy

  def permitted_attributes
    if user.admin? || user.owner_of?(post)
      [:title, :body, :tag_list]
    else
      [:tag_list]
    end
  end

end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def post_params
    permitted_attributes(@post)
  end

end

Ensuring policies are usedĀ 

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  after_action :verify_authorized
  after_action :verify_policy_scoped, :only => :index
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def show
    post = Post.find_by(attribute: "value")
    if post.present?
      authorize post
    else
      skip_authorization
    end
  end

end

Remember:

It's all PORO!

Thanks!

Authorization with Pundit

By Bernard Potocki

Authorization with Pundit

  • 1,066