pg_rls 0.1.11 → 0.2.1

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: 0e815f8e1e99ddf19dcd53be6876034dbeb535985c46ab7111555ac0fa02c02a
4
- data.tar.gz: 4ce207b4766242cdc97940b313bb9a8ea864250ebd953bf4c36168d6ceced848
3
+ metadata.gz: 6bd9ecf91e6966f35c1bdc24a9f312209bc505dcaa220957437eaf4b1b9c6d42
4
+ data.tar.gz: 605a1950d3f4c338f808bb49129564f3ae2d7fe20d40a00698e22bc7f4603bfd
5
5
  SHA512:
6
- metadata.gz: 9369bf467e43421b42093783428b3fdb544806ee378ad67e43500fecd530e971bc50b3671faf7cb54d126281bf21c5f88a7f9bb3eeffa52bdce0068578cfae04
7
- data.tar.gz: d6fef41724951488a13c21fad439cc3f1f0c781e918d022f426adce50d81e8133f98961695042143dd8c6f96a229d8e6d42baed8cadf563b14b5ece4f8960611
6
+ metadata.gz: e6ab0a689e37a49cf95bf5126f7897902abb15bfb061a750486806e0b2040d30d92a71f4bc0747a33e5121c0a94a9dee4e90c690511716a93eab2118a04d9486
7
+ data.tar.gz: 05fd9172dc9b97ca4cda73d6a5dd8accea22d022786b638a5268f47fd1f6d99c7a598594eaf914fed47541690e985de0fb27c6fcce76bcb54f5757ee6eba80d1
@@ -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
@@ -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,17 +36,32 @@ module PgRls
36
36
  end
37
37
 
38
38
  def fetch!
39
- PgRls::Current::Context.tenant ||= 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
 
@@ -54,30 +69,60 @@ module PgRls
54
69
  nil
55
70
  end
56
71
 
57
- def set_rls!(tenant_id)
72
+ def set_rls!(tenant)
73
+ tenant_id = tenant.tenant_id
58
74
  PgRls.execute_rls_in_shards do |connection_class|
59
- connection_class.transaction do
60
- connection_class.connection.execute(format('SET rls.tenant_id = %s',
61
- 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
62
80
  end
63
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
64
98
  end
65
99
 
66
100
  private
67
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
+
68
112
  def switch_tenant!(resource)
69
113
  tenant = find_tenant(resource)
70
114
 
71
- set_rls!(tenant.tenant_id)
115
+ PgRls.establish_new_connection! if PgRls.admin_connection?
116
+ set_rls!(tenant)
72
117
 
73
118
  tenant
74
119
  rescue NoMethodError
75
120
  raise PgRls::Errors::TenantNotFound
121
+ ensure
122
+ reset_rls! if tenant.blank?
76
123
  end
77
124
 
78
125
  def find_tenant(resource)
79
- reset_rls!
80
-
81
126
  tenant = nil
82
127
 
83
128
  PgRls.search_methods.each do |method|
@@ -86,14 +131,20 @@ module PgRls
86
131
  tenant = find_tenant_by_method(resource, method)
87
132
  end
88
133
 
134
+ reset_rls! if reset_rls?(tenant)
89
135
  raise PgRls::Errors::TenantNotFound if tenant.blank?
90
136
 
91
137
  tenant
92
138
  end
93
139
 
140
+ def reset_rls?(tenant)
141
+ PgRls::Current::Context.tenant.present? && tenant.present? && PgRls::Current::Context.tenant != tenant
142
+ end
143
+
94
144
  def find_tenant_by_method(resource, method)
95
- look_up_value = resource.is_a?(PgRls.main_model) ? resource.send(method) : resource
96
- PgRls.main_model.send("find_by_#{method}!", look_up_value)
145
+ return resource if resource.is_a?(PgRls.main_model)
146
+
147
+ PgRls.main_model.unscoped.send(:"find_by_#{method}!", resource)
97
148
  rescue ActiveRecord::RecordNotFound
98
149
  nil
99
150
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgRls
4
- VERSION = '0.1.11'
4
+ VERSION = '0.2.1'
5
5
  end
data/lib/pg_rls.rb CHANGED
@@ -5,6 +5,7 @@ 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'
@@ -12,19 +13,23 @@ require_relative 'pg_rls/multi_tenancy'
12
13
  require_relative 'pg_rls/railtie' if defined?(Rails)
13
14
  require_relative 'pg_rls/errors/index'
14
15
  require_relative 'pg_rls/current/context'
16
+ require_relative 'pg_rls/logger'
15
17
 
16
18
  ActiveRecord::Migrator.prepend PgRls::Admin::ActiveRecord::Migrator
17
19
  ActiveRecord::Tasks::DatabaseTasks.prepend PgRls::Admin::ActiveRecord::Tasks::DatabaseTasks
18
20
  ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgRls::Schema::Statements
21
+ ActiveRecord::SchemaDumper.prepend PgRls::Schema::Dumper
22
+
19
23
  # PostgreSQL Row Level Security
20
24
  module PgRls
21
25
  class Error < StandardError; end
22
26
  class << self
23
27
  extend Forwardable
24
28
 
25
- WRITER_METHODS = %i[table_name class_name search_methods].freeze
26
- READER_METHODS = %i[connection_class execute table_name class_name search_methods].freeze
27
- 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
28
33
 
29
34
  attr_writer(*WRITER_METHODS)
30
35
  attr_reader(*READER_METHODS)
@@ -37,7 +42,7 @@ module PgRls
37
42
  yield self
38
43
 
39
44
  Rails.application.config.to_prepare do
40
- PgRls.main_model.ignored_columns = []
45
+ PgRls.main_model.ignored_columns = [] # rubocop:disable Rails/IgnoredColumnsAssignment
41
46
  end
42
47
  end
43
48
 
@@ -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
 
@@ -171,4 +182,10 @@ module PgRls
171
182
 
172
183
  mattr_accessor :search_methods
173
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 = []
174
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.11
4
+ version: 0.2.1
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-08-28 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
@@ -55,6 +55,7 @@ files:
55
55
  - lib/pg_rls/errors/index.rb
56
56
  - lib/pg_rls/errors/rake_only_error.rb
57
57
  - lib/pg_rls/errors/tenant_not_found.rb
58
+ - lib/pg_rls/logger.rb
58
59
  - lib/pg_rls/middleware.rb
59
60
  - lib/pg_rls/middleware/set_reset_connection.rb
60
61
  - lib/pg_rls/middleware/sidekiq.rb
@@ -63,6 +64,7 @@ files:
63
64
  - lib/pg_rls/multi_tenancy.rb
64
65
  - lib/pg_rls/railtie.rb
65
66
  - lib/pg_rls/schema/down_statements.rb
67
+ - lib/pg_rls/schema/dumper.rb
66
68
  - lib/pg_rls/schema/statements.rb
67
69
  - lib/pg_rls/schema/up_statements.rb
68
70
  - lib/pg_rls/tenant.rb
@@ -87,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
89
  - !ruby/object:Gem::Version
88
90
  version: '0'
89
91
  requirements: []
90
- rubygems_version: 3.5.11
92
+ rubygems_version: 3.5.16
91
93
  signing_key:
92
94
  specification_version: 4
93
95
  summary: Write a short summary, because RubyGems requires one.