activerecord-tenant-level-security 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +3 -3
- data/CHANGELOG.md +28 -0
- data/README.md +62 -1
- data/activerecord-tenant-level-security.gemspec +2 -3
- data/gemfiles/rails_7.1.gemfile +5 -0
- data/gemfiles/rails_7.2.gemfile +5 -0
- data/gemfiles/rails_8.0.gemfile +5 -0
- data/lib/activerecord-tenant-level-security/schema_dumper.rb +25 -6
- data/lib/activerecord-tenant-level-security/schema_statements.rb +17 -15
- data/lib/activerecord-tenant-level-security/tenant_level_security.rb +2 -0
- data/lib/activerecord-tenant-level-security/version.rb +1 -1
- metadata +11 -26
- data/Appraisals +0 -11
- data/gemfiles/rails_6.0.gemfile +0 -7
- data/gemfiles/rails_6.1.gemfile +0 -7
- data/gemfiles/rails_7.0.gemfile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60f9c934a6a5bdad1bfb2ac9ea2d3f961edbb099d2611d8630511ab4454bfeb3
|
4
|
+
data.tar.gz: 9f2f5adc688fc7dbc983f108199c18332903e4ed97271fe7ad09275cb836c36a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d96db12fb43d2df0302802d40297be218b77717f671d661ddc8a7c54d2930cdd230666c62980cb54e0750e70fdd53f86a3de8bcd00ec27affc3439164885105b
|
7
|
+
data.tar.gz: f2e30cfe9a512ec32901a8a2d51c98dbd34c7bbe88018844c7c48d6e3c279b91e30598edaa9ef88c8e105336262179d980cdd9dcd64492615ca2ca93cbee612b
|
data/.circleci/config.yml
CHANGED
@@ -28,6 +28,6 @@ workflows:
|
|
28
28
|
- test:
|
29
29
|
matrix:
|
30
30
|
parameters:
|
31
|
-
ruby: ['ruby:3.
|
32
|
-
rails: ['
|
33
|
-
postgres: ['postgres:
|
31
|
+
ruby: ['ruby:3.2', 'ruby:3.3', 'ruby:3.4']
|
32
|
+
rails: ['rails_7.1', 'rails_7.2', 'rails_8.0']
|
33
|
+
postgres: ['postgres:13.21', 'postgres:14.18', 'postgres:15.13', 'postgres:16.9', 'postgres:17.5']
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
## v0.4.0 (2025-05-23)
|
2
|
+
|
3
|
+
### Breaking Changes
|
4
|
+
|
5
|
+
- [#27](https://github.com/kufu/activerecord-tenant-level-security/pull/27): CI against Ruby 3.4, drop Ruby 3.1, Rails 7.1
|
6
|
+
- Drop support for Ruby 3.1 and Rails 7.1
|
7
|
+
|
8
|
+
## v0.3.0 (2024-08-06)
|
9
|
+
|
10
|
+
### Enhancements
|
11
|
+
|
12
|
+
### Breaking Changes
|
13
|
+
|
14
|
+
- [#18](https://github.com/kufu/activerecord-tenant-level-security/pull/18): CI against Rails 7.1, drop Rails 6.0
|
15
|
+
- Drop support for Rails 6.0
|
16
|
+
|
17
|
+
### Enhancements
|
18
|
+
|
19
|
+
- [#22](https://github.com/kufu/activerecord-tenant-level-security/pull/22): Enable support for custom column names for tenant-level security policies
|
20
|
+
|
21
|
+
### Chores
|
22
|
+
|
23
|
+
- [#19](https://github.com/kufu/activerecord-tenant-level-security/pull/19): Modified the sample code in "Sidekiq Integration"
|
24
|
+
- [#20](https://github.com/kufu/activerecord-tenant-level-security/pull/20): CI against Ruby 3.3 and PostgreSQL 16, drop Ruby 3.0 and PostgreSQL 11
|
25
|
+
- [#21](https://github.com/kufu/activerecord-tenant-level-security/pull/21): Add a guide for using Rails fixtures with RLS in test
|
26
|
+
|
1
27
|
## v0.2.0 (2023-04-14)
|
2
28
|
|
3
29
|
This release includes changes to policies created by `create_policy`. Migrations that have already been performed will not be affected, so it is safe to upgrade as-is, but for performance reasons it is recommended to update existing policies to the new format. See https://github.com/kufu/activerecord-tenant-level-security/pull/16 for details.
|
@@ -28,3 +54,5 @@ This release includes changes to policies created by `create_policy`. Migrations
|
|
28
54
|
## v0.0.1 (2021-11-10)
|
29
55
|
|
30
56
|
Initial release 🥳
|
57
|
+
|
58
|
+
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# activerecord-tenant-level-security
|
2
|
+
|
2
3
|
[](https://circleci.com/gh/kufu/activerecord-tenant-level-security/tree/master)
|
3
4
|
[](https://rubygems.org/gems/activerecord-tenant-level-security)
|
4
5
|
[](https://github.com/kufu/activerecord-tenant-level-security/blob/master/LICENSE.txt)
|
@@ -50,12 +51,17 @@ class CreateEmployee < ActiveRecord::Migration[6.0]
|
|
50
51
|
t.string :name
|
51
52
|
end
|
52
53
|
|
54
|
+
# By default, "tenant_id" is used as a partition key.
|
53
55
|
create_policy :employees
|
56
|
+
|
57
|
+
# You can also use a column other than "tenant_id" by passing the "partition_key" option.
|
58
|
+
# create_policy :employees, partition_key: :company_id
|
59
|
+
# And you can also specify the partition key as a string.
|
54
60
|
end
|
55
61
|
end
|
56
62
|
```
|
57
63
|
|
58
|
-
When
|
64
|
+
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.
|
59
65
|
|
60
66
|
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:
|
61
67
|
|
@@ -119,6 +125,10 @@ Sidekiq.configure_server do |config|
|
|
119
125
|
config.server_middleware do |chain|
|
120
126
|
chain.add TenantLevelSecurity::Sidekiq::Middleware::Server
|
121
127
|
end
|
128
|
+
|
129
|
+
config.client_middleware do |chain|
|
130
|
+
chain.add TenantLevelSecurity::Sidekiq::Middleware::Client
|
131
|
+
end
|
122
132
|
end
|
123
133
|
```
|
124
134
|
|
@@ -130,6 +140,57 @@ Active Record 6+ adds support for [multiple databases](https://guides.rubyonrail
|
|
130
140
|
|
131
141
|
In multiple databases, Active Record creates a connection pool for each connection, but `TenantLevelSecurity.switch` only switches for the current connection.
|
132
142
|
|
143
|
+
## Testing with Rails Fixtures
|
144
|
+
|
145
|
+
When testing a Rails app with multiple tenants, you might have fixtures for different tenants that need loading into
|
146
|
+
your database. However, Row-Level Security (RLS) might block this because it restricts data access. In order to bypass
|
147
|
+
RLS for loading these fixtures, you need to use a special database configuration.
|
148
|
+
|
149
|
+
In your database configuration in `config/database.yml`, add a `bypass_rls` cd config. This must use a superuser
|
150
|
+
database account, which can load fixtures without RLS restrictions. Do not forget to set `database_tasks: false` to
|
151
|
+
prevent Rails from messing with your primary database during setup or teardown tasks.
|
152
|
+
|
153
|
+
```yml
|
154
|
+
# config/database.yml
|
155
|
+
test:
|
156
|
+
primary:
|
157
|
+
<<: *default
|
158
|
+
database: ...
|
159
|
+
username: non_super_user_without_bypass_rls
|
160
|
+
bypass_rls:
|
161
|
+
<<: *default
|
162
|
+
database: ...
|
163
|
+
database_tasks: false # So that the primary db is not re-created or dropped when running rake db:create or db:drop.
|
164
|
+
username: postgres # This user must have the superuser privileges.
|
165
|
+
```
|
166
|
+
|
167
|
+
Then in your test setup in `test/test_helper.rb`, make sure to use the `bypass_rls` configuration for loading fixtures.
|
168
|
+
This involves connecting to the database with superuser privileges before running tests, especially important for
|
169
|
+
parallel tests to ensure each test process works with the correct database instance.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
# test/test_helper.rb
|
173
|
+
|
174
|
+
# Set up the `test_setup` role so we can utilize the `bypass_rls` config:
|
175
|
+
ActiveRecord::Base.connects_to database: { test_setup: :bypass_rls }
|
176
|
+
|
177
|
+
class ActiveSupport::TestCase
|
178
|
+
# When running the tests in parallel, Rails automatically updates the primary db config but not the configs with
|
179
|
+
# the `database_tasks: false` option. We need to ensure that the `bypass_rls` config also points to the same db as
|
180
|
+
# the `primary` config.
|
181
|
+
parallelize_setup do |index|
|
182
|
+
ActiveRecord::Base.configurations.configs_for(env_name: "test", include_hidden: true).each do |config|
|
183
|
+
config._database = "#{config.database}-#{index}" unless config.database.end_with?("-#{index}")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Run setup_fixtures in the test setup to bypass RLS:
|
188
|
+
def setup_fixtures
|
189
|
+
ActiveRecord::Base.connected_to(role: :test_setup) { super }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
133
194
|
## Development
|
134
195
|
|
135
196
|
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.
|
@@ -24,12 +24,11 @@ Gem::Specification.new do |spec|
|
|
24
24
|
end
|
25
25
|
spec.require_paths = ["lib"]
|
26
26
|
|
27
|
-
spec.add_dependency "activerecord", ">=
|
28
|
-
spec.add_dependency "activesupport", ">=
|
27
|
+
spec.add_dependency "activerecord", ">= 7.1"
|
28
|
+
spec.add_dependency "activesupport", ">= 7.1"
|
29
29
|
spec.add_dependency "pg", ">= 1.0"
|
30
30
|
|
31
31
|
spec.add_development_dependency "bundler", ">= 2.0"
|
32
32
|
spec.add_development_dependency "rake", ">= 10.0"
|
33
33
|
spec.add_development_dependency "rspec", ">= 3.0"
|
34
|
-
spec.add_development_dependency "appraisal", "~> 2.4"
|
35
34
|
end
|
@@ -19,7 +19,8 @@ module TenantLevelSecurity
|
|
19
19
|
def policies_in_database
|
20
20
|
query = <<~SQL
|
21
21
|
SELECT
|
22
|
-
tablename
|
22
|
+
tablename,
|
23
|
+
qual
|
23
24
|
FROM
|
24
25
|
pg_policies
|
25
26
|
WHERE
|
@@ -28,23 +29,41 @@ module TenantLevelSecurity
|
|
28
29
|
tablename;
|
29
30
|
SQL
|
30
31
|
results = ActiveRecord::Base.connection.execute(query)
|
31
|
-
|
32
|
+
results.map do |result|
|
33
|
+
table_name = result["tablename"]
|
34
|
+
partition_key = convert_qual_to_partition_key(result["qual"])
|
35
|
+
Policy.new(table_name: table_name, partition_key: partition_key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def convert_qual_to_partition_key(qual)
|
42
|
+
matched = qual.match(/^\((.+?) = /)
|
43
|
+
# This error can occur if the specification of the 'tenant_policy' in PostgreSQL
|
44
|
+
# or the 'create_policy' method changes
|
45
|
+
raise "Failed to parse partition key from 'pg_policies.qual': #{qual}" unless matched
|
32
46
|
|
33
|
-
|
47
|
+
matched[1]
|
34
48
|
end
|
35
49
|
|
36
50
|
class Policy
|
37
|
-
def initialize(table_name)
|
51
|
+
def initialize(table_name:, partition_key:)
|
38
52
|
@table_name = table_name
|
53
|
+
@partition_key = partition_key
|
39
54
|
end
|
40
55
|
|
41
56
|
def to_schema
|
42
|
-
%( create_policy "#{table_name}")
|
57
|
+
schema = %( create_policy "#{table_name}")
|
58
|
+
if partition_key != TenantLevelSecurity::DEFAULT_PARTITION_KEY
|
59
|
+
schema += %(, partition_key: "#{partition_key}")
|
60
|
+
end
|
61
|
+
schema
|
43
62
|
end
|
44
63
|
|
45
64
|
private
|
46
65
|
|
47
|
-
attr_reader :table_name
|
66
|
+
attr_reader :table_name, :partition_key
|
48
67
|
end
|
49
68
|
end
|
50
69
|
end
|
@@ -1,36 +1,38 @@
|
|
1
1
|
module TenantLevelSecurity
|
2
2
|
module SchemaStatements
|
3
|
-
def create_policy(table_name)
|
3
|
+
def create_policy(table_name, partition_key: TenantLevelSecurity::DEFAULT_PARTITION_KEY)
|
4
|
+
quoted_table_name = quote_table_name(table_name)
|
5
|
+
quoted_partition_key = quote_column_name(partition_key)
|
4
6
|
execute <<~SQL
|
5
|
-
ALTER TABLE #{
|
6
|
-
ALTER TABLE #{
|
7
|
+
ALTER TABLE #{quoted_table_name} ENABLE ROW LEVEL SECURITY;
|
8
|
+
ALTER TABLE #{quoted_table_name} FORCE ROW LEVEL SECURITY;
|
7
9
|
SQL
|
8
|
-
tenant_id_data_type = get_tenant_id_data_type(table_name)
|
10
|
+
tenant_id_data_type = get_tenant_id_data_type(table_name, partition_key)
|
9
11
|
execute <<~SQL
|
10
|
-
CREATE POLICY tenant_policy ON #{
|
12
|
+
CREATE POLICY tenant_policy ON #{quoted_table_name}
|
11
13
|
AS PERMISSIVE
|
12
14
|
FOR ALL
|
13
15
|
TO PUBLIC
|
14
|
-
USING (
|
15
|
-
WITH CHECK (
|
16
|
+
USING (#{quoted_partition_key} = NULLIF(current_setting('tenant_level_security.tenant_id'), '')::#{tenant_id_data_type})
|
17
|
+
WITH CHECK (#{quoted_partition_key} = NULLIF(current_setting('tenant_level_security.tenant_id'), '')::#{tenant_id_data_type})
|
16
18
|
SQL
|
17
19
|
end
|
18
20
|
|
19
|
-
def remove_policy(table_name)
|
21
|
+
def remove_policy(table_name, *args)
|
22
|
+
quoted_table_name = quote_table_name(table_name)
|
20
23
|
execute <<~SQL
|
21
|
-
ALTER TABLE #{
|
22
|
-
ALTER TABLE #{
|
24
|
+
ALTER TABLE #{quoted_table_name} NO FORCE ROW LEVEL SECURITY;
|
25
|
+
ALTER TABLE #{quoted_table_name} DISABLE ROW LEVEL SECURITY;
|
23
26
|
SQL
|
24
27
|
execute <<~SQL
|
25
|
-
DROP POLICY tenant_policy ON #{
|
28
|
+
DROP POLICY tenant_policy ON #{quoted_table_name}
|
26
29
|
SQL
|
27
30
|
end
|
28
31
|
|
29
32
|
private
|
30
|
-
def get_tenant_id_data_type(table_name)
|
31
|
-
tenant_id_column = columns(table_name)
|
32
|
-
|
33
|
-
raise "tenant_id column is missing in #{table_name}" if tenant_id_column.nil?
|
33
|
+
def get_tenant_id_data_type(table_name, partition_key)
|
34
|
+
tenant_id_column = columns(table_name).find { |column| column.name == partition_key.to_s }
|
35
|
+
raise "#{partition_key} column is missing in #{table_name}" if tenant_id_column.nil?
|
34
36
|
|
35
37
|
tenant_id_column.sql_type
|
36
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-tenant-level-security
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SmartHR
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '7.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '7.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '7.1'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '7.1'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: pg
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,20 +94,6 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '3.0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: appraisal
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '2.4'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '2.4'
|
111
97
|
description: An Active Record extension for Multitenancy with PostgreSQL Row Level
|
112
98
|
Security
|
113
99
|
email:
|
@@ -119,7 +105,6 @@ files:
|
|
119
105
|
- ".circleci/config.yml"
|
120
106
|
- ".gitignore"
|
121
107
|
- ".rspec"
|
122
|
-
- Appraisals
|
123
108
|
- CHANGELOG.md
|
124
109
|
- CODE_OF_CONDUCT.jp.md
|
125
110
|
- CODE_OF_CONDUCT.md
|
@@ -130,9 +115,9 @@ files:
|
|
130
115
|
- activerecord-tenant-level-security.gemspec
|
131
116
|
- bin/console
|
132
117
|
- bin/setup
|
133
|
-
- gemfiles/
|
134
|
-
- gemfiles/
|
135
|
-
- gemfiles/
|
118
|
+
- gemfiles/rails_7.1.gemfile
|
119
|
+
- gemfiles/rails_7.2.gemfile
|
120
|
+
- gemfiles/rails_8.0.gemfile
|
136
121
|
- lib/activerecord-tenant-level-security.rb
|
137
122
|
- lib/activerecord-tenant-level-security/command_recorder.rb
|
138
123
|
- lib/activerecord-tenant-level-security/schema_dumper.rb
|
@@ -146,7 +131,7 @@ licenses:
|
|
146
131
|
metadata:
|
147
132
|
homepage_uri: https://github.com/kufu/activerecord-tenant-level-security
|
148
133
|
source_code_uri: https://github.com/kufu/activerecord-tenant-level-security
|
149
|
-
changelog_uri: https://github.com/kufu/activerecord-tenant-level-security/blob/v0.
|
134
|
+
changelog_uri: https://github.com/kufu/activerecord-tenant-level-security/blob/v0.4.0/CHANGELOG.md
|
150
135
|
post_install_message:
|
151
136
|
rdoc_options: []
|
152
137
|
require_paths:
|
@@ -162,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
147
|
- !ruby/object:Gem::Version
|
163
148
|
version: '0'
|
164
149
|
requirements: []
|
165
|
-
rubygems_version: 3.
|
150
|
+
rubygems_version: 3.5.16
|
166
151
|
signing_key:
|
167
152
|
specification_version: 4
|
168
153
|
summary: An Active Record extension for Multitenancy with PostgreSQL Row Level Security
|
data/Appraisals
DELETED
data/gemfiles/rails_6.0.gemfile
DELETED
data/gemfiles/rails_6.1.gemfile
DELETED