Pundit

Authorization done right

What is authorization?

Authorization is determining if given user has access to given resource.

Where does authorization fit in (Rails-flavoured) MVC?

  • Model - should not need to know anything about the user.
  • View - knows about user and the resource but should not decide about anything.
  • Controller - the right place for authorization

Pundit

  • Minimal authorization through OO design and pure Ruby classes
  • https://github.com/elabs/pundit
  • Second (after CanCan) in authorization on Ruby Toolbox
  • Better than CanCan, which stores all authorization rules in one huge file

The term originates from the Sanskrit term pandit (paṇḍitá), meaning "learned". It refers to someone who is erudite in various subjects and who conducts religious ceremonies and offers counsel to the king.

 

Nowadays, a pundit (sometimes called a talking head) is a person who offers to mass media their opinion or commentary on a particular subject area on which they are knowledgeable (or can at least appear to be knowledgeable), or considered a scholar in said area.

CanCan way

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new


    unless user.new_record?
      can :comment, Part
      can :rate, Part
      cannot :rate, Part, :story => { :user => user }
      can :comment, Article
      can :comment, NewsPost
      can :create, Story
      can :update, User, :id => user.id
      
      can :update, Story, :user => user
      can :update, Part, :story => { :user => user, :status => Story::OPEN }
      can :update, Part, :story => { :user => user, :story_type => Story::SINGLE }

      cannot :add_fragment_to, Story, :story_type => Story::SINGLE
      can :add_fragment_to, Story, :user => user, :status => Story::OPEN
    end

    if user.has_role?(:news_editor)
      can :manage, NewsPost
    end

    if user.has_role?(:stories_editor)
      can :manage, Story
      can :manage, Part
    end

    if user.has_role?(:users_manager)
      can :manage, User
    end

    if user.has_role?(:comments_editor)
      can :manage, Comment
    end

    if user.has_role?(:articles_editor)
      can :manage, Article
    end
    
  end
end

Pundit way

app/policies/document_policy.rb


class DocumentPolicy
	attr_reader :user, :document

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

	def view?
		@document.user == user
	end
end

user = current_user

document (resource) = any kind of ruby object

Pundit way - controller

def show
	@document = Document.find(params[:id])
	@document_file = @document.last_file
	authorize @document, :view?
	if File.exists?(@document_file.file.path)
		send_file @document_file.file.path, type: 'application/pdf', 
                    disposition: 'inline'
	else
		render :file_not_found
	end
end

Infer method name from controller action

# policy
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end

# controller
def update
  @post = Post.find(params[:id])
  authorize @post
  if @post.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end

View

Spoiler: this is where CanCan wins

# Pundit
<% if policy(@post).update? %>
  <%= link_to "Edit post", edit_post_path(@post) %>
<% end %>


# CanCan
<% if can? :update, @article %>
  <%= link_to "Edit", edit_article_path(@article) %>
<% end %>

Handle unauthorized acccess

class ApplicationController < ActionController::Base
	include Pundit

	rescue_from Pundit::NotAuthorizedError, with: :permission_denied

	def permission_denied
		render 'shared/permission_denied'
	end
end

Thank you

This was (hopefully) a part of Gems 101 series, where we discuss how it's done without Framework and why it's better/worse way.

pundit

By katafrakt

pundit

  • 1,016