module Authorizable
extend ActiveSupport::Concern
class NotAuthorized < StandardError; end
included do
rescue_from NotAuthorized, with: :render_forbidden
end
private
def authorize!(record, action: action_name, user: current_user)
policy = policy_for(record, user)
permission = "#{action}?"
unless policy.respond_to?(permission)
raise NotAuthorized, "#{policy.class} has no rule for #{permission}"
end
raise NotAuthorized unless policy.public_send(permission)
record
end
def policy_for(record, user)
klass = "#{record.class.name}Policy".constantize
klass.new(user, record)
end
def render_forbidden
respond_to do |format|
format.html { render file: Rails.root.join("public/403.html"), status: :forbidden, layout: false }
format.json { render json: { error: "forbidden" }, status: :forbidden }
end
end
end
class ArticlePolicy
attr_reader :user, :article
def initialize(user, article)
@user = user
@article = article
end
def show?
article.published? || owner? || admin?
end
def create?
user.present?
end
def update?
owner? || admin?
end
def destroy?
owner? || admin?
end
private
def owner?
user.present? && article.author_id == user.id
end
def admin?
user.present? && user.admin?
end
end
class ArticlesController < ApplicationController
include Authorizable
before_action :require_login, only: %i[create update destroy]
before_action :set_article, only: %i[show update destroy]
def show
authorize!(@article)
render json: @article
end
def create
authorize!(Article.new(author: current_user))
@article = current_user.articles.create!(article_params)
render json: @article, status: :created
end
def update
authorize!(@article)
@article.update!(article_params)
render json: @article
end
def destroy
authorize!(@article)
@article.destroy!
head :no_content
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 Rails application enforces per-action authorization using a small policy object wired into controllers through a before_action concern that renders a proper 403 Forbidden when access is denied. The pattern keeps controllers thin: each action declares what it needs authorized, and the mechanics of resolving a policy, calling it, and handling failure live in one reusable place.
In Authorizable concern, the authorize! helper is the heart of the design. It looks up a policy class by convention (ArticlePolicy for an Article), instantiates it with current_user and the record, and calls a predicate named after the current action (show?, update?, and so on). When the predicate returns false it raises NotAuthorized, a dedicated error class, rather than rendering inline. A rescue_from NotAuthorized handler centralizes the response so every denial produces the same 403 regardless of which action failed. Resolving the action name from action_name means callers usually just write authorize!(@article) without repeating the permission name.
Raising and rescuing is deliberate: it lets a single before_action short-circuit the request before the action body runs, and it keeps the failure path out of every individual method. The render_forbidden handler responds to both HTML and JSON, so API and browser clients each get an appropriate 403. Returning a machine-readable error code in the JSON branch is friendlier to frontend clients than a bare status.
ArticlePolicy holds the actual rules as plain predicate methods. Because it is an ordinary Ruby object with no framework coupling, the logic — owners can edit and destroy, anyone can read published articles — is trivial to unit test in isolation and to reason about. admin? short-circuits several checks, a common escape hatch.
In ArticlesController, before_action :require_login and a per-action authorize! call combine to guard mutating endpoints. The trade-off of convention-based lookup is that it depends on consistent naming; when a policy or predicate is missing it fails loudly, which is preferable to silently allowing access. This approach suits apps that have outgrown scattered if current_user checks but do not need a full gem.
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.