IMG_1424.jpeg

Pundit, a Rails gem that my former boss, Taylor Williams, taught me is a tool for "simple, robust and scalable authorization system" and the more I play with authorization, the more true I find that. This blog post lays out how to get started with Pundit with more details coming down the road.

I will freely admit that when Taylor taught me Pundit, I grokked about maybe 1/10th of 1/10th of what he taught me (the project I worked on under Taylor had a staggeringly complex security scheme). What I found, once I started from scratch with Pundit, is that it is strikingly simple to understand and a huge improvement over former Rails security gems like CanCan.

Security Basics: Authentication versus Authorization

There are two basic security concepts that often get tangled up together so let's start with the basics:

  • Authentication - Should the user be allowed access into the system?
  • Authorization - Should the user be granted permission to do something with a item within the system?

It should be noted that:

  • system
  • something
  • item

are all not defined. That's actually intentional at this point because we need to work in terms of abstractions and all of those are abstract.

Installing Pundit

Here's what you have to do:

  1. Add it into Gemfile i.e. gem "pundit"
  2. Add it into application_controller.rb i.e. include Pundit
  3. Run the pundit generator: rails g pundit:install
  4. Restart your server

Using Pundit

The way that Pundit works is that authorization is handled at the Rails controller level via an authorize statement like this:

# GET /projects
def index
  @pagy, @projects = pagy(current_user.projects.order("id DESC"))
  authorize @projects
end

So you make an instance variable of what you want to display in your HTML view template and then you authorize it via Pundit. The authorization is then defined by a Policy file stored in /app/policies/classname_policy.rb. Here's an example for the project.rb class:

class ProjectPolicy < ApplicationPolicy
  attr_reader :user, :project

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

  # Inheriting from the application policy scope generated by the generator
   class Scope < Scope
     def resolve
       if user.admin?
         scope.all
       else
         scope.where(active: true)
       end
     end
   end

  def index?
    valid_user?
  end
  
  # this ilustrates that the user MUST be logged in and 
  # that the owner of the data has to be the logged in user
  def show?
    valid_user? && project.user == user
  end

  def edit?
    valid_user?
  end

  def new?
    valid_user?
  end

  def create?
    valid_user?
  end

  def destroy?
    valid_user?
  end

  def update?
    valid_user?
  end

end