Rails Authorization with CanCanCan
CanCanCan is a popular authorization library for Rails that restricts what resources a given user is allowed to access. It centralizes all permission logic in a single Ability class, keeping authorization rules DRY and maintainable.
Quick Setup
# Add to Gemfile
bundle add cancancan
# Generate Ability class
rails generate cancan:ability
This creates app/models/ability.rb where all authorization rules are defined.
Core Concepts
Defining Abilities
The Ability class centralizes all permission logic:
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
# Guest users (not signed in)
can :read, Post, published: true
can :read, Comment
# Signed-in users
return unless user.present?
can :read, Post
can :create, Post
can :update, Post, user_id: user.id
can :destroy, Post, user_id: user.id
can :create, Comment
can :update, Comment, user_id: user.id
can :destroy, Comment, user_id: user.id
# Admin users
return unless user.admin?
can :manage, :all # Can do anything
end
end
Best Practice: Structure rules hierarchically (guest → user → admin) for clarity.
Actions and Resources
Standard CRUD Actions
:read # :index and :show
:create # :new and :create
:update # :edit and :update
:destroy # :destroy
:manage # All actions (use carefully!)
Custom Actions
can :publish, Post
can :archive, Post
can :approve, Comment
Multiple Resources
can :read, [Post, Comment, Category]
can :manage, [User, Post], user_id: user.id
Ability Conditions
Hash Conditions
# Simple equality
can :update, Post, user_id: user.id
# Multiple conditions (AND logic)
can :read, Post, published: true, category_id: user.accessible_category_ids
# SQL fragment (use sparingly)
can :read, Post, ["published_at <= ?", Time.zone.now]
Block Conditions
# Complex logic
can :update, Post do |post|
post.user_id == user.id || user.admin?
end
# With associations
can :read, Post do |post|
post.published? || post.user_id == user.id
end
# Accessing current user
can :destroy, Comment do |comment|
comment.user_id == user.id && comment.created_at > 15.minutes.ago
end
Important: Block conditions cannot be used with accessible_by for database queries. Use hash conditions when you need to filter collections.
Combining Conditions
# Multiple can statements are OR'd together
can :read, Post, published: true # Public posts
can :read, Post, user_id: user.id # Own posts
# User can read posts that are EITHER published OR owned by them
Controller Integration
Manual Authorization
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
authorize! :read, @post # Raises CanCan::AccessDenied if not authorized
end
def update
@post = Post.find(params[:id])
authorize! :update, @post
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
end
Automatic Loading and Authorization
class PostsController < ApplicationController
load_and_authorize_resource
def index
# @posts automatically loaded with accessible_by
end
def show
# @post automatically loaded and authorized
end
def create
# @post initialized and authorized
if @post.save
redirect_to @post
else
render :new
end
end
def update
# @post loaded and authorized
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
end
Benefits: Eliminates repetitive authorization code across RESTful actions.
Load and Authorize Options
# Specific actions only
load_and_authorize_resource only: [:show, :edit, :update, :destroy]
load_and_authorize_resource except: [:index]
# Different resource name
load_and_authorize_resource :article
# Custom find method
load_and_authorize_resource find_by: :slug
# Nested resources
class CommentsController < ApplicationController
load_and_authorize_resource :post
load_and_authorize_resource :comment, through: :post
end
# Skip loading (only authorize)
authorize_resource
# Skip authorization for specific actions
skip_authorize_resource only: [:index]
Fetching Authorized Records
accessible_by
Retrieve only records the user can access:
# In controller
def index
@posts = Post.accessible_by(current_ability)
end
# With specific action
@posts = Post.accessible_by(current_ability, :read)
@editable_posts = Post.accessible_by(current_ability, :update)
# Chainable with ActiveRecord
@published_posts = Post.published.accessible_by(current_ability)
@posts = Post.accessible_by(current_ability).where(category_id: params[:category_id])
Performance: Uses SQL conditions from ability rules for efficient database queries.
View Helpers
Conditional UI Elements
# Check single permission
<% if can? :update, @post %>
<%= link_to 'Edit', edit_post_path(@post) %>
<% end %>
<% if can? :destroy, @post %>
<%= link_to 'Delete', @post, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
# Negative check
<% if cannot? :update, @post %>
<p>You cannot edit this post</p>
<% end %>
# Multiple permissions
<% if can?(:update, @post) || can?(:destroy, @post) %>
<div class="post-actions">
<%= link_to 'Edit', edit_post_path(@post) if can? :update, @post %>
<%= link_to 'Delete', @post, method: :delete if can? :destroy, @post %>
</div>
<% end %>
# Check on class (useful in index views)
<% if can? :create, Post %>
<%= link_to 'New Post', new_post_path %>
<% end %>
Navigation Menus
<nav>
<%= link_to 'Posts', posts_path if can? :read, Post %>
<%= link_to 'New Post', new_post_path if can? :create, Post %>
<%= link_to 'Admin', admin_path if can? :manage, :all %>
</nav>
Handling Unauthorized Access
Exception Rescue
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |exception|
respond_to do |format|
format.html { redirect_to root_path, alert: exception.message }
format.json { render json: { error: exception.message }, status: :forbidden }
end
end
end
Custom Error Messages
# In Ability class
can :update, Post, user_id: user.id do |post|
post.user_id == user.id
end
# In controller with custom message
authorize! :update, @post, message: "You can only edit your own posts"
Flash Messages
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path, alert: "Access denied: #{exception.message}"
end
Common Patterns
Role-Based Authorization
# app/models/user.rb
class User < ApplicationRecord
ROLES = %w[guest user moderator admin].freeze
enum role: { guest: 0, user: 1, moderator: 2, admin: 3 }
def role?(check_role)
role.to_sym == check_role.to_sym
end
end
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # Guest user
if user.admin?
can :manage, :all
elsif user.moderator?
can :manage, Post
can :manage, Comment
can :read, User
elsif user.user?
can :read, :all
can :create, Post
can :manage, Post, user_id: user.id
can :manage, Comment, user_id: user.id
else
can :read, Post, published: true
end
end
end
Organization/Tenant-Based Authorization
class Ability
include CanCan::Ability
def initialize(user)
return unless user.present?
# User can manage resources in their organization
can :manage, Post, organization_id: user.organization_id
can :manage, Comment, post: { organization_id: user.organization_id }
# Admin can manage organization settings