pg_rls 0.0.2.6.11 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,7 +5,7 @@ module PgRls
5
5
  # Up Schema Statements
6
6
  module UpStatements
7
7
  def create_rls_user(name: PgRls.username, password: PgRls.password, schema: 'public')
8
- PgRls.execute <<-SQL
8
+ ActiveRecord::Migration.execute <<-SQL
9
9
  DO
10
10
  $do$
11
11
  BEGIN
@@ -35,7 +35,7 @@ module PgRls
35
35
  end
36
36
 
37
37
  def create_rls_blocking_function
38
- ActiveRecord::Migration.execute <<-SQL
38
+ ActiveRecord::Migration.execute <<-SQL.squish
39
39
  CREATE OR REPLACE FUNCTION id_safe_guard ()
40
40
  RETURNS TRIGGER LANGUAGE plpgsql AS $$
41
41
  BEGIN
@@ -45,7 +45,7 @@ module PgRls
45
45
  end
46
46
 
47
47
  def create_rls_setter_function
48
- ActiveRecord::Migration.execute <<-SQL
48
+ ActiveRecord::Migration.execute <<-SQL.squish
49
49
  CREATE OR REPLACE FUNCTION tenant_id_setter ()
50
50
  RETURNS TRIGGER LANGUAGE plpgsql AS $$
51
51
  BEGIN
@@ -56,7 +56,7 @@ module PgRls
56
56
  end
57
57
 
58
58
  def append_blocking_function(table_name)
59
- ActiveRecord::Migration.execute <<-SQL
59
+ ActiveRecord::Migration.execute <<-SQL.squish
60
60
  CREATE TRIGGER id_safe_guard
61
61
  BEFORE UPDATE OF id ON #{table_name}
62
62
  FOR EACH ROW EXECUTE PROCEDURE id_safe_guard();
@@ -64,7 +64,7 @@ module PgRls
64
64
  end
65
65
 
66
66
  def append_trigger_function(table_name)
67
- ActiveRecord::Migration.execute <<-SQL
67
+ ActiveRecord::Migration.execute <<-SQL.squish
68
68
  CREATE TRIGGER tenant_id_setter
69
69
  BEFORE INSERT OR UPDATE ON #{table_name}
70
70
  FOR EACH ROW EXECUTE PROCEDURE tenant_id_setter();
@@ -72,7 +72,7 @@ module PgRls
72
72
  end
73
73
 
74
74
  def add_rls_column_to_tenant_table(table_name)
75
- ActiveRecord::Migration.execute <<-SQL
75
+ ActiveRecord::Migration.execute <<-SQL.squish
76
76
  ALTER TABLE #{table_name}
77
77
  ADD COLUMN IF NOT EXISTS
78
78
  tenant_id uuid UNIQUE DEFAULT gen_random_uuid();
@@ -80,7 +80,7 @@ module PgRls
80
80
  end
81
81
 
82
82
  def add_rls_column(table_name)
83
- ActiveRecord::Migration.execute <<-SQL
83
+ ActiveRecord::Migration.execute <<-SQL.squish
84
84
  ALTER TABLE #{table_name}
85
85
  ADD COLUMN IF NOT EXISTS tenant_id uuid,
86
86
  ADD CONSTRAINT fk_#{PgRls.table_name}
@@ -91,7 +91,7 @@ module PgRls
91
91
  end
92
92
 
93
93
  def create_rls_policy(table_name, user = PgRls.username)
94
- ActiveRecord::Migration.execute <<-SQL
94
+ ActiveRecord::Migration.execute <<-SQL.squish
95
95
  ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY;
96
96
  CREATE POLICY #{table_name}_#{user}
97
97
  ON #{table_name}
data/lib/pg_rls/tenant.rb CHANGED
@@ -25,16 +25,10 @@ module PgRls
25
25
  raise e
26
26
  end
27
27
 
28
- def find_each(&)
29
- PgRls.main_model.find_each do |tenant|
30
- with_tenant!(tenant, &)
31
- end
32
- end
33
-
34
- def with_tenant!(resource, &block)
28
+ def with_tenant!(resource)
35
29
  tenant = switch_tenant!(resource)
36
30
 
37
- block.call(tenant) if block_given?
31
+ yield(tenant) if block_given?
38
32
  ensure
39
33
  reset_rls! unless PgRls.test_inline_tenant == true
40
34
  end
@@ -45,47 +39,50 @@ module PgRls
45
39
  nil
46
40
  end
47
41
 
48
- def fetch!
42
+ def tenant!
49
43
  @tenant ||= PgRls.main_model.find_by!(
50
44
  tenant_id: PgRls.connection_class.connection.execute(
51
45
  "SELECT current_setting('rls.tenant_id')"
52
46
  ).getvalue(0, 0)
53
47
  )
54
48
  end
55
-
56
- def find_main_model
57
- PgRls.main_model.ignored_columns = []
58
- PgRls.main_model.find_by!(
59
- tenant_id: PgRls.connection_class.connection.execute(
60
- "SELECT current_setting('rls.tenant_id')"
61
- ).getvalue(0, 0)
62
- )
63
- end
49
+ alias fetch! tenant!
64
50
 
65
51
  def reset_rls!
52
+ return if @tenant.blank?
53
+
66
54
  @tenant = nil
67
- PgRls.connection_class.connection.execute('RESET rls.tenant_id')
55
+ PgRls.execute_rls_in_shards do |connection_class|
56
+ connection_class.transaction do
57
+ connection_class.connection.execute('RESET rls.tenant_id')
58
+ end
59
+ end
60
+
61
+ nil
68
62
  end
69
63
 
70
64
  private
71
65
 
72
66
  def switch_tenant!(resource)
67
+ # rubocop: disable Rails/IgnoredColumnsAssignment
73
68
  PgRls.main_model.ignored_columns = []
69
+ # rubocop: enable Rails/IgnoredColumnsAssignment
74
70
 
75
- connection_adapter = PgRls.connection_class
76
71
  find_tenant(resource)
77
72
 
78
- raise PgRls::Errors::TenantNotFound if tenant.blank?
79
-
80
- connection_adapter.connection.transaction do
81
- connection_adapter.connection.execute(format('SET rls.tenant_id = %s',
82
- connection_adapter.connection.quote(tenant.tenant_id)))
73
+ PgRls.execute_rls_in_shards do |connection_class|
74
+ connection_class.transaction do
75
+ connection_class.connection.execute(format('SET rls.tenant_id = %s',
76
+ connection_class.connection.quote(tenant.tenant_id)))
77
+ end
83
78
  end
84
79
 
85
80
  tenant
86
81
  end
87
82
 
88
83
  def find_tenant(resource)
84
+ raise PgRls::Errors::AdminUsername if admin_username?
85
+
89
86
  reset_rls!
90
87
 
91
88
  PgRls.search_methods.each do |method|
@@ -95,7 +92,11 @@ module PgRls
95
92
  @tenant = find_tenant_by_method(resource, method)
96
93
  end
97
94
 
98
- raise PgRls::Errors::TenantNotFound if tenant.nil?
95
+ raise PgRls::Errors::TenantNotFound if tenant.blank?
96
+ end
97
+
98
+ def admin_username?
99
+ PgRls.username != PgRls.current_db_username
99
100
  end
100
101
 
101
102
  def find_tenant_by_method(resource, method)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgRls
4
- VERSION = '0.0.2.6.11'
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/pg_rls.rb CHANGED
@@ -5,13 +5,11 @@ 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/solo/statements'
9
- require_relative 'pg_rls/solo/tenant'
8
+ require_relative 'pg_rls/database/configurations'
10
9
  require_relative 'pg_rls/tenant'
11
- require_relative 'pg_rls/secure_connection'
12
10
  require_relative 'pg_rls/multi_tenancy'
13
11
  require_relative 'pg_rls/railtie' if defined?(Rails)
14
- require_relative 'pg_rls/errors/tenant_not_found'
12
+ require_relative 'pg_rls/errors/index'
15
13
 
16
14
  # PostgreSQL Row Level Security
17
15
  module PgRls
@@ -21,13 +19,13 @@ module PgRls
21
19
  class << self
22
20
  extend Forwardable
23
21
 
24
- WRITER_METHODS = %i[table_name class_name search_methods establish_default_connection].freeze
22
+ WRITER_METHODS = %i[table_name class_name search_methods].freeze
25
23
  READER_METHODS = %i[
26
- connection_class database_configuration execute table_name class_name search_methods establish_default_connection
24
+ connection_class execute table_name class_name search_methods
27
25
  ].freeze
28
26
  DELEGATORS_METHODS = %i[
29
- connection_class database_configuration execute table_name search_methods
30
- class_name all_tenants main_model establish_default_connection
27
+ connection_class execute table_name search_methods
28
+ class_name main_model
31
29
  ].freeze
32
30
 
33
31
  attr_writer(*WRITER_METHODS)
@@ -36,100 +34,104 @@ module PgRls
36
34
  def_delegators(*DELEGATORS_METHODS)
37
35
 
38
36
  def setup
37
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgRls::Schema::Statements
38
+ ActiveRecord::Base.ignored_columns += %w[tenant_id]
39
+
39
40
  yield self
40
41
  end
41
42
 
42
- def solo_mode?
43
- solo_mode
43
+ def connection_class
44
+ @connection_class ||= ActiveRecord::Base
44
45
  end
45
46
 
46
- def database_connection_file
47
- file = File.read(Rails.root.join('config/database.yml'))
48
-
49
- YAML.safe_load(ERB.new(file).result, aliases: true)
47
+ def rake_tasks?
48
+ Rake.application.top_level_tasks.present? || ARGV.any? { |arg| arg =~ /rake|dsl/ }
49
+ rescue NoMethodError
50
+ false
50
51
  end
51
52
 
52
- def connection_class
53
- @connection_class ||= ActiveRecord::Base
54
- end
53
+ def admin_tasks_execute
54
+ raise PgRls::Errors::RakeOnlyError unless rake_tasks?
55
55
 
56
- def establish_new_connection
57
- ActiveRecord::Base.connection.disconnect! if ActiveRecord::Base.connection_pool.connected?
56
+ self.as_db_admin = true
58
57
 
59
- connection_class.establish_connection(**database_configuration)
58
+ yield
59
+ ensure
60
+ self.as_db_admin = false
60
61
  end
61
62
 
62
- def admin_execute(query = nil)
63
+ def admin_execute(query = nil, &)
64
+ current_tenant = PgRls::Tenant.fetch
65
+
63
66
  self.as_db_admin = true
64
- establish_new_connection
65
- return yield if block_given?
67
+ establish_new_connection!
68
+
69
+ return ensure_block_execution(&) if block_given?
66
70
 
67
71
  execute(query)
68
72
  ensure
69
73
  self.as_db_admin = false
70
- establish_new_connection
71
- end
72
-
73
- def establish_default_connection=(value)
74
- ENV['AS_DB_ADMIN'] = value.to_s
75
- @default_connection = value
74
+ establish_new_connection!
75
+ PgRls::Tenant.switch(current_tenant) if current_tenant.present?
76
76
  end
77
77
 
78
- def default_connection?
79
- as_db_admin
78
+ def establish_new_connection!
79
+ execute_rls_in_shards do |connection_class, pool|
80
+ connection_class.remove_connection
81
+ connection_class.establish_connection(pool.db_config)
82
+ end
80
83
  end
81
84
 
82
85
  def main_model
83
86
  class_name.to_s.camelize.constantize
84
87
  end
85
88
 
86
- def all_tenants
87
- main_model.all.each do |tenant|
89
+ def on_each_tenant(&)
90
+ result = []
91
+ main_model.find_each do |tenant|
88
92
  allowed_search_fields = search_methods.map(&:to_s).intersection(main_model.column_names)
89
93
  Tenant.switch tenant.send(allowed_search_fields.first)
90
94
 
91
- yield(tenant) if block_given?
95
+ result << { tenant:, result: ensure_block_execution(tenant, &) }
92
96
  end
93
- end
94
97
 
95
- def current_connection_username
96
- connection_class.connection_db_config.configuration_hash[:username]
97
- end
98
+ PgRls::Tenant.reset_rls!
98
99
 
99
- def execute(query)
100
- ActiveRecord::Migration.execute(query)
100
+ result
101
101
  end
102
102
 
103
- def database_default_configuration
104
- connection_class.connection.pool.db_config.configuration_hash
105
- rescue ActiveRecord::NoDatabaseError
106
- connection_class.connection_db_config.configuration_hash
107
- end
103
+ def execute_rls_in_shards
104
+ connection_pool_list = PgRls.connection_class.connection_handler.connection_pool_list
105
+ result = []
108
106
 
109
- def database_admin_configuration
110
- environment_db_configuration = database_connection_file[Rails.env]
107
+ connection_pool_list.each do |pool|
108
+ pool.connection.transaction do
109
+ Rails.logger.info("Executing in #{pool.connection.connection_class}")
111
110
 
112
- return environment_db_configuration if environment_db_configuration['username'].present?
111
+ result << yield(pool.connection.connection_class, pool)
112
+ end
113
+ end
113
114
 
114
- environment_db_configuration.first.last
115
+ result
115
116
  end
116
117
 
117
- def database_configuration
118
- return database_admin_configuration if default_connection?
118
+ def current_db_username
119
+ ActiveRecord::Base.connection_db_config.configuration_hash[:username]
120
+ end
119
121
 
120
- current_configuration = database_default_configuration.deep_dup
121
- current_configuration.tap do |config|
122
- config[:username] = PgRls.username
123
- config[:password] = PgRls.password
124
- end
122
+ def as_db_admin?
123
+ @as_db_admin || false
124
+ end
125
+
126
+ private
125
127
 
126
- current_configuration.freeze
128
+ attr_writer :as_db_admin
129
+
130
+ def ensure_block_execution(*, **)
131
+ yield(*, **).presence
127
132
  end
128
133
  end
129
134
 
130
- mattr_accessor :as_db_admin
131
- @@as_db_admin = false
132
-
133
135
  mattr_accessor :table_name
134
136
  @@table_name = 'companies'
135
137
 
@@ -142,18 +144,9 @@ module PgRls
142
144
  mattr_accessor :password
143
145
  @@password = 'password'
144
146
 
145
- mattr_accessor :solo_mode
146
- @@solo_mode = false
147
-
148
147
  mattr_accessor :test_inline_tenant
149
148
  @@test_inline_tenant = false
150
149
 
151
- mattr_accessor :session_key
152
- @@session_key = '_hub_sessions'
153
-
154
- mattr_accessor :session_prefix
155
- @@session_prefix = '_session_id:2::'
156
-
157
150
  mattr_accessor :search_methods
158
151
  @@search_methods = %i[subdomain id tenant_id]
159
152
  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.0.2.6.11
4
+ version: 0.1.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: 2023-02-20 00:00:00.000000000 Z
11
+ date: 2023-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -61,8 +61,12 @@ files:
61
61
  - lib/generators/templates/pg_rls.rb.tt
62
62
  - lib/pg_rls.rb
63
63
  - lib/pg_rls/Rakefile
64
+ - lib/pg_rls/database/configurations.rb
64
65
  - lib/pg_rls/database/prepared.rb
65
66
  - lib/pg_rls/database/tasks/admin_database.rake
67
+ - lib/pg_rls/errors/admin_username.rb
68
+ - lib/pg_rls/errors/index.rb
69
+ - lib/pg_rls/errors/rake_only_error.rb
66
70
  - lib/pg_rls/errors/tenant_not_found.rb
67
71
  - lib/pg_rls/middleware.rb
68
72
  - lib/pg_rls/middleware/set_reset_connection.rb
@@ -72,12 +76,8 @@ files:
72
76
  - lib/pg_rls/multi_tenancy.rb
73
77
  - lib/pg_rls/railtie.rb
74
78
  - lib/pg_rls/schema/down_statements.rb
75
- - lib/pg_rls/schema/solo/statements.rb
76
- - lib/pg_rls/schema/solo/up_statements.rb
77
79
  - lib/pg_rls/schema/statements.rb
78
80
  - lib/pg_rls/schema/up_statements.rb
79
- - lib/pg_rls/secure_connection.rb
80
- - lib/pg_rls/solo/tenant.rb
81
81
  - lib/pg_rls/tenant.rb
82
82
  - lib/pg_rls/version.rb
83
83
  homepage: https://github.com/Dandush03/pg_rls
@@ -93,14 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '3.1'
96
+ version: '3.2'
97
97
  required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
102
  requirements: []
103
- rubygems_version: 3.3.26
103
+ rubygems_version: 3.4.19
104
104
  signing_key:
105
105
  specification_version: 4
106
106
  summary: Write a short summary, because RubyGems requires one.
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../statements'
4
- require_relative './up_statements'
5
-
6
- module PgRls
7
- module Schema
8
- module Solo
9
- # Schema Solo Statements
10
- module Statements
11
- include PgRls::Schema::Statements
12
- include PgRls::Schema::Solo::UpStatements
13
-
14
- def create_rls_table(table_name, **options, &)
15
- setup_rls_tenant_table
16
- create_table(table_name, **options, &)
17
- add_rls_column(table_name)
18
- create_rls_policy(table_name)
19
- append_trigger_function(table_name)
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- module Schema
5
- module Solo
6
- # Up Schema Solo Statements
7
- module UpStatements
8
- def setup_rls_tenant_table
9
- ActiveRecord::Migration.execute <<-SQL
10
- DO
11
- $do$
12
- BEGIN
13
- IF NOT EXISTS (
14
- SELECT FROM pg_tables
15
- WHERE schemaname = 'public' AND tablename = '#{PgRls.table_name}') THEN
16
- #{create_rls_user}
17
- #{create_rls_setter_function}
18
- #{create_rls_blocking_function}
19
- #{create_rls_solo_tenant_table}
20
- #{append_blocking_function}
21
- END IF;
22
- END;
23
- $do$;
24
- SQL
25
- end
26
-
27
- def create_rls_user(name: PgRls.username, password: PgRls.password, schema: 'public')
28
- <<~SQL
29
- -- Grant Role Permissions
30
- BEGIN
31
- IF NOT EXISTS (
32
- SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this
33
- WHERE rolname = '#{name}') THEN
34
-
35
- CREATE USER #{name} WITH PASSWORD '#{password}';
36
- END IF;
37
- GRANT ALL PRIVILEGES ON TABLE schema_migrations TO #{name};
38
- GRANT USAGE ON SCHEMA #{schema} TO #{name};
39
- ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
40
- GRANT USAGE, SELECT
41
- ON SEQUENCES TO #{name};
42
- ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
43
- GRANT SELECT, INSERT, UPDATE, DELETE
44
- ON TABLES TO #{name};
45
- GRANT SELECT, INSERT, UPDATE, DELETE
46
- ON ALL TABLES IN SCHEMA #{schema}
47
- TO #{name};
48
- GRANT USAGE, SELECT
49
- ON ALL SEQUENCES IN SCHEMA #{schema}
50
- TO #{name};
51
- END;
52
- SQL
53
- end
54
-
55
- def create_rls_setter_function
56
- <<~SQL
57
- -- Create RLS Setter Function
58
- CREATE OR REPLACE FUNCTION tenant_id_setter ()
59
- RETURNS TRIGGER LANGUAGE plpgsql AS $$
60
- BEGIN
61
- IF NOT EXISTS (
62
- SELECT FROM #{PgRls.table_name}
63
- WHERE tenant_id = (current_setting('rls.tenant_id'))::uuid
64
- ) THEN
65
- INSERT INTO #{PgRls.table_name} (tenant_id)
66
- VALUES ((current_setting('rls.tenant_id'))::uuid);
67
- END IF;
68
-
69
- NEW.tenant_id:= (current_setting('rls.tenant_id'));
70
- RETURN NEW;
71
- END $$;
72
- SQL
73
- end
74
-
75
- def create_rls_blocking_function
76
- <<~SQL
77
- -- Create RLS Blocking Function
78
- CREATE OR REPLACE FUNCTION id_safe_guard ()
79
- RETURNS TRIGGER LANGUAGE plpgsql AS $$
80
- BEGIN
81
- RAISE EXCEPTION 'This column is guarded due to tenancy dependency';
82
- END $$;
83
- SQL
84
- end
85
-
86
- def create_rls_solo_tenant_table
87
- <<~SQL
88
- -- Create Tenant Table
89
- CREATE TABLE #{PgRls.table_name} (
90
- tenant_id uuid PRIMARY KEY
91
- );
92
- SQL
93
- end
94
-
95
- def append_blocking_function
96
- <<~SQL
97
- -- Append Blocking Function
98
- CREATE TRIGGER id_safe_guard
99
- BEFORE UPDATE OF tenant_id ON #{PgRls.table_name}
100
- FOR EACH ROW EXECUTE PROCEDURE id_safe_guard();
101
- SQL
102
- end
103
- end
104
- end
105
- end
106
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- # Ensure Connection is with App_use
5
- module SecureConnection
6
- def self.establish_secure_connection
7
- return if secure_connection_established?
8
-
9
- return if PgRls.default_connection?
10
-
11
- PgRls.establish_new_connection
12
- end
13
-
14
- def self.secure_connection_established?
15
- PgRls.current_connection_username == PgRls.username
16
- end
17
-
18
- def self.included(base)
19
- establish_secure_connection
20
- base.ignored_columns = %w[tenant_id]
21
- end
22
- end
23
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- module Solo
5
- # Set and Fetch Tenant without loading a model
6
- class Tenant
7
- class << self
8
- attr_reader :tenant
9
-
10
- def switch!(resource)
11
- switch_tenant!(resource)
12
- rescue StandardError => e
13
- Rails.logger.info('connection was not made')
14
- raise e
15
- end
16
-
17
- def fetch
18
- @fetch ||= PgRls.connection_class.connection.execute(
19
- "SELECT current_setting('rls.tenant_id')"
20
- ).getvalue(0, 0)
21
- end
22
-
23
- def around(resource)
24
- switch_tenant!(resource)
25
- yield
26
- ensure
27
- reset_rls!
28
- end
29
-
30
- private
31
-
32
- def reset_rls!
33
- @fetch = nil
34
- @tenant = nil
35
- PgRls.connection_class.connection.execute('RESET rls.tenant_id')
36
- end
37
-
38
- def switch_tenant!(resource)
39
- connection_adapter = PgRls.connection_class
40
-
41
- raise PgRls::Errors::TenantNotFound if resource.blank?
42
-
43
- connection_adapter.connection.execute(format('SET rls.tenant_id = %s',
44
- connection_adapter.connection.quote(resource)))
45
- "RLS changed to '#{resource}'"
46
- end
47
- end
48
- end
49
- end
50
- end