skills/shoebtamboli/rails_claude_skills/rails-authorization-cancancan

rails-authorization-cancancan

SKILL.md

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
    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:

Resources

Weekly Installs
4
GitHub Stars
4
First Seen
Feb 17, 2026
Installed on
opencode4
gemini-cli4
github-copilot4
amp4
codex4
kimi-cli4