pg_rls 0.2.5 → 1.0.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 +4 -4
- data/.rubocop.yml +55 -17
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -2
- data/CODE_OF_CONDUCT.md +77 -29
- data/Guardfile +44 -0
- data/README.md +247 -83
- data/Rakefile +5 -12
- data/Steepfile +29 -0
- data/UPGRADE.md +106 -0
- data/app/models/pg_rls/admin.rb +24 -0
- data/app/models/pg_rls/current.rb +48 -0
- data/app/models/pg_rls/record.rb +13 -0
- data/app/models/pg_rls/tenant/searchable.rb +60 -0
- data/app/models/pg_rls/tenant/securable.rb +67 -0
- data/app/models/pg_rls/tenant/switchable.rb +40 -0
- data/app/models/pg_rls/tenant.rb +9 -0
- data/assets/logo.svg +8 -0
- data/docker-compose.yml +14 -0
- data/lib/generators/pg_rls/active_record/active_record_generator.rb +62 -65
- data/lib/generators/pg_rls/install/install_generator.rb +38 -0
- data/lib/generators/pg_rls/pg_rls_generator.rb +2 -1
- data/lib/generators/pg_rls/templates/USAGE +28 -0
- data/lib/generators/pg_rls/{active_record/templates → templates/app/models}/abstract_base_class.rb.tt +1 -3
- data/lib/generators/pg_rls/{active_record/templates → templates/app/models}/model.rb.tt +0 -2
- data/lib/generators/pg_rls/templates/config/initializers/pg_rls.rb.tt +58 -0
- data/lib/generators/pg_rls/templates/db/migrate/backport_pg_rls_table.rb.tt +14 -0
- data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_table.rb.tt +5 -0
- data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_tenant_table.rb.tt +5 -0
- data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_table.rb.tt +29 -0
- data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_tenant_table.rb.tt +29 -0
- data/lib/pg_rls/active_record/connection_adapters/connection_pool.rb +31 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rb +207 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/errors.rb +17 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rb +167 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rb +91 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rb +56 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rb +95 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rb +127 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rb +71 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rb +120 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rb +30 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql.rb +36 -0
- data/lib/pg_rls/active_record/connection_adapters.rb +12 -0
- data/lib/pg_rls/active_record/database_shards.rb +74 -0
- data/lib/pg_rls/active_record/migration/command_recorder.rb +28 -0
- data/lib/pg_rls/active_record/migration.rb +11 -0
- data/lib/pg_rls/active_record/test_databases.rb +19 -0
- data/lib/pg_rls/active_record.rb +11 -0
- data/lib/pg_rls/active_support/string_ext.rb +17 -0
- data/lib/pg_rls/active_support.rb +9 -0
- data/lib/pg_rls/connection_config.rb +61 -0
- data/lib/pg_rls/deprecation.rb +14 -0
- data/lib/pg_rls/engine.rb +8 -0
- data/lib/pg_rls/error.rb +10 -0
- data/lib/pg_rls/generators/.keep +0 -0
- data/lib/pg_rls/railtie.rb +1 -11
- data/lib/pg_rls/tasks/.keep +0 -0
- data/lib/pg_rls/version.rb +3 -1
- data/lib/pg_rls.rb +67 -151
- data/rbs_collection.lock.yaml +132 -0
- data/rbs_collection.yaml +127 -0
- data/review_code.sh +33 -0
- data/sig/generators/pg_rls/active_record/active_record_generator.rbs +43 -0
- data/sig/generators/pg_rls/install/install_generator.rbs +20 -0
- data/sig/generators/pg_rls/pg_rls_generator.rbs +9 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rbs +53 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/errors.rbs +24 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rbs +55 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rbs +31 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rbs +28 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rbs +35 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rbs +48 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rbs +38 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rbs +67 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rbs +21 -0
- data/sig/pg_rls/active_record/connection_adapters/postgresql.rbs +10 -0
- data/sig/pg_rls/active_record/connection_adapters.rbs +50 -0
- data/sig/pg_rls/active_record/database_shards.rbs +34 -0
- data/sig/pg_rls/active_record/migration/command_recorder.rbs +14 -0
- data/sig/pg_rls/active_record/migration.rbs +8 -0
- data/sig/pg_rls/active_record.rbs +7 -0
- data/sig/pg_rls/active_support/hash_ext.rbs +11 -0
- data/sig/pg_rls/active_support/string_ext.rbs +27 -0
- data/sig/pg_rls/active_support.rbs +7 -0
- data/sig/pg_rls/app/models/pg_rls/record.rbs +4 -0
- data/sig/pg_rls/connection_config.rbs +16 -0
- data/sig/pg_rls/deprecation.rbs +9 -0
- data/sig/pg_rls/engine.rbs +7 -0
- data/sig/pg_rls/errors.rbs +14 -0
- data/sig/pg_rls/railtie.rbs +6 -0
- data/sig/pg_rls/tenant_test_helper.rbs +14 -0
- data/sig/pg_rls.rbs +60 -0
- data/sig/support/active_record.rbs +86 -0
- data/sig/support/active_support.rbs +7 -0
- data/sig/support/fowardable.rbs +2 -0
- data/sig/support/pg.rbs +12 -0
- data/sig/support/rails.rbs +38 -0
- data/start.sh +30 -0
- metadata +167 -48
- data/.rspec +0 -3
- data/Gemfile +0 -21
- data/Gemfile.lock +0 -300
- data/LICENSE.txt +0 -21
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/lib/generators/pg_rls/active_record/templates/convert_migration.rb.tt +0 -11
- data/lib/generators/pg_rls/active_record/templates/convert_migration_backport.rb.tt +0 -12
- data/lib/generators/pg_rls/active_record/templates/init_convert_migration.rb.tt +0 -11
- data/lib/generators/pg_rls/active_record/templates/init_migration.rb.tt +0 -25
- data/lib/generators/pg_rls/active_record/templates/init_model.rb.tt +0 -24
- data/lib/generators/pg_rls/active_record/templates/migration.rb.tt +0 -17
- data/lib/generators/pg_rls/base.rb +0 -36
- data/lib/generators/pg_rls/install_generator.rb +0 -90
- data/lib/generators/pg_rls.rb +0 -19
- data/lib/generators/templates/README +0 -22
- data/lib/generators/templates/pg_rls.rb.tt +0 -48
- data/lib/pg_rls/Rakefile +0 -7
- data/lib/pg_rls/current/context.rb +0 -10
- data/lib/pg_rls/database/admin_statements.rb +0 -28
- data/lib/pg_rls/database/configurations.rb +0 -46
- data/lib/pg_rls/database/prepared.rb +0 -40
- data/lib/pg_rls/database/tasks/admin_database.rake +0 -40
- data/lib/pg_rls/errors/index.rb +0 -4
- data/lib/pg_rls/errors/rake_only_error.rb +0 -12
- data/lib/pg_rls/errors/tenant_not_found.rb +0 -13
- data/lib/pg_rls/logger.rb +0 -31
- data/lib/pg_rls/middleware/set_reset_connection.rb +0 -93
- data/lib/pg_rls/middleware/sidekiq/client.rb +0 -22
- data/lib/pg_rls/middleware/sidekiq/server.rb +0 -19
- data/lib/pg_rls/middleware/sidekiq.rb +0 -11
- data/lib/pg_rls/middleware.rb +0 -8
- data/lib/pg_rls/multi_tenancy.rb +0 -32
- data/lib/pg_rls/schema/down_statements.rb +0 -54
- data/lib/pg_rls/schema/dumper.rb +0 -36
- data/lib/pg_rls/schema/statements.rb +0 -72
- data/lib/pg_rls/schema/up_statements.rb +0 -104
- data/lib/pg_rls/tenant.rb +0 -153
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
class Tenant
|
5
|
+
# Searchable Class
|
6
|
+
class Searchable
|
7
|
+
SEARCH_METHOD_BY_COLUMN_TYPE = PgRls::Tenant.columns.each_with_object({}) do |column, hash|
|
8
|
+
type = column.sql_type_metadata.type
|
9
|
+
hash[type] ||= []
|
10
|
+
hash[type] << column.name
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.by_rls_object(tenant)
|
14
|
+
case tenant
|
15
|
+
when Tenant then tenant
|
16
|
+
when String, Symbol, Integer then Searchable.by_rls_methods(tenant)
|
17
|
+
when PgRls.main_model then tenant.becomes(Tenant)
|
18
|
+
else raise PgRls::Error::InvalidSearchInput, "Invalid search input: #{tenant}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.by_rls_methods(search_input)
|
23
|
+
new(search_input).search_methods.each do |search_method|
|
24
|
+
tenant = PgRls::Tenant.find_by(search_method => search_input)
|
25
|
+
return tenant if tenant.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
raise PgRls::Error::TenantNotFound, "No tenant found for #{search_input}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(search_input)
|
32
|
+
@search_input = search_input
|
33
|
+
end
|
34
|
+
|
35
|
+
def search_methods
|
36
|
+
SEARCH_METHOD_BY_COLUMN_TYPE[target_type] || []
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :search_input
|
42
|
+
|
43
|
+
def target_type
|
44
|
+
case search_input
|
45
|
+
when Integer then :integer
|
46
|
+
when String, Symbol then target_type_by_string
|
47
|
+
else raise PgRls::Error::InvalidSearchInput, "Invalid search input: #{search_input}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def target_type_by_string
|
52
|
+
case search_input
|
53
|
+
when /\A\d+\z/ then :integer
|
54
|
+
when /\A[0-9a-fA-F\-]{36}\z/ then :uuid
|
55
|
+
else :string
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
class Tenant
|
5
|
+
# Securable Module
|
6
|
+
module Securable
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
self.table_name = PgRls.table_name
|
11
|
+
|
12
|
+
self.ignored_columns = column_names.reject do |column|
|
13
|
+
PgRls.search_methods.map(&:to_s).include?(column)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def reset_rls_used_connections(connection = PgRls::Record.connection)
|
19
|
+
return connection if rls_connection_object_cache_by_thread.nil?
|
20
|
+
|
21
|
+
connection.exec_query("SET rls.tenant_id TO DEFAULT")
|
22
|
+
self.rls_connection_object_cache_by_thread = nil
|
23
|
+
connection
|
24
|
+
end
|
25
|
+
|
26
|
+
def rls_connection_object_cache_by_thread=(value)
|
27
|
+
::ActiveSupport::IsolatedExecutionState[:active_record_rls_used_connections] = value
|
28
|
+
end
|
29
|
+
|
30
|
+
def rls_connection_object_cache_by_thread
|
31
|
+
::ActiveSupport::IsolatedExecutionState[:active_record_rls_used_connections]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_rls(connection = PgRls::Record.connection)
|
36
|
+
self.class.reset_rls_used_connections if new_tenant?
|
37
|
+
return self if reused_connection?(connection)
|
38
|
+
|
39
|
+
connection.exec_query("SET rls.tenant_id = '#{tenant_id}'")
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def readonly?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def new_tenant?
|
51
|
+
!rls_used_connections.add?(tenant_id).nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
def reused_connection?(conn)
|
55
|
+
rls_used_connections.add?(conn.object_id).nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
def rls_used_connections
|
59
|
+
if self.class.rls_connection_object_cache_by_thread.nil?
|
60
|
+
self.class.rls_connection_object_cache_by_thread = Set.new([tenant_id])
|
61
|
+
end
|
62
|
+
|
63
|
+
self.class.rls_connection_object_cache_by_thread
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
class Tenant
|
5
|
+
# Switchable Module
|
6
|
+
module Switchable
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
def switch(tenant)
|
10
|
+
set_rls!(tenant)
|
11
|
+
rescue PgRls::Error::TenantNotFound, PgRls::Error::InvalidSearchInput
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def run_within(tenant)
|
16
|
+
set_rls!(tenant)
|
17
|
+
yield(PgRls::Current.tenant).presence
|
18
|
+
ensure
|
19
|
+
PgRls::Tenant.reset_rls
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_tenant!(...)
|
23
|
+
PgRls::Deprecation.warn("This method is deprecated and will be removed in future versions. " \
|
24
|
+
"please use PgRls::Tenant.run_within instead.")
|
25
|
+
run_within(...)
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_rls!(tenant)
|
29
|
+
PgRls::Current.tenant = Searchable.by_rls_object(tenant)
|
30
|
+
raise PgRls::Error::TenantNotFound, "No tenant found for #{tenant}" unless PgRls::Current.tenant.present?
|
31
|
+
|
32
|
+
PgRls::Current.tenant
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset_rls
|
36
|
+
PgRls::Current.reset
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/assets/logo.svg
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
2
|
+
<rect width="200" height="200" fill="#f8f8f8"/>
|
3
|
+
<g transform="translate(20, 20)">
|
4
|
+
<path d="M80 0 L160 40 L160 120 L80 160 L0 120 L0 40 Z" fill="#ff4444"/>
|
5
|
+
<path d="M80 20 L140 52 L140 108 L80 140 L20 108 L20 52 Z" fill="#ffffff"/>
|
6
|
+
<text x="80" y="95" font-family="Arial, sans-serif" font-size="36" font-weight="bold" text-anchor="middle" fill="#ff4444">PgRls</text>
|
7
|
+
</g>
|
8
|
+
</svg>
|
data/docker-compose.yml
ADDED
@@ -1,115 +1,112 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
|
3
|
+
require "rails/generators/active_record/model/model_generator"
|
4
|
+
require_relative "../install/install_generator"
|
5
5
|
|
6
6
|
module PgRls
|
7
7
|
module Generators
|
8
8
|
# Active Record Generator
|
9
9
|
class ActiveRecordGenerator < ::ActiveRecord::Generators::ModelGenerator
|
10
|
-
|
10
|
+
source_root File.expand_path("../templates", __dir__.to_s)
|
11
11
|
|
12
|
-
|
12
|
+
class_option :parent, type: :string, default: "ApplicationRecord",
|
13
|
+
desc: "The parent class for the generated model"
|
13
14
|
|
14
|
-
|
15
|
+
class_option :rls_parent, type: :string, default: "PgRls::Record",
|
16
|
+
desc: "The parent class for the rls generated model"
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@
|
18
|
+
# Need to override so it will not check for class collision
|
19
|
+
def check_class_collision
|
20
|
+
super
|
21
|
+
@class_coalescing = false
|
22
|
+
rescue Rails::Generators::Error
|
23
|
+
@class_coalescing = true
|
20
24
|
end
|
21
25
|
|
22
|
-
def
|
23
|
-
return
|
26
|
+
def create_model_file
|
27
|
+
return if class_coalescing?
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
migration_version:)
|
29
|
+
generate_abstract_class if database && !custom_parent?
|
30
|
+
template("app/models/model.rb", model_path, parent_class_name: parent_class_name)
|
28
31
|
end
|
29
32
|
|
30
|
-
def
|
31
|
-
unless
|
32
|
-
migration_template(convert_migration_template_path,
|
33
|
-
"#{migration_path}/#{convert_file_sub_name}_#{table_name}.rb",
|
34
|
-
migration_version:)
|
35
|
-
end
|
36
|
-
|
37
|
-
return unless installation_in_progress?
|
33
|
+
def upgrade_model_file
|
34
|
+
return unless class_coalescing? && model_file_exists?
|
38
35
|
|
39
|
-
|
40
|
-
"#{migration_path}/pg_rls_backport_#{table_name}.rb",
|
41
|
-
migration_version:)
|
36
|
+
gsub_file(model_path, /< ApplicationRecord/, "< #{parent_class_name}")
|
42
37
|
end
|
43
38
|
|
44
|
-
def
|
45
|
-
return if
|
46
|
-
|
47
|
-
generate_abstract_class if database && !parent
|
39
|
+
def create_migration_file
|
40
|
+
return if skip_migration_creation? || class_coalescing?
|
48
41
|
|
49
|
-
|
42
|
+
clean_indexes_attributes!
|
43
|
+
migration_template "db/migrate/create_#{migration_template_prefix}_table.rb",
|
44
|
+
"db/migrate/create_#{migration_template_prefix}_#{table_name}.rb"
|
50
45
|
end
|
51
46
|
|
52
|
-
def
|
53
|
-
return
|
54
|
-
|
55
|
-
gsub_file(model_file, /Class #{class_name} < #{parent_class_name.classify}/mi) do |match|
|
56
|
-
"#{match}\n def self.current\n PgRls::Tenant.fetch\n end\n"
|
57
|
-
end
|
58
|
-
end
|
47
|
+
def upgrade_migration_file
|
48
|
+
return if skip_migration_creation? || !class_coalescing?
|
59
49
|
|
60
|
-
|
61
|
-
|
50
|
+
migration_template "db/migrate/convert_to_#{migration_template_prefix}_table.rb",
|
51
|
+
"db/migrate/convert_to_#{migration_template_prefix}_#{table_name}.rb"
|
62
52
|
end
|
63
53
|
|
64
|
-
def
|
65
|
-
return
|
54
|
+
def backport_migration_file
|
55
|
+
return if skip_migration_creation? || installing? || !class_coalescing?
|
66
56
|
|
67
|
-
|
57
|
+
migration_template "db/migrate/backport_pg_rls_table.rb",
|
58
|
+
"db/migrate/backport_pg_rls_to_#{table_name}.rb"
|
68
59
|
end
|
69
60
|
|
70
|
-
|
71
|
-
return 'init_convert_migration.rb.tt' if installation_in_progress?
|
61
|
+
private
|
72
62
|
|
73
|
-
|
63
|
+
def installing?
|
64
|
+
Kernel.const_defined?("PgRls::InstallGenerator")
|
74
65
|
end
|
75
66
|
|
76
|
-
def
|
77
|
-
return
|
67
|
+
def parent_class_name
|
68
|
+
return rls_parent unless installing?
|
78
69
|
|
79
|
-
|
70
|
+
super
|
80
71
|
end
|
81
72
|
|
82
|
-
def
|
83
|
-
|
73
|
+
def rls_parent
|
74
|
+
options[:rls_parent]
|
75
|
+
end
|
84
76
|
|
85
|
-
|
77
|
+
def migration_template_prefix
|
78
|
+
installing? ? "pg_rls_tenant" : "pg_rls"
|
86
79
|
end
|
87
80
|
|
88
|
-
def
|
89
|
-
return
|
81
|
+
def generate_abstract_class
|
82
|
+
return if File.exist?(generate_abstract_class_path)
|
90
83
|
|
91
|
-
|
84
|
+
template "app/models/abstract_base_class.rb", generate_abstract_class_path
|
92
85
|
end
|
93
86
|
|
94
|
-
def
|
95
|
-
|
87
|
+
def model_path
|
88
|
+
File.join("app/models", class_path, "#{file_name}.rb")
|
96
89
|
end
|
97
90
|
|
98
|
-
def
|
99
|
-
"
|
91
|
+
def generate_abstract_class_path
|
92
|
+
File.join("app/models", "#{database.underscore}_record.rb")
|
100
93
|
end
|
101
94
|
|
102
|
-
def
|
103
|
-
|
95
|
+
def class_coalescing?
|
96
|
+
@class_coalescing
|
104
97
|
end
|
105
98
|
|
106
|
-
def
|
107
|
-
|
108
|
-
end
|
99
|
+
def clean_indexes_attributes!
|
100
|
+
return unless options[:indexes] == false
|
109
101
|
|
110
|
-
|
102
|
+
attributes.each do |a|
|
103
|
+
a.attr_options.delete(:index) if a.reference? && !a.has_index?
|
104
|
+
end
|
105
|
+
end
|
111
106
|
|
112
|
-
def
|
107
|
+
def model_file_exists?
|
108
|
+
File.exist?(File.join(destination_root, model_path))
|
109
|
+
end
|
113
110
|
end
|
114
111
|
end
|
115
112
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
# Generator to install the gem
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __dir__.to_s)
|
7
|
+
|
8
|
+
attr_reader :attributes
|
9
|
+
|
10
|
+
def initialize(args, *options)
|
11
|
+
pg_rls_config(args.first)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_install_config
|
16
|
+
template "config/initializers/pg_rls.rb"
|
17
|
+
end
|
18
|
+
|
19
|
+
hook_for :orm, required: true
|
20
|
+
|
21
|
+
def show_readme
|
22
|
+
readme "USAGE" if invoke?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def pg_rls_config(tenant_model_or_table)
|
28
|
+
raise "Tenant model or table name is required" if tenant_model_or_table.blank?
|
29
|
+
|
30
|
+
PgRls.class_name = tenant_model_or_table.capitalize.singularize.to_sym
|
31
|
+
PgRls.table_name = tenant_model_or_table.underscore.pluralize.to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
def invoke?
|
35
|
+
behavior == :invoke
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join(File.dirname(__FILE__),
|
3
|
+
require_relative File.join(File.dirname(__FILE__), "active_record/active_record_generator")
|
4
4
|
|
5
5
|
module PgRls
|
6
6
|
module Generators
|
7
|
+
# PgRls Generator
|
7
8
|
class PgRlsGenerator < ::Rails::Generators::NamedBase
|
8
9
|
# override ModelGenerator
|
9
10
|
hook_for :orm, required: true
|
@@ -0,0 +1,28 @@
|
|
1
|
+
===============================================================================
|
2
|
+
|
3
|
+
🎉 Congratulations! The PgRls gem has been successfully installed.
|
4
|
+
|
5
|
+
To generate a model with secure Row-Level Security (RLS), run the following command:
|
6
|
+
|
7
|
+
rails g pg_rls ModelName
|
8
|
+
|
9
|
+
or
|
10
|
+
|
11
|
+
rails generate pg_rls ModelName
|
12
|
+
|
13
|
+
📌 Remember:
|
14
|
+
|
15
|
+
- Replace `ModelName` with the actual name of your model (e.g., `User`, `Post`).
|
16
|
+
- This will set up the necessary RLS policies and table configurations for multi-tenant support.
|
17
|
+
|
18
|
+
🛠 Next Steps:
|
19
|
+
|
20
|
+
1. After generating the model, review the generated migration and model files.
|
21
|
+
2. Ensure your database is configured to support Row-Level Security.
|
22
|
+
3. Run `rails db:migrate` to apply the generated migration.
|
23
|
+
4. Customize the generated policies to fit your application's security needs.
|
24
|
+
|
25
|
+
💡 Tip: Check the documentation at <https://github.com/Dandush03/pg_rls>
|
26
|
+
for more details on how to fine-tune RLS policies.
|
27
|
+
|
28
|
+
===============================================================================
|
@@ -1,7 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
<% module_namespacing do -%>
|
4
|
-
class <%= abstract_class_name %> <
|
2
|
+
class <%= abstract_class_name %> < <%= parent_class_name.classify %>
|
5
3
|
self.abstract_class = true
|
6
4
|
|
7
5
|
connects_to database: { <%= ActiveRecord.writing_role %>: :<%= database -%> }
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# PgRls Configuration
|
4
|
+
# ----------------------
|
5
|
+
# Use this file to configure PgRls to fit your application's needs.
|
6
|
+
# Below are the default settings and examples of how to customize them.
|
7
|
+
#
|
8
|
+
# You can modify these settings as needed and restart your application.
|
9
|
+
|
10
|
+
PgRls.setup do |config|
|
11
|
+
# The main class name used for the RLS tenant model.
|
12
|
+
# This should be the base model representing your tenant (e.g., :organization, :account, etc.).
|
13
|
+
# Default: Derived from your tenant model specified during installation.
|
14
|
+
config.class_name = :<%= PgRls.class_name %>
|
15
|
+
|
16
|
+
# The name of the database table where tenant information is stored.
|
17
|
+
# Ensure this table is properly configured with RLS enabled.
|
18
|
+
# Default: Derived from your tenant model.
|
19
|
+
config.table_name = :<%= PgRls.table_name %>
|
20
|
+
|
21
|
+
# The methods used to search for the tenant. These methods should exist on the tenant model.
|
22
|
+
# Default: Search methods specified during installation.
|
23
|
+
config.search_methods = <%= "%i[#{PgRls.search_methods.join(' ')}]" %>
|
24
|
+
|
25
|
+
# The attributes that should be tracked in the request context.
|
26
|
+
# This allows you to specify which attributes PgRls::Current should manage dynamically.
|
27
|
+
# By default, this is an empty array, as only the current tenant is tracked for RLS purposes.
|
28
|
+
# You can add additional attributes when needed, such as theme, branch, etc.
|
29
|
+
config.current_attributes = []
|
30
|
+
|
31
|
+
# If you need to track additional attributes dynamically, use the `__` convention.
|
32
|
+
# This allows calling nested models with a double underscore, inspired by Stimulus controllers.
|
33
|
+
# Example: If `organization__branch` is added, it will resolve to Organization::Branch.first
|
34
|
+
# config.current_attributes = %i[organization__branch]
|
35
|
+
|
36
|
+
# Database credentials for the RLS user (if needed).
|
37
|
+
# If using Rails credentials, ensure they are correctly stored in config/credentials.yml.enc.
|
38
|
+
# Uncomment and update these lines if you need to provide explicit credentials.
|
39
|
+
# config.username = Rails.application.credentials.dig(:database, :username)
|
40
|
+
# config.password = Rails.application.credentials.dig(:database, :password)
|
41
|
+
|
42
|
+
# The base class from which PgRls::Record should inherit.
|
43
|
+
# This allows your PgRls models to inherit from the same base class as your application models.
|
44
|
+
# For Rails 5+ applications that use ApplicationRecord, this is particularly useful.
|
45
|
+
# Default: 'ActiveRecord::Base'
|
46
|
+
# config.abstract_base_record_class = 'ApplicationRecord'
|
47
|
+
|
48
|
+
# The database role group used for RLS operations.
|
49
|
+
# This role acts as a base role, allowing multiple users to be assigned this role for consistent access control.
|
50
|
+
# Default: "rls_group"
|
51
|
+
# config.rls_role_group = "rls_group"
|
52
|
+
|
53
|
+
# The database schema where your tenant tables are located.
|
54
|
+
# Default: "public"
|
55
|
+
# config.schema = "public"
|
56
|
+
|
57
|
+
# You can add additional custom configurations here if needed.
|
58
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class BackportPgRlsTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def up
|
3
|
+
# Suggested Code:
|
4
|
+
# PgRls.on_each_tenant do |tenant|
|
5
|
+
# <%= class_name.camelize %>.where(identifier_reference_for_tenant: tenant.id)
|
6
|
+
# .in_batches.update_all(tenant_id: tenant.tenant_id)
|
7
|
+
# end
|
8
|
+
end
|
9
|
+
|
10
|
+
def down
|
11
|
+
# Suggested Code:
|
12
|
+
# raise ActiveRecord::IrreversibleMigration, 'This migration is irreversible, please restore from backup.'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class CreatePgRls<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
create_rls_table :<%= table_name %><%= primary_key_type %> do |t|
|
4
|
+
<% attributes.each do |attribute| -%>
|
5
|
+
<% if attribute.password_digest? -%>
|
6
|
+
t.string :password_digest<%= attribute.inject_options %>
|
7
|
+
<% elsif attribute.token? -%>
|
8
|
+
t.string :<%= attribute.name %><%= attribute.inject_options %>
|
9
|
+
<% elsif attribute.reference? -%>
|
10
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
|
11
|
+
<% elsif !attribute.virtual? -%>
|
12
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
13
|
+
<% end -%>
|
14
|
+
<% end -%>
|
15
|
+
<% unless attributes.empty? -%>
|
16
|
+
|
17
|
+
<% end -%>
|
18
|
+
<% if options[:timestamps] -%>
|
19
|
+
t.timestamps
|
20
|
+
<% end -%>
|
21
|
+
end
|
22
|
+
<% attributes.select(&:token?).each do |attribute| -%>
|
23
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
|
24
|
+
<% end -%>
|
25
|
+
<% attributes_with_index.each do |attribute| -%>
|
26
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
27
|
+
<% end -%>
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class CreatePgRlsTenant<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
create_rls_tenant_table :<%= table_name %><%= primary_key_type %> do |t|
|
4
|
+
<% attributes.each do |attribute| -%>
|
5
|
+
<% if attribute.password_digest? -%>
|
6
|
+
t.string :password_digest<%= attribute.inject_options %>
|
7
|
+
<% elsif attribute.token? -%>
|
8
|
+
t.string :<%= attribute.name %><%= attribute.inject_options %>
|
9
|
+
<% elsif attribute.reference? -%>
|
10
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
|
11
|
+
<% elsif !attribute.virtual? -%>
|
12
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
13
|
+
<% end -%>
|
14
|
+
<% end -%>
|
15
|
+
<% unless attributes.empty? -%>
|
16
|
+
|
17
|
+
<% end -%>
|
18
|
+
<% if options[:timestamps] -%>
|
19
|
+
t.timestamps
|
20
|
+
<% end -%>
|
21
|
+
end
|
22
|
+
<% attributes.select(&:token?).each do |attribute| -%>
|
23
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
|
24
|
+
<% end -%>
|
25
|
+
<% attributes_with_index.each do |attribute| -%>
|
26
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
27
|
+
<% end -%>
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
# ActiveRecord ConnectionPool Connection Adapter Extension
|
7
|
+
module ConnectionPool
|
8
|
+
def checkout(checkout_timeout = @checkout_timeout)
|
9
|
+
conn = super
|
10
|
+
return conn unless rls_connection?
|
11
|
+
return reset_rls_used_connections(conn) if PgRls::Current.tenant.nil?
|
12
|
+
|
13
|
+
PgRls::Current.tenant.set_rls(conn)
|
14
|
+
conn
|
15
|
+
end
|
16
|
+
|
17
|
+
def rls_connection?
|
18
|
+
pool_config.db_config.configuration_hash[:rls] == true
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset_rls_used_connections(connection)
|
22
|
+
PgRls::Tenant.reset_rls_used_connections(connection)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(
|
30
|
+
PgRls::ActiveRecord::ConnectionAdapters::ConnectionPool
|
31
|
+
)
|