Skip to content

An Active Record extension for Multitenancy with PostgreSQL Row Level Security

License

Notifications You must be signed in to change notification settings

kufu/activerecord-tenant-level-security

Repository files navigation

activerecord-tenant-level-security

CircleCI gem-version License

An Active Record extension for Multitenancy with PostgreSQL Row Level Security.

Installation

Add this line to your application's Gemfile:

gem 'activerecord-tenant-level-security'

And then execute:

$ bundle

Or install it yourself as:

$ gem install activerecord-tenant-level-security

Usage

The activerecord-tenant-level-security provides an API for applying PostgreSQL Row Level Security (RLS) as follows:

Employee.pluck(:name) # => ["Jane", "Tom"]

# Switch a connection as non-superuser
ActiveRecord::Base.establish_connection(app_user_config)

TenantLevelSecurity.with(tenant1.id) do
  Employee.pluck(:name) # => ["Jane"]
end

TenantLevelSecurity.with(tenant2.id) do
  Employee.pluck(:name) # => ["Tom"]
end

To enable RLS, you need to create a policy on the table. You can use create_policy in a migration file:

class CreateEmployee < ActiveRecord::Migration[6.0]
  def change
    create_table :employees do |t|
      t.integer :tenant_id
      t.string :name
    end

    # By default, "tenant_id" is used as a partition key.
    create_policy :employees

    # You can also use a column other than "tenant_id" by passing the "partition_key" option.
    # create_policy :employees, partition_key: :company_id
    # And you can also specify the partition key as a string.
  end
end

When experimenting, be aware of the database user you are trying to connect with. The default user postgres has the BYPASSRLS attribute and therefore bypasses the RLS. You must create a user who does not have these privileges in order for your application to connect.

If you want to use this gem, you first need to register a callback which gets the current tenant. This callback is invoked when checking out a new connection from a connection pool. Create an initializer and tell how it should resolve the current tenant like the following:

TenantLevelSecurity.current_tenant_id { RequestStore.store[:current_tenant_id] }

The above is an example of getting the current tenant stored using RequestStore. You are responsible for storing the current tenant, such as at the beginning of the request.

We strongly recommend using the activerecord-multi-tenant for this config. activerecord-multi-tenant provides multi-tenant data isolation at the application layer by rewriting queries. On the other hand, this gem provides the isolation at the database layer by RLS. Multi-layered security is important.

TenantLevelSecurity.current_tenant_id { MultiTenant.current_tenant_id }

Do not query the database in this callback. As mentioned above, this callback is invoked at checking out a connection, so it may be called recursively.

How it works

The policy created by the activerecord-tenant-level-security is:

CREATE POLICY tenant_policy ON employees
  AS PERMISSIVE
  FOR ALL
  TO PUBLIC
  USING (tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id'), '')::integer)
  WITH CHECK (tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id'), '')::integer)

In the table in which the policy is created, only the rows that match the current setting of tenant_level_security.tenant_id can be referenced. This value is set by TenantLevelSecurity.with etc.

# Set default tenant to "tenant2"
TenantLevelSecurity.current_tenant_id { tenant2.id }

TenantLevelSecurity.with(tenant1.id) do # => SET tenant_level_security.tenant_id = '1'
  Employee.pluck(:name)
end # => SET tenant_level_security.tenant_id TO DEFAULT

Thread.new {
  # Checkout a new connection in a thread
  Employee.connection # => SET tenant_level_security.tenant_id = '2'
}.join

In this way, sessions are used to determine the current tenant. Therefore, avoid using it with transaction pooling like PgBouncer.

Sidekiq Integration

If you are using Sidekiq, The activerecord-tenant-level-security will provide middlewares:

Sidekiq.configure_client do |config|
  config.client_middleware do |chain|
    chain.add TenantLevelSecurity::Sidekiq::Middleware::Client
  end
end

Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    chain.add TenantLevelSecurity::Sidekiq::Middleware::Server
  end

  config.client_middleware do |chain|
    chain.add TenantLevelSecurity::Sidekiq::Middleware::Client
  end
end

The middleware propagates the current tenant to the job through the session. This allows RLS to be enabled even within workers.

Multiple Databases

Active Record 6+ adds support for multiple databases. Note that when using multiple databases with this gem, you need to explicitly switch when connecting other databases.

In multiple databases, Active Record creates a connection pool for each connection, but TenantLevelSecurity.switch only switches for the current connection.

Testing with Rails Fixtures

When testing a Rails app with multiple tenants, you might have fixtures for different tenants that need loading into your database. However, Row-Level Security (RLS) might block this because it restricts data access. In order to bypass RLS for loading these fixtures, you need to use a special database configuration.

In your database configuration in config/database.yml, add a bypass_rls cd config. This must use a superuser database account, which can load fixtures without RLS restrictions. Do not forget to set database_tasks: false to prevent Rails from messing with your primary database during setup or teardown tasks.

# config/database.yml
test:
  primary:
    <<: *default
    database: ...
    username: non_super_user_without_bypass_rls
  bypass_rls:
    <<: *default
    database: ...
    database_tasks: false # So that the primary db is not re-created or dropped when running rake db:create or db:drop.
    username: postgres    # This user must have the superuser privileges.

Then in your test setup in test/test_helper.rb, make sure to use the bypass_rls configuration for loading fixtures. This involves connecting to the database with superuser privileges before running tests, especially important for parallel tests to ensure each test process works with the correct database instance.

# test/test_helper.rb

# Set up the `test_setup` role so we can utilize the `bypass_rls` config:
ActiveRecord::Base.connects_to database: { test_setup: :bypass_rls }

class ActiveSupport::TestCase
  # When running the tests in parallel, Rails automatically updates the primary db config but not the configs with
  # the `database_tasks: false` option. We need to ensure that the `bypass_rls` config also points to the same db as
  # the `primary` config.
  parallelize_setup do |index|
    ActiveRecord::Base.configurations.configs_for(env_name: "test", include_hidden: true).each do |config|
      config._database = "#{config.database}-#{index}" unless config.database.end_with?("-#{index}")
    end
  end

  # Run setup_fixtures in the test setup to bypass RLS:
  def setup_fixtures
    ActiveRecord::Base.connected_to(role: :test_setup) { super }
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kufu/activerecord-tenant-level-security. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the activerecord-tenant-level-security project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

An Active Record extension for Multitenancy with PostgreSQL Row Level Security

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published