pg_rls 0.1.10 → 0.2.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: 492256f8c775835d6ed888cc64e93bff4c0a3f9583eb9c459e1ed9a5520d7fab
4
- data.tar.gz: f05611afd74ad000316df9622663de94d8feda84538de06cf5c548e749a60302
3
+ metadata.gz: e81a178c9c3c7d4a42c2dff85faa720c8340a2c3373eab5c38edb50e862bdfb9
4
+ data.tar.gz: 8a8d78bd15baca991c1f50eb3adc956f5b467a640c6b6b629d8260e19b608ea4
5
5
  SHA512:
6
- metadata.gz: cabd8e2479dbf5432a26b34680932a6ff2a4b1d2f6cbd6c1c369d8f4b954a69baa4cfefe8a81f86995b07f2a966a8129d664adf9115de23c0dbb3a91623cc52a
7
- data.tar.gz: 4f66a3686927071d5aa5f66374a63450cda26c9be6ebc86d1cf085fc373f90033e2e7d2fe50f4b1945452088122df3884ca5cd97746c43905615858cccf5fed6
6
+ metadata.gz: c461c7b3263d1e936a04a0693509cef55d4a699169a8d4addd7e5818b02ef608cf1cadc39aee287b5b3d6cc11705e3bf10eefeafbc8d62e3db413b0010ebcb36
7
+ data.tar.gz: f216cec76a8d3a5703abd0913decc36ef1344d6327331555e6e97168c5b4cf7aeda46254cf04daaadcca159adf56a3747c54c95be8d16b1f32d5de8dc9e5bd31
data/Gemfile CHANGED
@@ -17,4 +17,5 @@ gem 'rubocop-performance'
17
17
  gem 'rubocop-rails'
18
18
  gem 'rubocop-rake'
19
19
  gem 'rubocop-rspec'
20
+ gem 'sidekiq'
20
21
  gem 'solargraph'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg_rls (0.1.6)
4
+ pg_rls (0.1.10)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -109,6 +109,7 @@ GEM
109
109
  kramdown-parser-gfm (1.1.0)
110
110
  kramdown (~> 2.0)
111
111
  language_server-protocol (3.17.0.3)
112
+ logger (1.6.0)
112
113
  loofah (2.21.4)
113
114
  crass (~> 1.0.2)
114
115
  nokogiri (>= 1.12.0)
@@ -184,6 +185,8 @@ GEM
184
185
  rbs (2.8.4)
185
186
  rdoc (6.5.0)
186
187
  psych (>= 4.0.0)
188
+ redis-client (0.22.2)
189
+ connection_pool
187
190
  regexp_parser (2.8.2)
188
191
  reline (0.3.9)
189
192
  io-console (~> 0.5)
@@ -236,6 +239,12 @@ GEM
236
239
  rubocop-factory_bot (~> 2.22)
237
240
  ruby-progressbar (1.13.0)
238
241
  ruby2_keywords (0.0.5)
242
+ sidekiq (7.3.1)
243
+ concurrent-ruby (< 2)
244
+ connection_pool (>= 2.3.0)
245
+ logger
246
+ rack (>= 2.2.4)
247
+ redis-client (>= 0.22.2)
239
248
  solargraph (0.49.0)
240
249
  backport (~> 1.2)
241
250
  benchmark
@@ -268,6 +277,7 @@ GEM
268
277
 
269
278
  PLATFORMS
270
279
  arm64-darwin-22
280
+ arm64-darwin-23
271
281
  x86_64-linux
272
282
 
273
283
  DEPENDENCIES
@@ -280,6 +290,7 @@ DEPENDENCIES
280
290
  rubocop-rails
281
291
  rubocop-rake
282
292
  rubocop-rspec
293
+ sidekiq
283
294
  solargraph
284
295
 
285
296
  RUBY VERSION
@@ -2,9 +2,6 @@
2
2
 
3
3
  <% module_namespacing do -%>
4
4
  class <%= PgRls.class_name.camelize %> < <%= parent_class_name.classify %>
5
- def self.current
6
- PgRls::Tenant.fetch
7
- end
8
5
  <% attributes.select(&:reference?).each do |attribute| -%>
9
6
  belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %>
10
7
  <% end -%>
@@ -48,7 +48,6 @@ module PgRls
48
48
  def copy_initializer
49
49
  raise MissingORMError, orm_error_message unless options[:orm]
50
50
 
51
- inject_include_to_application
52
51
  inject_include_to_application_controller
53
52
  template 'pg_rls.rb.tt', 'config/initializers/pg_rls.rb'
54
53
  end
@@ -61,14 +60,6 @@ module PgRls
61
60
  end
62
61
  end
63
62
 
64
- def inject_include_to_application
65
- return if aplication_already_included?
66
-
67
- gsub_file(APPLICATION_PATH, /(#{Regexp.escape(APPLICATION_LINE)})/mio) do |match|
68
- "#{match}\n config.active_record.schema_format = :sql\n"
69
- end
70
- end
71
-
72
63
  def inject_include_to_application_controller
73
64
  return if aplication_controller_already_included?
74
65
 
@@ -81,10 +72,6 @@ module PgRls
81
72
  File.readlines(APPLICATION_CONTROLLER_PATH).grep(/include PgRls::MultiTenancy/).any?
82
73
  end
83
74
 
84
- def aplication_already_included?
85
- File.readlines(APPLICATION_PATH).grep(/config.active_record.schema_format = :sql/).any?
86
- end
87
-
88
75
  def environment_already_included?
89
76
  File.readlines(ENVIRONMENT_PATH).grep(%r{require_relative 'initializers/pg_rls'}).any?
90
77
  end
@@ -9,6 +9,17 @@ PgRls.setup do |config|
9
9
  config.class_name = :<%= PgRls.class_name %>
10
10
  config.table_name = :<%= PgRls.table_name %>
11
11
  config.search_methods = <%= PgRls.search_methods %>
12
+ # If you are using `solid_queue`, `solid_cache`, or `solid_cable` with a sharding configuration,
13
+ # we recommend excluding these shards from Row-Level Security (RLS) to avoid the need to reset
14
+ # RLS on each shard.
15
+ #
16
+ # By default, RLS will be enabled for all shards.
17
+ # You can specify which shards to exclude from RLS using the `config.excluded_shards` option:
18
+ #
19
+ # config.excluded_shards = []
20
+ #
21
+ # Note: While it's technically possible to leave `solid_cache` and `solid_cable` under RLS,
22
+ # it is generally unnecessary and may introduce complexity without added benefit.
12
23
 
13
24
  ##
14
25
  ## Uncomment this lines if you have a custome user per environment
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Current Context
5
+ module Current
6
+ class Context < ActiveSupport::CurrentAttributes
7
+ attribute :tenant
8
+ end
9
+ end
10
+ end
@@ -5,7 +5,7 @@ module PgRls
5
5
  class RakeOnlyError < StandardError
6
6
  def initialize(msg = nil)
7
7
  msg ||= 'This method can only be executed through rake tasks'
8
- super(msg)
8
+ super
9
9
  end
10
10
  end
11
11
  end
@@ -5,9 +5,8 @@ module PgRls
5
5
  # Raise Tenant Not found and ensure that the tenant is resetted
6
6
  class TenantNotFound < StandardError
7
7
  def initialize(msg = nil)
8
- PgRls::Tenant.reset_rls!
9
8
  msg ||= "Tenant Doesn't exist"
10
- super(msg)
9
+ super
11
10
  end
12
11
  end
13
12
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module PgRls
6
+ class Logger
7
+ def initialize
8
+ @logger = ::Logger.new($stdout) # You can output to a file if needed
9
+ @logger.level = ::Logger::DEBUG
10
+ end
11
+
12
+ def log(message, level: :info)
13
+ case level
14
+ when :debug
15
+ @logger.debug(message)
16
+ when :info
17
+ @logger.info(message)
18
+ when :warn
19
+ @logger.warn(message)
20
+ when :error
21
+ @logger.error(message)
22
+ else
23
+ @logger.info(message)
24
+ end
25
+ end
26
+
27
+ def deprecation_warning(message)
28
+ log("[DEPRECATION WARNING]: #{message}", level: :warn)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module Schema
5
+ module Dumper
6
+ def table(table, stream)
7
+ temp_stream = StringIO.new
8
+ super(table, temp_stream)
9
+ temp_stream_string = temp_stream.string
10
+ if rls_tenant_table?(table)
11
+ temp_stream_string.gsub!('create_table', 'create_rls_tenant_table')
12
+ elsif rls_table?(table)
13
+ temp_stream_string.gsub!('create_table', 'create_rls_table')
14
+ end
15
+
16
+ stream.print(temp_stream_string)
17
+ end
18
+
19
+ private
20
+
21
+ def rls_table?(table_name)
22
+ # Logic to determine if the table uses RLS
23
+ # You can check if the table has RLS policies or use a naming convention
24
+ @connection.execute(<<-SQL.squish).any?
25
+ SELECT 1 FROM pg_policies WHERE tablename = #{ActiveRecord::Base.connection.quote(table_name)};
26
+ SQL
27
+ end
28
+
29
+ def rls_tenant_table?(table_name)
30
+ # Logic to determine if the table is a tenant table
31
+ # You can check if the table has a specific column or use a naming convention
32
+ PgRls.table_name.to_s == table_name
33
+ end
34
+ end
35
+ end
36
+ end
@@ -15,7 +15,6 @@ module PgRls
15
15
 
16
16
  CREATE USER #{name} WITH PASSWORD '#{password}';
17
17
  END IF;
18
- GRANT ALL PRIVILEGES ON TABLE schema_migrations TO #{name};
19
18
  GRANT USAGE ON SCHEMA #{schema} TO #{name};
20
19
  ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
21
20
  GRANT USAGE, SELECT
data/lib/pg_rls/tenant.rb CHANGED
@@ -25,7 +25,7 @@ module PgRls
25
25
 
26
26
  yield(tenant).presence if block_given?
27
27
  ensure
28
- reset_rls! unless PgRls.test_inline_tenant == true
28
+ reset_rls! unless PgRls.test_inline_tenant == true || PgRls::Current::Context.tenant.blank?
29
29
  end
30
30
  end
31
31
 
@@ -36,47 +36,92 @@ module PgRls
36
36
  end
37
37
 
38
38
  def fetch!
39
- PgRls.main_model.find_by!(
40
- tenant_id: PgRls.connection_class.connection.execute(
41
- "SELECT current_setting('rls.tenant_id')"
42
- ).getvalue(0, 0)
43
- )
39
+ PgRls::Current::Context.tenant ||= PgRls.main_model.connection_pool.with_connection do |connection|
40
+ tenant_id = get_tenant_id(connection)
41
+ if tenant_id.present?
42
+ PgRls.main_model.find_by!(
43
+ tenant_id:
44
+ )
45
+ end
46
+ end
47
+ end
48
+
49
+ # rubocop:disable Lint/RescueStandardError
50
+ # rubocop:disable Lint/UselessAssignment
51
+ def get_tenant_id(connection)
52
+ connection.execute("SELECT current_setting('rls.tenant_id')").getvalue(0, 0)
53
+ rescue => e
54
+ nil
44
55
  end
56
+ # rubocop:enable Lint/RescueStandardError
57
+ # rubocop:enable Lint/UselessAssignment
45
58
 
46
59
  def reset_rls!
47
60
  PgRls.execute_rls_in_shards do |connection_class|
48
- connection_class.transaction do
49
- connection_class.connection.execute('RESET rls.tenant_id')
61
+ connection_class.connection_pool.with_connection do |connection|
62
+ connection.transaction do
63
+ connection.execute('RESET rls.tenant_id')
64
+ end
50
65
  end
51
66
  end
52
67
 
68
+ PgRls::Current::Context.clear_all
53
69
  nil
54
70
  end
55
71
 
56
- def set_rls!(tenant_id)
72
+ def set_rls!(tenant)
73
+ tenant_id = tenant.tenant_id
57
74
  PgRls.execute_rls_in_shards do |connection_class|
58
- connection_class.transaction do
59
- connection_class.connection.execute(format('SET rls.tenant_id = %s',
60
- connection_class.connection.quote(tenant_id)))
75
+ connection_class.connection_pool.with_connection do |connection|
76
+ connection.transaction do
77
+ connection.execute(format('SET rls.tenant_id = %s',
78
+ connection.quote(tenant_id)))
79
+ end
61
80
  end
62
81
  end
82
+ PgRls::Current::Context.clear_all
83
+ PgRls::Current::Context.tenant = tenant
84
+ end
85
+
86
+ def on_find_each(ids: [], scope: nil, &)
87
+ raise 'Invalid Scope' if scope.present? && PgRls.main_model != scope.klass
88
+
89
+ result = []
90
+
91
+ query = build_on_each_query(ids, scope)
92
+
93
+ query.find_each do |tenant|
94
+ result << { tenant_id: tenant.id, result: with_tenant!(tenant, &) }
95
+ end
96
+
97
+ result
63
98
  end
64
99
 
65
100
  private
66
101
 
102
+ def build_on_each_query(ids, scope)
103
+ return PgRls.main_model.all if ids.empty? && scope.blank?
104
+
105
+ return PgRls.main_model.where(id: ids) if scope.blank?
106
+
107
+ return scope.where(id: ids) if ids.present?
108
+
109
+ scope
110
+ end
111
+
67
112
  def switch_tenant!(resource)
68
113
  tenant = find_tenant(resource)
69
114
 
70
- set_rls!(tenant.tenant_id)
115
+ set_rls!(tenant)
71
116
 
72
117
  tenant
73
118
  rescue NoMethodError
74
119
  raise PgRls::Errors::TenantNotFound
120
+ ensure
121
+ reset_rls! if tenant.blank?
75
122
  end
76
123
 
77
124
  def find_tenant(resource)
78
- reset_rls!
79
-
80
125
  tenant = nil
81
126
 
82
127
  PgRls.search_methods.each do |method|
@@ -85,14 +130,20 @@ module PgRls
85
130
  tenant = find_tenant_by_method(resource, method)
86
131
  end
87
132
 
133
+ reset_rls! if reset_rls?(tenant)
88
134
  raise PgRls::Errors::TenantNotFound if tenant.blank?
89
135
 
90
136
  tenant
91
137
  end
92
138
 
139
+ def reset_rls?(tenant)
140
+ PgRls::Current::Context.tenant.present? && tenant.present? && PgRls::Current::Context.tenant != tenant
141
+ end
142
+
93
143
  def find_tenant_by_method(resource, method)
94
- look_up_value = resource.is_a?(PgRls.main_model) ? resource.send(method) : resource
95
- PgRls.main_model.send("find_by_#{method}!", look_up_value)
144
+ return resource if resource.is_a?(PgRls.main_model)
145
+
146
+ PgRls.main_model.unscoped.send(:"find_by_#{method}!", resource)
96
147
  rescue ActiveRecord::RecordNotFound
97
148
  nil
98
149
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgRls
4
- VERSION = '0.1.10'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/pg_rls.rb CHANGED
@@ -5,25 +5,31 @@ require 'forwardable'
5
5
  require_relative 'pg_rls/version'
6
6
  require_relative 'pg_rls/database/prepared'
7
7
  require_relative 'pg_rls/schema/statements'
8
+ require_relative 'pg_rls/schema/dumper'
8
9
  require_relative 'pg_rls/database/configurations'
9
10
  require_relative 'pg_rls/database/admin_statements'
10
11
  require_relative 'pg_rls/tenant'
11
12
  require_relative 'pg_rls/multi_tenancy'
12
13
  require_relative 'pg_rls/railtie' if defined?(Rails)
13
14
  require_relative 'pg_rls/errors/index'
15
+ require_relative 'pg_rls/current/context'
16
+ require_relative 'pg_rls/logger'
14
17
 
15
18
  ActiveRecord::Migrator.prepend PgRls::Admin::ActiveRecord::Migrator
16
19
  ActiveRecord::Tasks::DatabaseTasks.prepend PgRls::Admin::ActiveRecord::Tasks::DatabaseTasks
17
20
  ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgRls::Schema::Statements
21
+ ActiveRecord::SchemaDumper.prepend PgRls::Schema::Dumper
22
+
18
23
  # PostgreSQL Row Level Security
19
24
  module PgRls
20
25
  class Error < StandardError; end
21
26
  class << self
22
27
  extend Forwardable
23
28
 
24
- WRITER_METHODS = %i[table_name class_name search_methods].freeze
25
- READER_METHODS = %i[connection_class execute table_name class_name search_methods].freeze
26
- DELEGATORS_METHODS = %i[connection_class execute table_name search_methods class_name main_model].freeze
29
+ WRITER_METHODS = %i[table_name class_name search_methods logger excluded_shards].freeze
30
+ READER_METHODS = %i[connection_class execute table_name class_name search_methods logger excluded_shards].freeze
31
+ DELEGATORS_METHODS = %i[connection_class execute table_name search_methods class_name main_model logger
32
+ excluded_shards].freeze
27
33
 
28
34
  attr_writer(*WRITER_METHODS)
29
35
  attr_reader(*READER_METHODS)
@@ -36,7 +42,7 @@ module PgRls
36
42
  yield self
37
43
 
38
44
  Rails.application.config.to_prepare do
39
- PgRls.main_model.ignored_columns = []
45
+ PgRls.main_model.ignored_columns = [] # rubocop:disable Rails/IgnoredColumnsAssignment
40
46
  end
41
47
  end
42
48
 
@@ -54,7 +60,6 @@ module PgRls
54
60
  def establish_new_connection!(admin: false)
55
61
  self.as_db_admin = admin
56
62
 
57
- db_config = PgRls.main_model.connection_db_config.configuration_hash
58
63
  execute_rls_in_shards do |connection_class, pool|
59
64
  connection_class.connection_pool.disconnect!
60
65
  connection_class.remove_connection
@@ -66,20 +71,23 @@ module PgRls
66
71
  class_name.to_s.camelize.constantize
67
72
  end
68
73
 
69
- def on_each_tenant(&)
70
- with_rls_connection do
71
- result = []
72
-
73
- main_model.find_each do |tenant|
74
- allowed_search_fields = search_methods.map(&:to_s).intersection(main_model.column_names)
75
- Tenant.switch tenant.send(allowed_search_fields.first)
76
-
77
- result << { tenant:, result: ensure_block_execution(tenant, &) }
78
- end
79
-
80
- PgRls::Tenant.reset_rls!
74
+ def on_each_tenant(ids: [], scope: nil, &)
75
+ logger.deprecation_warning(
76
+ 'PgRls.on_each_tenant is deprecated and will be removed in future versions. ' \
77
+ 'Please use PgRls::Tenant.on_find_each instead.'
78
+ )
79
+ Tenant.on_find_each(ids:, scope:, &)
80
+ end
81
81
 
82
- result
82
+ rails_version = Gem.loaded_specs['rails'].version
83
+ if rails_version >= Gem::Version.new('7.2') && rails_version < Gem::Version.new('7.3')
84
+ def pool_connection(pool)
85
+ pool.lease_connection
86
+ end
87
+ else
88
+ def pool_connection(pool)
89
+ PgRls.logger.deprecation_warning('PgRls.pool_connection is deprecated and will be removed in future PgRls 0.2.0. Please use pool.lease_connection instead.')
90
+ pool.connection
83
91
  end
84
92
  end
85
93
 
@@ -88,10 +96,13 @@ module PgRls
88
96
  result = []
89
97
 
90
98
  connection_pool_list.each do |pool|
91
- pool.connection.transaction do
92
- Rails.logger.info("Executing in #{pool.connection.connection_class}")
99
+ connection = pool_connection(pool)
100
+ next if excluded_shards.include?(connection.connection_class.connection_db_config.name)
101
+
102
+ connection.transaction do
103
+ Rails.logger.info("Executing in #{connection.connection_class}")
93
104
 
94
- result << yield(pool.connection.connection_class, pool)
105
+ result << yield(connection.connection_class, pool)
95
106
  end
96
107
  end
97
108
 
@@ -141,11 +152,9 @@ module PgRls
141
152
  end
142
153
 
143
154
  def execute_query_or_block(query = nil, &)
144
- if block_given?
145
- ensure_block_execution(&)
146
- else
147
- execute(query)
148
- end
155
+ return ensure_block_execution(&) if block_given?
156
+
157
+ execute(query)
149
158
  end
150
159
 
151
160
  def reset_connection_if_needed(current_tenant, reset_rls_connection)
@@ -173,4 +182,10 @@ module PgRls
173
182
 
174
183
  mattr_accessor :search_methods
175
184
  @@search_methods = %i[subdomain id tenant_id]
185
+
186
+ mattr_accessor :logger
187
+ @@logger = PgRls::Logger.new
188
+
189
+ mattr_accessor :excluded_shards
190
+ @@excluded_shards = []
176
191
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_rls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Laloush
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-05 00:00:00.000000000 Z
11
+ date: 2024-09-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  This gem will help you to integrate PostgreSQL RLS to help you develop a great multitenancy application
@@ -47,6 +47,7 @@ files:
47
47
  - lib/generators/templates/pg_rls.rb.tt
48
48
  - lib/pg_rls.rb
49
49
  - lib/pg_rls/Rakefile
50
+ - lib/pg_rls/current/context.rb
50
51
  - lib/pg_rls/database/admin_statements.rb
51
52
  - lib/pg_rls/database/configurations.rb
52
53
  - lib/pg_rls/database/prepared.rb
@@ -54,6 +55,7 @@ files:
54
55
  - lib/pg_rls/errors/index.rb
55
56
  - lib/pg_rls/errors/rake_only_error.rb
56
57
  - lib/pg_rls/errors/tenant_not_found.rb
58
+ - lib/pg_rls/logger.rb
57
59
  - lib/pg_rls/middleware.rb
58
60
  - lib/pg_rls/middleware/set_reset_connection.rb
59
61
  - lib/pg_rls/middleware/sidekiq.rb
@@ -62,6 +64,7 @@ files:
62
64
  - lib/pg_rls/multi_tenancy.rb
63
65
  - lib/pg_rls/railtie.rb
64
66
  - lib/pg_rls/schema/down_statements.rb
67
+ - lib/pg_rls/schema/dumper.rb
65
68
  - lib/pg_rls/schema/statements.rb
66
69
  - lib/pg_rls/schema/up_statements.rb
67
70
  - lib/pg_rls/tenant.rb
@@ -86,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
89
  - !ruby/object:Gem::Version
87
90
  version: '0'
88
91
  requirements: []
89
- rubygems_version: 3.5.11
92
+ rubygems_version: 3.5.16
90
93
  signing_key:
91
94
  specification_version: 4
92
95
  summary: Write a short summary, because RubyGems requires one.