Rabarber Authorization Library

repository·main·Indexed 41 days ago

https://github.com/enjaku4/rabarber

A role-based authorization library for Ruby on Rails featuring a deny-by-default model, multi-tenancy support via a context system, and easy role management for user models. It provides controller-level authorization with `grant_access`, view helpers like `visible_to` and `hidden_from`, and the `Rabarber::Roleable` concern for model integration.

Tokens
6.2K
Snippets
29
Records
32
Agent score
51%

What's inside Rabarber

  1. Implement multi-tenancy with Context

    main

    Rabarber supports multi-tenancy by allowing roles to be scoped to a context. A context can be an ActiveRecord model instance, a model class, or nil (for global roles).

    Contextual Role Management

    • user.assign_roles(:role, context: object): Assigns a role within a specific instance scope.
    • user.has_role?(:role, context: Class): Checks for a role within a class scope.
    • Rabarber.create_role(:role, context: object): Creates a role within a context.
    • Rabarber.roles(context: object): Lists roles available in a specific context.

    Contextual Authorization

    In controllers, you can resolve context using a Symbol (instance method) or a Proc:

    # Using a method-based context
    grant_access action: :show, roles: :member, context: :current_project
    
    # Using a Proc-based context
    grant_access action: :update, roles: :owner, context: -> { Project.find(params[:id]) }

    Maintenance

    • Rabarber.prune: Cleans up orphaned roles if context objects were deleted from the database without revoking roles.
    • migrate_authorization_context!("OldModel", "NewModel"): Irreversible migration to rename a context class.
    • delete_authorization_context!("ModelName"): Irreversible migration to remove context data for a deleted model.
    # Contextual Assignment
    user.assign_roles(:owner, context: project)
    
    # Contextual Query
    user.has_role?(:owner, context: project)
    
    # Contextual Authorization in Controller
    class ProjectsController < ApplicationController
      grant_access action: :show, roles: :member, context: :current_project
    
      private
    
      def current_project
        @current_project ||= Project.find(params[:id])
      end
    end
  2. Implement controller authorization

    main

    To use authorization in controllers, include the Rabarber::Authorization module.

    Setup

    Use with_authorization to enable checks for all actions in a controller. You can use skip_authorization (similar to skip_before_action) to exclude specific actions, or with_authorization except: [...] to exclude specific actions.

    Note: Authorization requires an authenticated user. Ensure your authentication logic runs before Rabarber's authorization checks.

    Defining Rules

    Rabarber follows a deny-by-default principle. If no grant_access rule is defined, access is denied.

    • grant_access roles: :role_name: Grants access to the entire controller to the specified role(s).
    • grant_access action: :index, roles: [:role1, :role2]: Grants access to a specific action for specific roles.
    • grant_access: (Omitting roles) Allows unrestricted access to the specified action or controller.

    Rules are additive and controller-wide rules are inherited by subclasses.

    class ApplicationController < ActionController::Base
      include Rabarber::Authorization
      with_authorization
    end
    
    class InvoicesController < 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
      end
    end
  3. Install Rabarber in a Rails application

    main

    To install Rabarber, add the gem to your Gemfile and run bundle install. Then, generate the necessary migration to store roles. You can specify whether your primary keys are standard integers or UUIDs.

    For standard integer IDs:

    rails generate rabarber:roles users

    For UUID primary keys:

    rails generate rabarber:roles users --uuid

    Finally, run the migration:

    rails db:migrate

    Note: Replace users with your actual user table name if it differs.

    gem "rabarber"
    bundle install
    rails generate rabarber:roles users
    rails db:migrate
  4. Include Rabarber view helpers in your Rails application

    main

    To use role-based conditional rendering in your views, you must include the Rabarber::Helpers module in your application's helper module (typically ApplicationHelper).

    module ApplicationHelper
      include Rabarber::Helpers
    end
  5. Handle unauthorized requests

    main

    When a user fails an authorization check, Rabarber calls the when_unauthorized method. By default, the behavior depends on the request format:

    • HTML requests: Redirects the user back to the root_path.
    • Non-HTML requests (e.g., JSON): Returns a 403 Forbidden status code (head :forbidden).

    You can override this method in your controller to implement custom error handling (e.g., rendering a specific error page or JSON error message).

  6. Configure Rabarber settings

    main

    You can customize Rabarber's behavior using an initializer. The following configuration options are available:

    • config.cache_enabled: Enables or disables role caching (defaults to true).
    • config.current_user_method: The method used to access the current user (defaults to :current_user).
    • config.user_model_name: The name of your user model (defaults to "User").

    To manually clear the role cache, use Rabarber::Cache.clear.

    Rabarber.configure do |config|
      config.cache_enabled = true
      config.current_user_method = :current_user
      config.user_model_name = "User"
    end
  7. Add role management to models using Roleable

    main

    To enable Rabarber authorization on a model (e.g., User), include the Rabarber::Roleable concern. This adds a many-to-many relationship to Rabarber::Role via the rabarber_roles association and provides methods for querying, assigning, and checking roles.

    Note: Rabarber::Roleable can only be included once per model class.

    class User < ApplicationRecord
      include Rabarber::Roleable
    end
  8. Customize unauthorized access behavior

    main

    By default, Rabarber redirects back (for HTML requests) or returns a 403 Forbidden status. You can override the when_unauthorized method in your controller to change this behavior (e.g., to return a 404 Not Found to hide the existence of a resource).

    class ApplicationController < ActionController::Base
      include Rabarber::Authorization
      with_authorization
    
      private
    
      def when_unauthorized
        head :not_found
      end
    end
  9. Use dynamic authorization rules

    main

    For complex logic, you can use if or unless arguments in grant_access. These can be a Symbol (referring to an instance method in the controller) or a Proc (evaluated at request time).

    Dynamic rules can also be used without roles to delegate authorization to custom logic or policy objects.

    class OrdersController < ApplicationController
      # Using a Proc
      grant_access roles: :manager, unless: -> { current_user.suspended? }
    
      # Using a Symbol (instance method)
      grant_access action: :show, roles: :client, if: :user_company_matches_order?
    
      # Delegating to a policy object
      grant_access action: :destroy, if: :destroy_allowed?
    
      private
    
      def user_company_matches_order?
        current_user.company == Order.find(params[:id]).company
      end
    
      def destroy_allowed?
        InvoicePolicy.new(current_user).destroy?(invoice)
      end
    end
  10. Manage application roles directly

    main

    Use the Rabarber module to manage the global list of available roles in your application.

    • Rabarber.create_role(:name): Creates a new role. Returns true if created, false if it exists.
    • Rabarber.rename_role(:old_name, :new_name, force: false): Renames a role. Use force: true to rename even if the role is currently assigned to users.
    • Rabarber.delete_role(:name, force: false): Deletes a role. Use force: true to delete even if the role is assigned to users.
    • Rabarber.roles: Lists available roles in the global context.
    • Rabarber.all_roles: Lists all available roles grouped by context.
    Rabarber.create_role(:admin)
    Rabarber.rename_role(:admin, :administrator, force: true)
    Rabarber.delete_role(:admin, force: true)
    Rabarber.roles
    Rabarber.all_roles
  11. Manage user roles and queries

    main

    Rabarber augments your user model with methods for assigning, revoking, and querying roles.

    Role Assignment

    • user.assign_roles(:role1, :role2): Assigns roles (creates them if they don't exist).
    • user.assign_roles(:role1, create_new: false): Assigns only existing roles.
    • user.revoke_roles(:role1): Revokes specific roles.
    • user.revoke_all_roles: Revokes all roles from the user.

    Role Queries

    • user.has_role?(:role1): Checks if the user has a specific role.
    • user.roles: Returns the user's roles in the global context.
    • user.all_roles: Returns all user roles grouped by context.
    • User.with_role(:role1): Returns all users possessing the specified role.
    # Assignment
    user.assign_roles(:admin, :manager)
    user.assign_roles(:accountant, create_new: false)
    user.revoke_roles(:admin)
    user.revoke_all_roles
    
    # Queries
    user.has_role?(:accountant)
    user.roles
    user.all_roles
    User.with_role(:admin)
  12. Use conditional rendering with `visible_to` and `hidden_from`

    main

    Rabarber provides view helpers to conditionally render content based on the current user's roles.

    • visible_to(*roles, context: nil): Renders the block if the user has any of the specified roles. You can provide an optional context object to evaluate roles against a specific resource.
    • hidden_from(*roles): Renders the block only if the user does NOT have any of the specified roles.

    Note: These helpers require an authenticated user. If a view can be accessed by unauthenticated visitors, wrap these helpers in your existing authentication check to avoid errors.

    <%# Render content if user is an admin or manager %>
    <%= visible_to(:admin, :manager) do %>
      <div class="admin-panel">
        <!-- Admin/Manager content -->
      </div>
    <% end %>
    
    <%# Hide content from guests %>
    <%= hidden_from(:guest) do %>
      <div class="member-content">
        <!-- Content hidden from guests -->
      </div>
    <% end %>
    
    <%# Render content if user is the owner of a specific project context %>
    <%= visible_to(:owner, context: @project) do %>
      <div class="project-owner-panel">
        <!-- Content visible to project owners -->
      </div>
    <% end %>