# Rabarber: Simple Role-Based Authorization for Rails [![Gem Version](https://badge.fury.io/rb/rabarber.svg)](http://badge.fury.io/rb/rabarber) [![Downloads](https://img.shields.io/gem/dt/rabarber.svg)](https://rubygems.org/gems/rabarber) [![Github Actions badge](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml/badge.svg)](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/enjaku4/rabarber.svg)](LICENSE) Rabarber is a role-based authorization library for Ruby on Rails. It provides a set of tools for managing user roles and defining access rules, with support for multi-tenancy through context. **Example of Usage:** Consider a CRM system where users with different roles have distinct access levels. For instance, the role `accountant` can access and manage invoice data, while the role `analyst` can only view it. Here's how you'd define that with Rabarber: ```rb class InvoicesController < ApplicationController grant_access roles: :accountant grant_access action: :index, roles: :analyst def index # Accessible to accountant and analyst end def update # Accessible to accountant only end end ``` ## Table of Contents **Gem Usage:** - [Installation](#installation) - [Configuration](#configuration) - [User Role Methods](#user-role-methods) - [Direct Role Management](#direct-role-management) - [Authorization](#authorization) - [Dynamic Authorization Rules](#dynamic-authorization-rules) - [When Unauthorized](#when-unauthorized) - [Context / Multi-tenancy](#context--multi-tenancy) - [View Helpers](#view-helpers) **Community Resources:** - [Getting Help and Contributing](#getting-help-and-contributing) - [License](#license) - [Code of Conduct](#code-of-conduct) - [Old Versions](#old-versions) ## Installation Add Rabarber to your Gemfile: ```rb gem "rabarber" ``` Install the gem: ```shell bundle install ``` Generate the migration to store roles (replace `users` with your table name if different): ```shell # For standard integer IDs rails generate rabarber:roles users # For UUID primary keys rails generate rabarber:roles users --uuid ``` Run the migration: ```shell rails db:migrate ``` ## Configuration Create an initializer to customize Rabarber's behavior (optional): ```rb Rabarber.configure do |config| # Enable/disable role caching (default: true) config.cache_enabled = true # Method to access current user (default: :current_user) config.current_user_method = :current_user # User model name (default: "User") config.user_model_name = "User" end ``` Roles are cached by default for better performance. Clear the cache manually when needed: ```rb Rabarber::Cache.clear ``` ## User Role Methods Your user model is automatically augmented with role management methods: ### Role Assignment ```rb # Assign roles (creates roles if they don't exist) user.assign_roles(:admin, :manager) # Assign only existing roles (don't create new ones) user.assign_roles(:accountant, :manager, create_new: false) # Revoke specific roles user.revoke_roles(:admin, :manager) # Revoke all roles user.revoke_all_roles ``` All role assignment and revocation methods return the list of roles currently assigned to the user. ### Role Queries ```rb # Check if user has any of the specified roles user.has_role?(:accountant, :manager) # Get user's roles in the global context user.roles # Get all user's roles grouped by context user.all_roles # Get users with any of the specified roles User.with_role(:admin, :manager) ``` ## Direct Role Management You can also directly manage roles available in the application: ```rb # Create a new role Rabarber.create_role(:admin) # => true if created, false if already exists # Rename a role Rabarber.rename_role(:admin, :administrator) # => true if renamed, false if new name exists or role is assigned # Force rename even if role is assigned Rabarber.rename_role(:admin, :administrator, force: true) # Remove a role Rabarber.delete_role(:admin) # => true if deleted, false if role is assigned # Force deletion even if role is assigned Rabarber.delete_role(:admin, force: true) # List available roles in the global context Rabarber.roles # List all available roles grouped by context Rabarber.all_roles ``` ## Authorization ### Setup Include `Rabarber::Authorization` module in your controllers and configure authorization: ```rb class ApplicationController < ActionController::Base include Rabarber::Authorization # Enable authorization check for all actions in all controllers by default with_authorization end ``` You can also enable authorization checks selectively. Both `with_authorization` and `skip_authorization` work exactly the same as Rails' `before_action` and `skip_before_action` methods: ```rb class TicketsController < ApplicationController # Skip authorization for specific actions skip_authorization only: [:index, :show] end class InvoicesController < ApplicationController # Enable authorization for all actions except index with_authorization except: [:index] end ``` Authorization requires an authenticated user. Rabarber will raise an error if no user is found via the configured `current_user_method`. Ensure authentication happens before authorization. ### Authorization Rules Rabarber follows a deny-by-default principle: if no `grant_access` rule is defined for an action or controller, access is denied to everyone. Define authorization rules using `grant_access`: ```rb class TicketsController < ApplicationController # Controller-wide access grant_access roles: :admin # Action-specific access grant_access action: :index, roles: [:manager, :support] def index # Accessible to admin, manager, and support roles end def destroy # Accessible to admin role only end end ``` Authorization rules are additive - rules defined multiple times combine, and controller-wide rules are inherited by subclasses: ```rb class BaseController < ApplicationController # Admin can access everything grant_access roles: :admin end class InvoicesController < BaseController # Accountant can also access InvoicesController (along with admin) grant_access roles: :accountant grant_access action: :index, roles: :manager grant_access action: :index, roles: :supervisor def index # Index is accessible to admin, accountant, manager, and supervisor end end ``` It's possible to omit roles to allow unrestricted access: ```rb class UnrestrictedController < ApplicationController # Allow all users to access all actions grant_access end class MixedController < ApplicationController # Unrestricted index action grant_access action: :index def index # Accessible to all users end # Restricted show action grant_access action: :show, roles: :member def show # Accessible to members only end end ``` ## Dynamic Authorization Rules For more complex scenarios, Rabarber supports dynamic authorization rules: ```rb class OrdersController < ApplicationController grant_access roles: :manager, unless: -> { current_user.suspended? } grant_access action: :show, roles: :client, if: :user_company_matches_order? def show # ... end private def user_company_matches_order? current_user.company == Order.find(params[:id]).company end end ``` You can pass a dynamic rule as an `if` or `unless` argument, which can be a symbol or a proc. Symbols refer to instance methods, and procs are evaluated in the controller at request time. Dynamic rules can also be used without roles at all, allowing you to define custom logic or even delegate to custom policy objects: ```rb class InvoicesController < ApplicationController grant_access action: :update, unless: -> { invoice.period_closed? } def update # ... end grant_access action: :destroy, if: :destroy_allowed? def destroy # ... end private def destroy_allowed? InvoicePolicy.new(current_user).destroy?(invoice) end def invoice @invoice ||= Invoice.find(params[:id]) end end ``` ## When Unauthorized By default, when unauthorized, Rabarber will redirect back (HTML format) or return 403 (other formats). You can override `when_unauthorized` method to customize unauthorized access behavior: ```rb class ApplicationController < ActionController::Base include Rabarber::Authorization with_authorization private def when_unauthorized # Custom behavior to hide existence of protected resources head :not_found end end ``` The `when_unauthorized` method can be overridden in any controller to provide controller-specific unauthorized access handling. ## Context / Multi-tenancy Rabarber supports multi-tenancy through its context feature. All Rabarber methods accept a `context` parameter, allowing you to work with roles within specific scopes. By default, context is `nil`, meaning roles are global. Context can also be an instance of an `ActiveRecord` model or a class. For example, in a project management app, you might want users to have different roles in different projects - someone could be an `owner` in one project but just a `member` in another. ### Contextual Role Assignment and Queries ```rb # Assign roles within a specific model instance user.assign_roles(:owner, context: project) user.assign_roles(:member, context: project) # Assign roles within a model class user.assign_roles(:admin, context: Project) # Check contextual roles user.has_role?(:owner, context: project) user.has_role?(:admin, context: Project) # Revoke roles user.revoke_roles(:owner, context: project) # Get user roles user.roles(context: project) # Get users with a role User.with_role(:member, context: project) ``` ### Contextual Role Management ```rb # Create a new role within a context Rabarber.create_role(:admin, context: Project) # Rename a role within a context Rabarber.rename_role(:admin, :owner, context: project) # Remove a contextual role Rabarber.delete_role(:admin, context: project) # List available roles within a specific context Rabarber.roles(context: project) ``` ### Contextual Authorization In authorization rules, in addition to specifying context explicitly, you can also provide a proc or a symbol (similar to dynamic rules): ```rb class ProjectsController < ApplicationController grant_access roles: :admin, context: Project # Method-based context resolution grant_access action: :show, roles: :member, context: :current_project def show # Accessible to Project admin and members of the current project end # Proc-based context resolution grant_access action: :update, roles: :owner, context: -> { Project.find(params[:id]) } def update # Accessible to Project admin and owner of the current project end private def current_project @current_project ||= Project.find(params[:id]) end end ``` ### Orphaned Contextual Roles When a context object is deleted from your database, its associated roles become orphaned and ignored by Rabarber. To clean up orphaned roles, use: ```rb Rabarber.prune ``` ### Context Migrations When you rename or remove models used as contexts, you need to update Rabarber's stored context data accordingly. Use these irreversible data migrations: ```rb # Rename a context class (e.g., when you rename your Ticket model to Task) migrate_authorization_context!("Ticket", "Task") # Remove context data (e.g., when you delete the Ticket model entirely) delete_authorization_context!("Ticket") ``` ## View Helpers Include view helpers in your application: ```rb module ApplicationHelper include Rabarber::Helpers end ``` Use conditional rendering based on roles: ```erb <%= visible_to(:admin, :manager) do %>
<% end %> <%= hidden_from(:guest) do %>
<% end %> <%= visible_to(:owner, context: @project) do %>
<% end %> ``` Like authorization, these helpers require an authenticated user. In views that can be rendered for both authenticated and unauthenticated visitors, wrap them in your authentication check. ## Getting Help and Contributing ### Getting Help Have a question or need assistance? Open a discussion in the [discussions section](https://github.com/enjaku4/rabarber/discussions) for: - Usage questions - Implementation guidance - Open-ended ideas or suggestions ### Issues [Issues](https://github.com/enjaku4/rabarber/issues) track bugs, planned features, and other work. When reporting a bug, please include: - A clear description of the problem - Steps to reproduce the issue - Your environment details (Rails and Ruby versions, OS, etc.) ### Contributing Code Any open issue is free to pick up or discuss. Check the [project board](https://github.com/users/enjaku4/projects/10) or [issues tab](https://github.com/enjaku4/rabarber/issues). Before contributing, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md). ## License The gem is available as open source under the terms of the [MIT License](https://github.com/enjaku4/rabarber/blob/main/LICENSE.txt). ## Code of Conduct Everyone interacting in the Rabarber project is expected to follow the [code of conduct](https://github.com/enjaku4/rabarber/blob/main/CODE_OF_CONDUCT.md). ## Old Versions Only the latest major version is actively maintained. READMEs for older versions are available here for reference: [v5.x.x](https://github.com/enjaku4/rabarber/blob/12a23858e974f5cc0a4ebddfc18bdf84171dd554/README.md) | [v4.x.x](https://github.com/enjaku4/rabarber/blob/9353e70281971154d5acd70693620197a132c543/README.md) | [v3.x.x](https://github.com/enjaku4/rabarber/blob/3bb273de7e342004abc7ef07fa4d0a9a3ce3e249/README.md) | [v2.x.x](https://github.com/enjaku4/rabarber/blob/875b357ea949404ddc3645ad66eddea7ed4e2ee4/README.md) | [v1.x.x](https://github.com/enjaku4/rabarber/blob/b81428429404e197d70317b763e7b2a21e02c296/README.md)