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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e847b09c2072b1d43800f05548532cbea63ee65700b80a50f48ce212ce1128c4
4
- data.tar.gz: '0884eb072cb058b099b449cbc940a373abb247f6c01a396df64486941b091167'
3
+ metadata.gz: 60f9c934a6a5bdad1bfb2ac9ea2d3f961edbb099d2611d8630511ab4454bfeb3
4
+ data.tar.gz: 9f2f5adc688fc7dbc983f108199c18332903e4ed97271fe7ad09275cb836c36a
5
5
  SHA512:
6
- metadata.gz: a2761438baef9b4e0c3ea159121f28da51f537bb5c70bcb8d4c81b6d22283d2d0bab9eea784ba84d0b68635d68954cc27eb3bc36f56f6c92508914998e548422
7
- data.tar.gz: 33e9fbbc489d73f12418e7c53483989b404417051aff14a28ce0aa77598aef9547d89a08a6476e908cdb26972963b876c37a15d79e145f14a6c514f55dfc5310
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.0', 'ruby:3.1', 'ruby:3.2']
32
- rails: ['rails_6.0', 'rails_6.1', 'rails_7.0']
33
- postgres: ['postgres:11.19', 'postgres:12.14', 'postgres:13.10', 'postgres:14.7', 'postgres:15.2']
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
  [![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)
3
4
  [![gem-version](https://img.shields.io/gem/v/activerecord-tenant-level-security.svg)](https://rubygems.org/gems/activerecord-tenant-level-security)
4
5
  [![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)
@@ -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 testing, be aware of the database user you are connecting to. The default user `postgres` has the `BYPASSRLS` attribute and therefore bypass the RLS. You must create a user who does not have these privileges in order for your application to connect.
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", ">= 6.0"
28
- spec.add_dependency "activesupport", ">= 6.0"
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
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 7.1.1"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 7.2.2"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 8.0.0"
4
+
5
+ gemspec path: "../"
@@ -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
- table_names = results.map { |x| x["tablename"] }
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
- table_names.map { |t| Policy.new(t) }
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 #{table_name} ENABLE ROW LEVEL SECURITY;
6
- ALTER TABLE #{table_name} FORCE ROW LEVEL SECURITY;
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 #{table_name}
12
+ CREATE POLICY tenant_policy ON #{quoted_table_name}
11
13
  AS PERMISSIVE
12
14
  FOR ALL
13
15
  TO PUBLIC
14
- USING (tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id'), '')::#{tenant_id_data_type})
15
- WITH CHECK (tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id'), '')::#{tenant_id_data_type})
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 #{table_name} NO FORCE ROW LEVEL SECURITY;
22
- ALTER TABLE #{table_name} DISABLE ROW LEVEL SECURITY;
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 #{table_name}
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
- .find{|column| column.name == 'tenant_id'}
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
@@ -1,4 +1,6 @@
1
1
  module TenantLevelSecurity
2
+ DEFAULT_PARTITION_KEY = 'tenant_id'.freeze
3
+
2
4
  class << self
3
5
  # The current_tenant_id sets the default tenant from the outside.
4
6
  # Be sure to register in advance as `TenantLevelSecurity.current_tenant_id { id }` with initializers.
@@ -1,3 +1,3 @@
1
1
  module TenantLevelSecurity
2
- VERSION = '0.2.0'
2
+ VERSION = '0.4.0'
3
3
  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.2.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: 2023-04-14 00:00:00.000000000 Z
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: '6.0'
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: '6.0'
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: '6.0'
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: '6.0'
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/rails_6.0.gemfile
134
- - gemfiles/rails_6.1.gemfile
135
- - gemfiles/rails_7.0.gemfile
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.2.0/CHANGELOG.md
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.2.33
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
@@ -1,11 +0,0 @@
1
- appraise "rails-6.0" do
2
- gem "rails", "~> 6.0.4"
3
- end
4
-
5
- appraise "rails-6.1" do
6
- gem "rails", "~> 6.1.4"
7
- end
8
-
9
- appraise "rails-7.0" do
10
- gem "rails", "~> 7.0.2"
11
- end
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~> 6.0.4"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~> 6.1.4"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~> 7.0.2"
6
-
7
- gemspec path: "../"