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.
npx -y skills add Shoebtamboli/rails_claude_skills --skill rails-authorization-cancancanAssembled 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:
- Multi-tenancy: references/multi-tenancy.md
- API authorization: references/api-authorization.md
- Complex permissions: references/complex-permissions.md