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:
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.
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
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