# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
.or(scope.where(user: user))
end
end
end
def index?
true # Everyone can view post listings
end
def show?
record.published? || record.user == user || user.admin?
end
def create?
user.present?
end
def update?
user.present? && (record.user == user || user.admin?)
end
def destroy?
user.present? && (record.user == user || user.admin?)
end
def publish?
user.admin? || (record.user == user && user.can_publish?)
end
# Custom permission
def feature?
user.admin?
end
# Attribute-level authorization
def permitted_attributes
if user.admin?
[:title, :content, :published, :featured, :category_id]
else
[:title, :content]
end
end
end
# app/policies/application_policy.rb
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 new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
raise NotImplementedError
end
end
end
# Controller usage
class PostsController < ApplicationController
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@posts = policy_scope(Post).page(params[:page])
end
def show
@post = Post.find(params[:id])
authorize @post
end
def create
@post = Post.new(permitted_attributes(Post))
authorize @post
if @post.save
redirect_to @post
else
render :new
end
end
def update
@post = Post.find(params[:id])
authorize @post
if @post.update(permitted_attributes(@post))
redirect_to @post
else
render :edit
end
end
def destroy
@post = Post.find(params[:id])
authorize @post
@post.destroy
redirect_to posts_path
end
def publish
@post = Post.find(params[:id])
authorize @post, :publish?
@post.update(published: true)
redirect_to @post
end
end
# Application controller setup
class ApplicationController < ActionController::Base
include Pundit::Authorization
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
end
# View helpers
<% if policy(@post).update? %>
<%= link_to 'Edit', edit_post_path(@post) %>
<% end %>
<% if policy(@post).destroy? %>
<%= link_to 'Delete', @post, method: :delete %>
<% end %>
<% if policy(@post).publish? %>
<%= link_to 'Publish', publish_post_path(@post), method: :post %>
<% end %>
# Policy testing (RSpec)
RSpec.describe PostPolicy do
subject { described_class }
let(:admin) { create(:user, :admin) }
let(:user) { create(:user) }
let(:post) { create(:post, user: user) }
permissions :show? do
it "allows admin to view any post" do
expect(subject).to permit(admin, post)
end
it "allows owner to view their post" do
expect(subject).to permit(user, post)
end
it "denies other users from viewing draft" do
draft = create(:post, published: false)
expect(subject).not_to permit(user, draft)
end
end
permissions :update? do
it "allows owner to update" do
expect(subject).to permit(user, post)
end
it "denies non-owner" do
other_user = create(:user)
expect(subject).not_to permit(other_user, post)
end
end
end
Pundit provides simple, object-oriented authorization. Policies encapsulate authorization rules in plain Ruby classes. Each model gets a policy class defining who can perform actions. I use Pundit for fine-grained permissions—different users see different data. Policies are easy to test—pure Ruby objects without Rails dependencies. Scopes filter collections based on permissions—users only see authorized records. Pundit integrates seamlessly with controllers via authorize helper. Policy errors raise Pundit::NotAuthorizedError, easily rescuable for proper responses. Policies keep authorization logic out of models and controllers. Understanding policy context—user and record—is key. Pundit scales better than CanCanCan for complex permissions, being more explicit and testable.