RBACan
Role-Based Access Control for Rails. Assign roles to users, attach permissions to roles, and enforce access at the controller, view, and route level.
Table of Contents
- Installation
- Setup
- Configuration
- Defining Roles and Permissions
- Role Management
- Checking Permissions
- Querying Users by Role or Permission
- Controller Authorization
- View Helpers
- Route Constraints
- Development
- Contributing
- License
Installation
Add to your Gemfile:
gem 'rbacan'Then run:
bundle installSetup
1. Run the generator
rails generate rbacan:installThis copies four migrations, a seed helper file, and an initializer into your app.
2. Migrate
rails db:migrate3. Include the concern in your user model
class User < ApplicationRecord
include Rbacan::Permittable
end4. Define your roles and permissions in db/seeds.rb
Open db/copy_to_seeds.rb (created by the generator) and copy its contents into your db/seeds.rb. Fill in your roles and permissions, then run:
rails db:seedConfiguration
The generator creates config/initializers/rbacan.rb. All options are optional — the defaults work for a standard Devise setup.
Rbacan.configure do |config|
# Your user model name (default: "User")
config.permittable_class = "User"
# Override model class names if you have custom implementations
# config.role_class = "Rbacan::Role"
# config.permission_class = "Rbacan::Permission"
# config.user_role_class = "Rbacan::UserRole"
# config.role_permission_class = "Rbacan::RolePermission"
# Override table names if needed
# config.role_table = "roles"
# config.permission_table = "permissions"
# config.user_role_table = "user_roles"
# config.role_permission_table = "role_permissions"
# How to handle unauthorized access (see Controller Authorization)
# config.unauthorized_handler = :raise # default — raises Rbacan::NotAuthorized
# config.unauthorized_handler = :redirect # redirects to unauthorized_redirect_path
# config.unauthorized_handler = ->(controller, permission:, role:) {
# controller.render plain: "Forbidden", status: :forbidden
# }
# Redirect path used when unauthorized_handler is :redirect (default: "/")
# config.unauthorized_redirect_path = "/login"
endDefining Roles and Permissions
Use the Rbacan::RolesAndPermissions module in your seeds. All methods are idempotent — safe to run multiple times.
# db/seeds.rb
roles = ["admin", "moderator", "viewer"]
permissions = ["edit_post", "delete_post", "publish_post", "view_dashboard"]
Rbacan::RolesAndPermissions.create_roles(roles)
Rbacan::RolesAndPermissions.create_permissions(permissions)
# Assign permissions to each role
Rbacan::RolesAndPermissions.assign_permissions_to_role("admin", permissions)
Rbacan::RolesAndPermissions.assign_permissions_to_role("moderator", ["edit_post", "publish_post"])
Rbacan::RolesAndPermissions.assign_permissions_to_role("viewer", ["view_dashboard"])You can also create roles and permissions programmatically at runtime:
Rbacan.create_role("editor")
Rbacan.create_permission("manage_comments")
Rbacan.assign_permission_to_role("editor", "manage_comments")Role Management
user = User.find(1)
# Assign a role — idempotent, safe to call multiple times
user.assign_role("admin")
user.assign_role(:moderator)
# Remove a role
user.remove_role("moderator")Checking Permissions
On a single permission
user.can?("edit_post") # => true or false
user.can?(:edit_post) # symbols work tooChecking all permissions at once
# Returns true only if the user has every listed permission
user.can_all?(:edit_post, :delete_post, :publish_post)Checking roles
# Does the user have this specific role?
user.has_role?(:admin)
# Does the user have at least one of these roles?
user.has_any_role?(:admin, :moderator)Querying Users by Role or Permission
Use these ActiveRecord scopes to query your user table.
# All users with the admin role
User.with_role(:admin)
# All users who have a given permission (via any of their roles)
User.with_permission(:publish_post)
# Combine with other scopes
User.with_role(:moderator).where(active: true)Controller Authorization
Include Rbacan::Authorization in your ApplicationController (or any specific controller):
class ApplicationController < ActionController::Base
include Rbacan::Authorization
endThen use authorize! or authorize_role! as before_action callbacks:
class PostsController < ApplicationController
before_action -> { authorize!(:edit_post) }, only: [:edit, :update]
before_action -> { authorize!(:delete_post) }, only: [:destroy]
before_action -> { authorize_role!(:admin) }, only: [:admin_index]
endOr call them directly inside an action:
def destroy
authorize!(:delete_post)
@post.destroy
endHandling unauthorized access
The default behavior raises Rbacan::NotAuthorized. You can rescue it globally:
# app/controllers/application_controller.rb
rescue_from Rbacan::NotAuthorized, with: :handle_unauthorized
private
def handle_unauthorized(exception)
render plain: exception.message, status: :forbidden
endOr configure a different handler in the initializer:
# Redirect to a path instead of raising
config.unauthorized_handler = :redirect
config.unauthorized_redirect_path = "/login"
# Or use a fully custom lambda
config.unauthorized_handler = ->(controller, permission:, role:) {
controller.render json: { error: "Forbidden" }, status: :forbidden
}View Helpers
Rbacan::ViewHelpers is automatically included in all views. Use authorized? to conditionally render content:
<% authorized?(:delete_post) do %>
<%= link_to "Delete", post_path(@post), data: { turbo_method: :delete } %>
<% end %>
<% authorized?(:publish_post) do %>
<%= button_to "Publish", publish_post_path(@post) %>
<% end %>You can also use has_role? and has_any_role? directly in views since they are instance methods on the user:
<% if current_user.has_role?(:admin) %>
<%= link_to "Admin Panel", admin_root_path %>
<% end %>
<% if current_user.has_any_role?(:admin, :moderator) %>
<%= link_to "Moderation Queue", moderation_path %>
<% end %>Route Constraints
Restrict access to entire route namespaces based on role or permission. Add constraints in config/routes.rb:
# Restrict by role
constraints Rbacan::RouteConstraint.new(role: :admin) do
namespace :admin do
resources :users
resources :roles
end
end
# Restrict by permission
constraints Rbacan::RouteConstraint.new(permission: :access_dashboard) do
get "/dashboard", to: "dashboard#index"
endThe constraint reads the current user from the Warden session (used by Devise). For apps using a manual session, it falls back to looking up session[:user_id].
Development
bin/setup # install dependencies
bundle exec rake spec # run all tests
bundle exec rspec spec/rbacan_spec.rb # run a specific file
bundle exec rake install # install gem locallyTo release a new version:
- Bump the version in
lib/rbacan/version.rb gem build rbacan.gemspecgem push rbacan-<version>.gem
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/hamdi777/RBACan.
License
Available as open source under the MIT License.