class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def update?
false
end
def destroy?
false
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
raise NotImplementedError, "#{self.class} must implement #resolve"
end
end
end
class ArticlePolicy < ApplicationPolicy
def index?
true
end
def show?
record.published? || owner? || user&.admin?
end
def create?
user.present?
end
def update?
owner? || user&.admin?
end
def destroy?
owner? || user&.admin?
end
class Scope < ApplicationPolicy::Scope
def resolve
if user&.admin?
scope.all
else
scope.where(author_id: user&.id)
end
end
end
private
def owner?
user.present? && record.author_id == user.id
end
end
module Authorizable
extend ActiveSupport::Concern
class NotAuthorizedError < StandardError; end
included do
rescue_from NotAuthorizedError, with: :deny_access
end
def authorize(record, query = nil)
query ||= "#{action_name}?"
policy = policy_for(record)
unless policy.public_send(query)
raise NotAuthorizedError, "not allowed to #{query} this #{record.class}"
end
@_authorized = true
record
end
def policy_scope(scope)
@_authorized = true
"#{scope.name}Policy".constantize::Scope.new(current_user, scope).resolve
end
def verify_authorized
return if @_authorized
raise NotAuthorizedError, "#{self.class}##{action_name} did not authorize"
end
private
def policy_for(record)
"#{record.class.name}Policy".constantize.new(current_user, record)
end
def deny_access
flash[:alert] = "You are not permitted to perform this action."
redirect_back(fallback_location: root_path, status: :forbidden)
end
end
class ArticlesController < ApplicationController
include Authorizable
before_action :set_article, only: %i[show update destroy]
after_action :verify_authorized, except: :index
def index
@articles = policy_scope(Article).order(created_at: :desc)
end
def show
authorize @article
end
def update
authorize @article
if @article.update(article_params)
redirect_to @article, notice: "Article updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
authorize @article
@article.destroy
redirect_to articles_path, notice: "Article deleted."
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :published)
end
end
This snippet shows how a Pundit-style authorization layer is wired into a Rails controller so that every sensitive action is guarded by an explicit policy object rather than ad-hoc if checks scattered through the code. The pattern separates who can do what (the policy) from how the request flows (the controller), which keeps authorization logic testable in isolation and consistent across actions.
In ApplicationPolicy, a small base class captures the two things every policy needs: the user performing the action and the record being acted on. Each permission is a predicate method (update?, destroy?) that returns a boolean, and the default answers are false so that a missing rule fails closed — a critical security property. The nested Scope class handles collection-level filtering, letting index actions narrow what a user is even allowed to see.
ArticlePolicy subclasses it and encodes the real rules: admins can do anything, authors can edit or delete their own articles, and resolve limits non-admins to their own records. Because these are plain Ruby objects with no Rails coupling, they can be unit-tested by instantiating them directly with a user and a record.
Authorizable is a controller concern that provides the glue. authorize infers the policy class from the record's class name, instantiates it, and raises NotAuthorizedError unless the named query method returns true. policy_scope does the analogous thing for collections. The rescue_from handler translates that exception into a 403 response and a flash message, so an authorization failure never leaks data or 500s. verify_authorized is an after_action guard that raises if a request completed without ever calling authorize — this catches the dangerous case where a developer forgets to protect a new action.
Finally, ArticlesController shows the payoff: authorize @article is a single, readable line before each mutating operation, and policy_scope(Article) replaces a manual where in index. The controller no longer knows the rules; it just asks. This trade of a little indirection for centralized, fail-closed, unit-testable authorization is why the policy-object pattern scales far better than inline checks as an application grows.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.