activerecord-tenant-level-security 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e847b09c2072b1d43800f05548532cbea63ee65700b80a50f48ce212ce1128c4
4
- data.tar.gz: '0884eb072cb058b099b449cbc940a373abb247f6c01a396df64486941b091167'
3
+ metadata.gz: 4670da50cc5b9667dcfe37e6d0ec256514e37045ab2108b85a2d518599e03c88
4
+ data.tar.gz: c1415e5e3580ea401e0d6fa4a26ec7f7ed3be9fb94f1cc5313c204f9a101621f
5
5
  SHA512:
6
- metadata.gz: a2761438baef9b4e0c3ea159121f28da51f537bb5c70bcb8d4c81b6d22283d2d0bab9eea784ba84d0b68635d68954cc27eb3bc36f56f6c92508914998e548422
7
- data.tar.gz: 33e9fbbc489d73f12418e7c53483989b404417051aff14a28ce0aa77598aef9547d89a08a6476e908cdb26972963b876c37a15d79e145f14a6c514f55dfc5310
6
+ metadata.gz: f9274229b916d770bfc716301a5cd374c7f250c9839a44b95881f938e4d697e6e4791dccb80f520e890ad61ab0a74164ce7bda12a879b8a1b4b3aeab6556ca87
7
+ data.tar.gz: aafa04e789160c58d8f13aca7f8ff977cccced85f08c2fe240dda17c5b3dbb0c37d110ec6ba8d160d306339efd4d938321173007fdfdbd60e1ebb0b9a5887efb
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.1', 'ruby:3.2', 'ruby:3.3']
32
+ rails: ['rails_6.1', 'rails_7.0', 'rails_7.1']
33
+ postgres: ['postgres:12.17', 'postgres:13.13', 'postgres:14.10', 'postgres:15.5', 'postgres:16.1']
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## v0.3.0 (2024-08-06)
2
+
3
+ ### Enhancements
4
+
5
+ ### Breaking Changes
6
+
7
+ - [#18](https://github.com/kufu/activerecord-tenant-level-security/pull/18): CI against Rails 7.1, drop Rails 6.0
8
+ - Drop support for Rails 6.0
9
+
10
+ ### Enhancements
11
+
12
+ - [#22](https://github.com/kufu/activerecord-tenant-level-security/pull/22): Enable support for custom column names for tenant-level security policies
13
+
14
+ ### Chores
15
+
16
+ - [#19](https://github.com/kufu/activerecord-tenant-level-security/pull/19): Modified the sample code in "Sidekiq Integration"
17
+ - [#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
18
+ - [#21](https://github.com/kufu/activerecord-tenant-level-security/pull/21): Add a guide for using Rails fixtures with RLS in test
19
+
1
20
  ## v0.2.0 (2023-04-14)
2
21
 
3
22
  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.
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", ">= 6.1"
28
+ spec.add_dependency "activesupport", ">= 6.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
@@ -1,5 +1,3 @@
1
- # This file was generated by Appraisal
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
3
  gem "rails", "~> 6.1.4"
@@ -1,5 +1,3 @@
1
- # This file was generated by Appraisal
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
3
  gem "rails", "~> 7.0.2"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 7.1.1"
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.3.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.3.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: 2024-08-06 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: '6.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: '6.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: '6.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: '6.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
118
  - gemfiles/rails_6.1.gemfile
135
119
  - gemfiles/rails_7.0.gemfile
120
+ - gemfiles/rails_7.1.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.3.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.4.19
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: "../"