rails-authorization-cancancan
npx machina-cli add skill Shoebtamboli/rails_claude_skills/rails-authorization-cancancan --openclawRails 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
can :manage, Organization, id: user.organization_id if user.admin?
end
end
Time-Based Authorization
class Ability
include CanCan::Ability
def initialize(user)
return unless user.present?
# Can edit posts within 1 hour of creation
can :update, Post do |post|
post.user_id == user.id && post.created_at > 1.hour.ago
end
# Can read posts after publication date
can :read, Post, ["published_at <= ?", Time.current]
end
end
Attribute-Based Authorization
class Ability
include CanCan::Ability
def initialize(user)
return unless user.present?
# Users can update specific attributes of their own posts
can [:update], Post, user_id: user.id
# Only admins can change published status
cannot :update, Post, :published unless user.admin?
# Users can update their profile but not role
can :update, User, id: user.id
cannot :update, User, :role
end
end
Strong Parameters with CanCanCan
# app/controllers/posts_controller.rb
def post_params
params.require(:post).permit(:title, :body, :published)
end
# Restrict based on abilities
def post_params
params.require(:post).permit(
current_user.admin? ? [:title, :body, :published] : [:title, :body]
)
end
Testing
RSpec Setup
# spec/support/cancan.rb
RSpec.configure do |config|
config.include CanCan::Ability
end
Testing Abilities
# spec/models/ability_spec.rb
require 'rails_helper'
require 'cancan/matchers'
RSpec.describe Ability, type: :model do
subject(:ability) { Ability.new(user) }
describe 'Guest user' do
let(:user) { nil }
it { is_expected.to be_able_to(:read, Post.new(published: true)) }
it { is_expected.not_to be_able_to(:create, Post) }
it { is_expected.not_to be_able_to(:update, Post) }
end
describe 'Regular user' do
let(:user) { create(:user) }
let(:own_post) { create(:post, user: user) }
let(:other_post) { create(:post) }
it { is_expected.to be_able_to(:read, Post) }
it { is_expected.to be_able_to(:create, Post) }
it { is_expected.to be_able_to(:update, own_post) }
it { is_expected.not_to be_able_to(:update, other_post) }
it { is_expected.to be_able_to(:destroy, own_post) }
it { is_expected.not_to be_able_to(:destroy, other_post) }
end
describe 'Admin user' do
let(:user) { create(:user, admin: true) }
it { is_expected.to be_able_to(:manage, :all) }
end
end
Testing Controllers
# spec/controllers/posts_controller_spec.rb
RSpec.describe PostsController, type: :controller do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:post) { create(:post, user: user) }
before { sign_in user }
describe 'GET #edit' do
context 'when editing own post' do
it 'allows access' do
get :edit, params: { id: post.id }
expect(response).to have_http_status(:ok)
end
end
context 'when editing other user post' do
let(:other_post) { create(:post, user: other_user) }
it 'denies access' do
expect {
get :edit, params: { id: other_post.id }
}.to raise_error(CanCan::AccessDenied)
end
end
end
end
Testing accessible_by
RSpec.describe 'Post access', type: :model do
let(:user) { create(:user) }
let(:admin) { create(:user, admin: true) }
let!(:published_post) { create(:post, published: true) }
let!(:draft_post) { create(:post, published: false, user: user) }
let!(:other_draft) { create(:post, published: false) }
it 'returns correct posts for user' do
ability = Ability.new(user)
accessible = Post.accessible_by(ability)
expect(accessible).to include(published_post, draft_post)
expect(accessible).not_to include(other_draft)
end
it 'returns all posts for admin' do
ability = Ability.new(admin)
accessible = Post.accessible_by(ability)
expect(accessible).to include(published_post, draft_post, other_draft)
end
end
Performance Considerations
Use Hash Conditions for Collections
# Good - generates SQL query
can :read, Post, user_id: user.id
@posts = Post.accessible_by(current_ability)
# Bad - cannot generate SQL, will raise error
can :read, Post do |post|
post.user_id == user.id
end
@posts = Post.accessible_by(current_ability) # Error!
Eager Loading
# Prevent N+1 queries
@posts = Post.accessible_by(current_ability).includes(:user, :comments)
Caching Abilities
# Cache ability checks in instance variable
def current_ability
@current_ability ||= Ability.new(current_user)
end
Integration with Pundit
If migrating from Pundit or using both:
# CanCanCan uses a single Ability class
# Pundit uses policy classes per model
# They can coexist, but choose one primary approach
# CanCanCan: Centralized, better for simple RBAC
# Pundit: Decentralized, better for complex domain logic
Advanced Patterns
For more complex scenarios, see:
- Multi-tenancy: references/multi-tenancy.md
- API authorization: references/api-authorization.md
- Complex permissions: references/complex-permissions.md
Resources
Source
git clone https://github.com/Shoebtamboli/rails_claude_skills/blob/main/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.mdView on GitHub Overview
CanCanCan provides an authorization library for Rails, centralizing permission logic in a single Ability class to keep rules DRY and maintainable. It defines who can do what with which resources and supports role-based access, UI gating, and admin vs guest scenarios. This skill covers defining abilities, restricting access in controllers, and testing authorization logic.
How This Skill Works
Define all permissions in app/models/ability.rb by including CanCan::Ability and implementing initialize(user). Use can and block conditions to express rules, and hash conditions for database queries. Controllers can enforce rules with authorize! or load_and_authorize_resource, while views use can? helpers to hide or show UI elements.
When to Use It
- Implementing role-based access control (RBAC)
- Defining user permissions and abilities
- Restricting resource access in controllers
- Hiding/showing UI elements based on authorization
- Testing authorization logic
Quick Start
- Step 1: Add cancancan to your Gemfile and run bundle install.
- Step 2: Generate the Ability class: rails generate cancan:ability
- Step 3: Define abilities in app/models/ability.rb and use authorize! in controllers (or load_and_authorize_resource for automation).
Best Practices
- Structure rules hierarchically (guest → user → admin) for clarity.
- Keep all authorization logic in a single Ability class (app/models/ability.rb).
- Prefer hash conditions for database queries; reserve block conditions for in-memory checks.
- Use load_and_authorize_resource for automatic loading and authorization in controllers.
- Write tests covering guest, user, and admin scenarios to ensure coverage.
Example Use Cases
- Guest users can read published posts; comments may be readable by all.
- Signed-in users can read all posts and create/update their own posts and comments.
- Admin users can manage all resources (can :manage, :all).
- Custom actions: can :publish, Post; can :archive, Post; can :approve, Comment.
- Multiple resources: can read [Post, Comment, Category]; can manage [User, Post] where user_id: user.id.