RoleFu is a modern, explicit role management gem for Ruby on Rails. It is designed as a cleaner, more performant alternative to legacy role gems, providing full control over role assignments and granular permissions.
- Explicit Models: Uses an explicit
RoleAssignmentjoin model instead of hidden tables, making it easy to add metadata or audit trails. - N+1 Prevention: Built-in support for
has_cached_role?and optimized scopes. - Strict by Default: Resource-specific checks are strict, ensuring global roles don't accidentally leak permissions unless configured otherwise.
- Advanced Features: Supports temporal (expiring) roles, metadata, audit logging, and granular abilities.
- Modern Infrastructure: Fully compatible with Rails 7.2 through 8.1, includes Lefthook and Appraisal support.
Add this line to your application's Gemfile:
gem 'role_fu'And then execute:
bundle install- Install Configuration:
rails generate role_fu:install-
Generate Models:
You can generate the role models with flexible arguments:
- Default (Role & User):
rails generate role_fu # Generates 'Role' and 'RoleAssignment', linked to 'User' - Custom Role Name:
rails generate role_fu Group # Generates 'Group' and 'GroupAssignment', linked to 'User' - Custom Role & User Names:
rails generate role_fu Group Account # Generates 'Group' and 'GroupAssignment', linked to 'Account'
- Default (Role & User):
-
Run Migrations:
rails db:migrateInclude RoleFu::Roleable in your User model (done automatically by the generator).
class User < ApplicationRecord
include RoleFu::Roleable
# Optional callbacks
role_fu_options after_add: :notify_user
def notify_user(role)
# ...
end
enduser = User.find(1)
# Global roles
user.grant(:admin)
user.has_role?(:admin) # => true
user.revoke(:admin)
# Resource-specific roles
org = Organization.first
user.grant(:manager, org)
user.has_role?(:manager, org) # => true
user.has_role?(:manager) # => false (strict check)
user.has_role?(:manager, :any) # => true
user.only_has_role?(:manager, org) # => true if this is their only roleUser.with_role(:admin)
User.with_role(:manager, org)
User.without_role(:admin)
User.with_any_role(:admin, :editor)
User.with_all_roles(:admin, :manager)Roles can be assigned with an expiration time. They are automatically filtered out from queries once expired.
user.grant(:manager, org, expires_at: 1.week.from_now)
# To physically remove expired roles from the database:
# rake role_fu:cleanup
# Or generate a scheduled ActiveJob:
# rails generate role_fu:jobAttach arbitrary metadata to a role assignment.
user.grant(:manager, org, meta: { assigned_by: current_user.id, reason: "Project lead" })Track every grant, revoke, and update (e.g., expiration extensions).
Setup:
rails generate role_fu:audit
# OR with a custom name:
# rails generate role_fu:audit MyAudit
rails db:migrateUsage:
Wrap changes in with_actor to capture the responsible user:
RoleFu.with_actor(current_user) do
user.grant(:manager, org)
end
# Check history
RoleAssignmentAudit.where(user: user).last
# => #<RoleAssignmentAudit operation: "INSERT", whodunnit: "1", ...>Attach granular permissions to roles.
Setup:
rails generate role_fu:abilities
# OR with a custom name:
# rails generate role_fu:abilities MyPermission
rails db:migrateUsage:
# Setup permissions
manager_role = Role.find_by(name: "manager")
manager_role.permissions.create(action: "posts.edit")
# Check abilities
user.role_fu_can?("posts.edit") # => trueclass Ability
include CanCan::Ability
include RoleFu::Adapters::CanCanCan
def initialize(user)
role_fu_load_permissions!(user)
end
endclass ApplicationPolicy
include RoleFu::Adapters::Pundit
endPostPolicy#update? will automatically check user.role_fu_can?('posts.update').
Use has_cached_role? when roles are preloaded to avoid database roundtrips.
users = User.includes(:roles).all
users.each do |user|
user.has_cached_role?(:admin) # No extra queries!
endInclude RoleFu::Resourceable in any model that should have roles scoped to it.
class Organization < ApplicationRecord
include RoleFu::Resourceable
end
org = Organization.first
org.users_with_role(:manager)
org.available_roles # ["manager", "admin"]
# Scopes
Organization.with_role(:manager, user)
Organization.without_role(:manager, user)Customize your model names in config/initializers/role_fu.rb:
RoleFu.configure do |config|
config.user_class_name = "Account"
config.role_class_name = "Group"
# Enable Rolify-style permissive checks (Global roles override resource checks)
config.global_roles_override = true
endIf you prefer different terminology (e.g., "Groups" instead of "Roles"), you can alias the methods in your models:
class User < ApplicationRecord
include RoleFu::Roleable
role_fu_alias :group
end
# Now you can use:
user.add_group(:admin)
user.has_group?(:admin)
User.in_group(:admin) # Alias for with_group/with_role
User.not_in_group(:admin) # Alias for without_group/without_role- Code Changes: Replace
rolifywithinclude RoleFu::Roleableandresourcifywithinclude RoleFu::Resourceable. - Data Migration:
INSERT INTO role_assignments (user_id, role_id, created_at, updated_at)
SELECT user_id, role_id, NOW(), NOW()
FROM users_roles;- Behavior: Set
config.global_roles_override = trueif you rely on global roles satisfying resource checks.
The gem is available as open source under the terms of the MIT License.