Skip to content
alec-c4Public

About

Explicit role management gem for Ruby on Rails

Topics

Resources

Stars

17 stars

Watchers

1 watching

Forks

Repository files navigation

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.

About

Explicit role management gem for Ruby on Rails

Topics

Resources

Stars

17 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages