Get the FREE Ultimate OpenClaw Setup Guide →

rails-authorization-cancancan

npx machina-cli add skill Shoebtamboli/rails_claude_skills/rails-authorization-cancancan --openclaw
Files (1)
SKILL.md
13.7 KB

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

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

  1. Step 1: Add cancancan to your Gemfile and run bundle install.
  2. Step 2: Generate the Ability class: rails generate cancan:ability
  3. 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.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers