# Shoebtamboli Rails Claude Skills Rails Authorization Cancancan

> Rails Authorization with CanCanCan

- Skill: `tomevault-io/shoebtamboli-rails-claude-skills-rails-authorization-cancanc` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/shoebtamboli-rails-claude-skills-rails-authorization-cancanc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/shoebtamboli-rails-claude-skills-rails-authorization-cancanc/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/shoebtamboli-rails-claude-skills-rails-authorization-cancanc

---


# 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

```bash
# 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:

```ruby
# 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

```ruby
:read    # :index and :show
:create  # :new and :create
:update  # :edit and :update
:destroy # :destroy

:manage  # All actions (use carefully!)
```

### Custom Actions

```ruby
can :publish, Post
can :archive, Post
can :approve, Comment
```

### Multiple Resources

```ruby
can :read, [Post, Comment, Category]
can :manage, [User, Post], user_id: user.id
```

## Ability Conditions

### Hash Conditions

```ruby
# 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

```ruby
# 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

```ruby
# 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

```ruby
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

```ruby
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

```ruby
# 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:

```ruby
# 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

```ruby
# 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

```ruby
<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

```ruby
# 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

```ruby
# 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

```ruby
rescue_from CanCan::AccessDenied do |exception|
  redirect_to root_path, alert: "Access denied: #{exception.message}"
end
```

## Common Patterns

### Role-Based Authorization

```ruby
# 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

```ruby
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

```ruby
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

```ruby
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

```ruby
# 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

```ruby
# spec/support/cancan.rb
RSpec.configure do |config|
  config.include CanCan::Ability
end
```

### Testing Abilities

```ruby
# 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

```ruby
# 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

```ruby
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

```ruby
# 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

```ruby
# Prevent N+1 queries
@posts = Post.accessible_by(current_ability).includes(:user, :comments)
```

### Caching Abilities

```ruby
# 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:

```ruby
# 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](references/multi-tenancy.md)
- **API authorization**: [references/api-authorization.md](references/api-authorization.md)
- **Complex permissions**: [references/complex-permissions.md](references/complex-permissions.md)

## Resources

- [CanCanCan GitHub](https://github.com/CanCanCommunity/cancancan)
- [CanCanCan Wiki](https://github.com/CanCanCommunity/cancancan/wiki)

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/shoebtamboli) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-13 -->

