Getting Started with Pundit
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:
- Add it into Gemfile i.e. gem "pundit"
- Add it into application_controller.rb i.e. include Pundit
- Run the pundit generator: rails g pundit:install
- 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