agentsclimarketplace

Rails authorization cancancan

Skill Shoebtamboli/rails_claude_skills/lib/generators/claude/skills_library/rails-authorization-cancancan

A Rails generator gem that scaffolds Claude AI skills and agents for any Rails project.

Install
npx -y skills add Shoebtamboli/rails_claude_skills --skill rails-authorization-cancancan

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 14 stars14 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Authorization and permissions management for Ruby on Rails applications using CanCanCan. Use when: (1) Implementing role-based access control (RBAC), (2) Defining user permissions and abilities, (3) Restricting resource access in controllers, (4) Filtering queries based on user permissions, (5) Hiding/showing UI elements based on authorization, (6) Testing authorization logic, (7) Managing admin vs user vs guest permissions, (8) Implementing attribute-based access control

SKILL.md

13.7 KB, as published. Nobody here has run it

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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.