Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/kufu/activerecord-tenant-level-security

An Active Record extension for Multitenancy with PostgreSQL Row Level Security
https://github.com/kufu/activerecord-tenant-level-security

Last synced: about 6 hours ago
JSON representation

An Active Record extension for Multitenancy with PostgreSQL Row Level Security

Awesome Lists containing this project

README

        

# activerecord-tenant-level-security

[![CircleCI](https://circleci.com/gh/kufu/activerecord-tenant-level-security/tree/master.svg?style=svg)](https://circleci.com/gh/kufu/activerecord-tenant-level-security/tree/master)
[![gem-version](https://img.shields.io/gem/v/activerecord-tenant-level-security.svg)](https://rubygems.org/gems/activerecord-tenant-level-security)
[![License](https://img.shields.io/github/license/kufu/activerecord-tenant-level-security.svg?color=blue)](https://github.com/kufu/activerecord-tenant-level-security/blob/master/LICENSE.txt)

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

## Installation

Add this line to your application's Gemfile:

```ruby
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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) (RLS) as follows:

```ruby
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:

```ruby
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:

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

The above is an example of getting the current tenant stored using [RequestStore](https://github.com/steveklabnik/request_store). You are responsible for storing the current tenant, such as at the beginning of the request.

We strongly recommend using the [activerecord-multi-tenant](https://github.com/citusdata/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.

```ruby
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:

```sql
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.

```ruby
# 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](https://sidekiq.org/), The activerecord-tenant-level-security will provide [middlewares](https://github.com/mperham/sidekiq/wiki/Middleware):

```ruby
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](https://guides.rubyonrails.org/active_record_multiple_databases.html). 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.

```yml
# 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.

```ruby
# 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](https://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](http://contributor-covenant.org) code of conduct.

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## 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](https://github.com/kufu/activerecord-tenant-level-security/blob/master/CODE_OF_CONDUCT.md).