Project

role_fu

0.0
The project is in a healthy, maintained state
RoleFu provides explicit role management with Role and RoleAssignment models, avoiding hidden HABTM tables. Supports scopes, resource-specific roles, and cleaner architecture.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.0
~> 0.21
~> 2.0

Runtime

 Project Readme

RoleFu

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.

Gem Version Build Status

Why RoleFu?

  • Explicit Models: Uses an explicit RoleAssignment join 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.

Installation

Add this line to your application's Gemfile:

gem 'role_fu'

And then execute:

bundle install

Setup

  1. Install Configuration:
rails generate role_fu:install
  1. 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'
  2. Run Migrations:

rails db:migrate

Usage

Roleable (User Model)

Include 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
end

Basic Operations

user = 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 role

Role names are normalized ("Admin", "admin", "Admin User" and "admin-user" all resolve to the same role) so casing/formatting mistakes don't silently create duplicate roles. Normalization does not singularize — "sales" stays "sales" — since blindly singularizing arbitrary business terms tends to mangle them. Upgrading? Existing rows with mixed-case names aren't rewritten automatically — rails generate role_fu:upgrade will detect and warn about them (it won't backfill on its own: two differently-cased roles may already coexist and need a human to decide how to merge them).

Scopes (Finders)

User.with_role(:admin)
User.with_role(:manager, org)
User.without_role(:admin)
User.with_any_role(:admin, :editor)
User.with_all_roles(:admin, :manager)

Advanced Features

1. Temporal Roles (Expiration)

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:job

2. Metadata

Attach arbitrary metadata to a role assignment.

user.grant(:manager, org, meta: { assigned_by: current_user.id, reason: "Project lead" })

3. Audit Log

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:migrate

Usage: 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", ...>

4. Role Abilities (Permissions)

Attach granular permissions to roles.

Setup:

rails generate role_fu:abilities
# OR with a custom name:
# rails generate role_fu:abilities MyPermission
rails db:migrate

Usage:

# Setup permissions
manager_role = Role.find_by(name: "manager")
manager_role.permissions.create(action: "posts.edit")

# Check abilities
user.role_fu_can?("posts.edit") # => true

Field-level granularity:

Permissions can optionally be scoped to a single attribute. A permission granted without a field is a wildcard that satisfies any field-scoped check for that same action — so existing action-only permissions keep working unchanged.

support_role.permissions.create(action: "reports.update", field: "status")

user.role_fu_can?("reports.update")                # => true  (can do *something* here)
user.role_fu_can?("reports.update", field: :status) # => true
user.role_fu_can?("reports.update", field: :amount) # => false

# Build a form / strong params allowlist dynamically:
user.role_fu_permitted_fields("reports.update")
# => ["status"]                    if scoped to specific fields
# => :all                          if granted without a field restriction
# => []                            if the action isn't granted at all

Upgrading an existing permissions table? Run rails generate role_fu:upgrade — it inspects your current schema/config and generates only the migrations you're actually missing (here: adding field to permissions). Safe to re-run any time, regardless of which version you're upgrading from. Without the column, RoleFu transparently falls back to action-only checks.

The CanCanCan adapter translates field-scoped permissions into CanCanCan's own native attribute restriction (can :update, Report, :status) instead of role_fu re-implementing attribute-level authorization itself.


Adapters (Pundit & CanCanCan)

CanCanCan

class Ability
  include CanCan::Ability
  include RoleFu::Adapters::CanCanCan

  def initialize(user)
    role_fu_load_permissions!(user)
  end
end

Pundit

class ApplicationPolicy
  include RoleFu::Adapters::Pundit
end

PostPolicy#update? will automatically check user.role_fu_can?('posts.update').

Lightweight Guard (no authorization gem)

If you don't want to add Pundit or CanCanCan as a dependency just to raise on a missing role, RoleFu::Authorizable gives you two guard-clause helpers built on top of has_role? / role_fu_can?. It is intentionally not an allow/deny DSL or a rule-resolution engine — for anything beyond "raise unless this check passes" (policy objects, scopes, composable rules), use the Pundit or CanCanCan adapters instead.

class ApplicationController < ActionController::Base
  include RoleFu::Authorizable

  rescue_from RoleFu::AccessDenied, with: :render_forbidden

  private

  def render_forbidden
    head :forbidden
  end
end

class PostsController < ApplicationController
  def destroy
    role_fu_authorize!(:admin)          # raises RoleFu::AccessDenied unless current_user.has_role?(:admin)
    role_fu_can!("posts.destroy")       # raises RoleFu::AccessDenied unless current_user.role_fu_can?("posts.destroy")
    # ...
  end
end

Resolves the acting user via current_user by default; override role_fu_current_user for jobs/service objects that don't have one.


Performance (N+1 Prevention)

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!
end

Resourceable

Include RoleFu::Resourceable in any model that should have roles scoped to it.

class Organization < ApplicationRecord
  include RoleFu::Resourceable
end

org = Organization.first
org.users                   # All users with ANY role (via has_many :through)
org.users_with_role(:manager)
org.users_with_roles        # All users with ANY role on this org
org.available_roles         # ["manager", "admin"]
org.users_grouped_by_role   # { "manager" => [user1], "admin" => [user2] }

# Cleanup
org.destroy_role(:manager)  # Removes the "manager" role and all its assignments from this org

# Scopes
Organization.with_role(:manager, user)
Organization.without_role(:manager, user)

Configuration

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

  # Enable dynamic shortcuts (e.g. user.is_admin?)
  # Default is nil (disabled). Recommended: "is_%{role}?"
  config.dynamic_shortcuts_pattern = "is_%{role}?"
end

Dynamic Shortcuts

RoleFu supports dynamic methods for quick role checking if you configure a pattern.

Configuration:

# In config/initializers/role_fu.rb
RoleFu.configure do |config|
  config.dynamic_shortcuts_pattern = "is_%{role}?"
end

Usage:

user.is_admin?              # Equivalent to user.has_role?(:admin)
user.is_manager?(org)       # Equivalent to user.has_role?(:manager, org)

Custom Pattern:

If you use groups:

config.dynamic_shortcuts_pattern = "in_%{role}_group?"

# Usage:
user.in_admin_group?        # Equivalent to user.has_role?(:admin)

Custom Aliases (e.g. Groups)

If 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

Upgrading RoleFu

After bumping the gem version, run:

rails generate role_fu:upgrade
rails db:migrate

It inspects your actual schema/config (not "which version you were on" — nothing persists that reliably) and generates only what's missing, so it's safe to run regardless of how many releases you skipped, and safe to re-run. Currently checks for: the field column on permissions (see Field-level granularity), and role names that don't match RoleFu's normalized format (reported only — never auto-merged, since two differently-cased roles may already coexist).

Migrating from Rolify

  1. Code Changes: Replace rolify with include RoleFu::Roleable and resourcify with include RoleFu::Resourceable.
  2. Data Migration:
INSERT INTO role_assignments (user_id, role_id, created_at, updated_at)
SELECT user_id, role_id, NOW(), NOW()
FROM users_roles;
  1. Behavior: Set config.global_roles_override = true if you rely on global roles satisfying resource checks.

License

The gem is available as open source under the terms of the MIT License.