acts_as_tenant Documentation

repository·master·Indexed 23 days ago

https://github.com/erwinm/acts_as_tenant

A row-level multitenancy gem for Ruby on Rails that implements a shared database strategy. It provides tools to scope models to specific tenants, automatically apply scopes to queries, and manage tenant identification via subdomains, domains, or manual filters. Features include tenant-scoped uniqueness validations, block-level tenant management, and ActiveJob extensions to preserve tenant context in background processes.

Tokens
5.1K
Snippets
15
Records
33
Agent score
81%

What's inside acts_as_tenant

  1. Setup testing for acts_as_tenant

    master

    When testing, you must ensure the tenant is cleaned up after each test. For integration/request tests, use ActsAsTenant::TestTenantMiddleware to handle the test_tenant value, which survives across requests better than current_tenant.

    1. Configure Middleware

    In test.rb:

    require_dependency 'acts_as_tenant/test_tenant_middleware'
    
    Rails.application.configure do
      config.middleware.use ActsAsTenant::TestTenantMiddleware
    end

    2. Configure RSpec/Test Setup

    In spec_helper.rb:

    config.before(:suite) do |example|
      $default_account = Account.create!
    end
    
    config.before(:each) do |example|
      if example.metadata[:type] == :request
        ActsAsTenant.test_tenant = $default_account
      else
        ActsAsTenant.current_tenant = $default_account
      end
    end
    
    config.after(:each) do |example|
      ActsAsTenant.current_tenant = nil
      ActsAsTenant.test_tenant = nil
    end
    # test.rb
    require_dependency 'acts_as_tenant/test_tenant_middleware'
    
    Rails.application.configure do
      config.middleware.use ActsAsTenant::TestTenantMiddleware
    end
  2. How to set the current tenant in a controller

    master

    There are three primary ways to set the current tenant in a Rails application:

    1. By Subdomain

    Use set_current_tenant_by_subdomain to identify the tenant using the last subdomain. It assumes the tenant model (e.g., Account) has a column named :subdomain.

    class ApplicationController < ActionController::Base
      set_current_tenant_by_subdomain(:account, :subdomain)
    end

    To use the first subdomain instead of the last, pass subdomain_lookup: :first.

    2. By Subdomain or Domain

    Use set_current_tenant_by_subdomain_or_domain to check for a subdomain and fallback to a domain if the subdomain is not present.

    class ApplicationController < ActionController::Base
      set_current_tenant_by_subdomain_or_domain(:account, :subdomain, :domain)
    end

    3. Manually via before_action

    If you need custom logic to find the tenant, use set_current_tenant_through_filter and a before_action.

    Note: If setting the tenant in a specific controller (other than ApplicationController), the declaration must be at the TOP of the file.

    class MembersController < ActionController::Base
      set_current_tenant_through_filter
      before_action :set_tenant
      before_action :set_member, only: [:show, :edit, :update, :destroy]
    
      def set_tenant
        set_current_tenant(current_user.account)
      end
    end
  3. Implement multi-tenancy in a model using acts_as_tenant

    master

    To enable multi-tenancy for a specific model using a shared database strategy, you must first ensure the model has a foreign key pointing to the tenant model (e.g., account_id for an Account tenant).

    Then, use the acts_as_tenant method in your model to:

    1. Scope all searches to the current tenant.
    2. Add validation for associations to ensure they belong to the current tenant.
    3. Implement safeguards against form-injection attacks.

    You can also use validates_uniqueness_to_tenant to ensure uniqueness of a field within the scope of the current tenant. This method accepts the same options as the standard Rails validates_uniqueness_of method.

    class Project < ActiveRecord::Base
      acts_as_tenant(:account)
      validates_uniqueness_to_tenant :name
    end
  4. Preserve tenant context in ActiveJob

    master
    The ActsAsTenant::ActiveJobExtensions module allows background jobs to automatically preserve and restore the tenant context. When included in an ActiveJob class, the current tenant is serialized into the job data using its GlobalID. Upon job execution, the tenant is automatically deserialized and set as the ActsAsTenant.current_tenant before the job runs. This ensures that any tenant-scoped queries performed within the job are correctly scoped to the tenant that triggered the job.
  5. Configure require_tenant and job_scope

    master

    You can configure acts_as_tenant via an initializer (e.g., config/initializers/acts_as_tenant.rb).

    Configuration Options

    • config.require_tenant: When set to true, ActsAsTenant::NoTenant is raised if a query is made without a tenant set. This can also be a lambda that evaluates at runtime.
    • config.job_scope: A lambda used to customize the query for loading the tenant in background jobs.
    ActsAsTenant.configure do |config|
      config.require_tenant = true
      config.job_scope = ->{ all }
    end

    Conditional requirement via Lambda

    You can use a lambda for require_tenant to exclude certain paths (like admin routes) from requiring a tenant:

    ActsAsTenant.configure do |config|
      config.require_tenant = lambda do
        if $request_env.present?
          return false if $request_env["REQUEST_PATH"].start_with?("/admin/")
        end
        true
      end
    end
    ActsAsTenant.configure do |config|
      config.require_tenant = false
      config.job_scope = ->{ all }
    end
  6. Configure Sidekiq middleware for tenant propagation

    master

    To ensure the current tenant context is preserved when pushing jobs to Sidekiq and automatically restored when the worker executes, you must add the ActsAsTenant::Sidekiq middleware to both your Sidekiq client and server configurations.

    Client Configuration

    Add ActsAsTenant::Sidekiq::Client to your client middleware chain. This captures the current tenant's class and ID and injects them into the Sidekiq job message.

    Server Configuration

    Add ActsAsTenant::Sidekiq::Server to your server middleware chain. This extracts the tenant information from the job message and wraps the job execution in an ActsAsTenant.with_tenant block.

    Note: The middleware automatically handles different Sidekiq versions and attempts to insert itself before RetryJobs or Batch::Server to ensure correct context during retries or batches.

    Sidekiq.configure_client do |config|
      config.client_middleware do |chain|
        chain.add ActsAsTenant::Sidekiq::Client
      end
    end
    
    Sidekiq.configure_server do |config|
      config.client_middleware do |chain|
        chain.add ActsAsTenant::Sidekiq::Client
      end
      config.server_middleware do |chain|
        if defined?(Sidekiq::Middleware::Server::RetryJobs)
          chain.insert_before Sidekiq::Middleware::Server::RetryJobs, ActsAsTenant::Sidekiq::Server
        elsif defined?(Sidekiq::Batch::Server)
          chain.insert_before Sidekiq::Batch::Server, ActsAsTenant::Sidekiq::Server
        else
          chain.add ActsAsTenant::Sidekiq::Server
        end
      end
    end
  7. Important caveats for acts_as_tenant

    master

    When using acts_as_tenant, keep the following in mind:

    • No Tenant, No Scope: Model scoping only works if a current_tenant has been explicitly set using one of the provided methods. If no tenant is set, no scope will be applied to searches, which could lead to data exposure.
    • Uniqueness Validation: To validate uniqueness within a tenant scope, you must use validates_uniqueness_to_tenant instead of the standard Rails validates_uniqueness_of.
    • Declaration Order: It is recommended to place the acts_as_tenant declaration in your model after any other default_scope declarations.
  8. Set the current tenant for a block

    master

    Use ActsAsTenant.with_tenant(tenant) to wrap a block of code where the current tenant is explicitly set. This is thread-safe and is particularly useful for background processes (like Sidekiq or ActiveJob workers) where you need to ensure all database queries within that block are scoped to a specific tenant.

    ActsAsTenant.with_tenant(current_account) do
      # Current tenant is set for all code in this block
    end
  9. Set the current tenant by subdomain

    master

    You can automatically identify the current tenant based on the request's subdomain. In your ApplicationController, use set_current_tenant_by_subdomain with the following arguments:

    1. The tenant model name (e.g., :account).
    2. The column name on that model used for the lookup (e.g., :subdomain).

    Example: If a user visits account1.myappdomain.com, the gem will look up an Account where the subdomain column matches account1.

    class ApplicationController < ActionController::Base
      set_current_tenant_by_subdomain(:account, :subdomain)
    end
  10. Scope models to a tenant using acts_as_tenant

    master

    To enable row-level multitenancy on a model, you must first ensure the table has a foreign key column linking it to the tenant (e.g., account_id). Then, call acts_as_tenant in the model.

    Important: acts_as_tenant automatically includes the belongs_to relationship. Do not manually add belongs_to :account if you are using acts_as_tenant(:account).

    Basic Usage

    class Project < ActiveRecord::Base
      acts_as_tenant(:account)
    end

    Custom Foreign and Primary Keys

    If your schema uses non-standard naming, specify them explicitly:

    acts_as_tenant(:account, :foreign_key => 'accountID', :primary_key => 'primaryID')

    Scoping HABTM relationships

    For models in a Has and Belongs to Many relationship, use the through option:

    class User < ActiveRecord::Base
      acts_as_tenant :organisation, through: :organisations_users
    end

    Passing belongs_to options

    You can pass standard belongs_to options directly to acts_as_tenant:

    acts_as_tenant(:account, counter_cache: true, optional: true)
  11. Disable tenant checking or allow tenant updating for a block

    master

    You can temporarily bypass tenant restrictions using block-level methods:

    Disable tenant checking

    Use ActsAsTenant.without_tenant to run code without any tenant scoping. This is useful for admin panels or internal dashboards where require_tenant is enabled globally.

    ActsAsTenant.without_tenant do
      # Tenant checking is disabled for all code in this block
    end

    Allow tenant updating

    Use ActsAsTenant.with_mutable_tenant to allow changing the tenant of a model. This is useful for admin screens where a user might need to reassign a record to a different tenant.

    ActsAsTenant.with_mutable_tenant do
      # Tenant updating is enabled for all code in this block
    end