rls_multi_tenant 0.2.1 → 0.2.3
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/README.md +62 -62
- data/lib/rls_multi_tenant/concerns/multi_tenant.rb +7 -6
- data/lib/rls_multi_tenant/concerns/tenant_context.rb +14 -17
- data/lib/rls_multi_tenant/generators/install/install_generator.rb +13 -30
- data/lib/rls_multi_tenant/generators/install/templates/rls_multi_tenant.rb +5 -8
- data/lib/rls_multi_tenant/generators/migration/migration_generator.rb +21 -64
- data/lib/rls_multi_tenant/generators/migration/templates/enable_rls.rb +4 -4
- data/lib/rls_multi_tenant/generators/model/model_generator.rb +13 -18
- data/lib/rls_multi_tenant/generators/model/templates/migration.rb +4 -5
- data/lib/rls_multi_tenant/generators/setup/setup_generator.rb +28 -92
- data/lib/rls_multi_tenant/generators/shared/template_helper.rb +1 -1
- data/lib/rls_multi_tenant/middleware/subdomain_tenant_selector.rb +19 -14
- data/lib/rls_multi_tenant/railtie.rb +13 -18
- data/lib/rls_multi_tenant/rls_helper.rb +14 -28
- data/lib/rls_multi_tenant/rls_multi_tenant.rb +19 -13
- data/lib/rls_multi_tenant/security_validator.rb +8 -38
- data/lib/rls_multi_tenant/version.rb +1 -1
- data/lib/rls_multi_tenant.rb +12 -16
- data/rls_multi_tenant.gemspec +26 -26
- metadata +32 -48
- data/lib/rls_multi_tenant/generators/shared/templates/create_app_user.rb +0 -49
- data/lib/rls_multi_tenant/generators/shared/templates/db_admin.rake +0 -27
- data/lib/rls_multi_tenant/generators/task/task_generator.rb +0 -34
|
@@ -7,132 +7,68 @@ module RlsMultiTenant
|
|
|
7
7
|
module Generators
|
|
8
8
|
class SetupGenerator < Rails::Generators::Base
|
|
9
9
|
include Shared::TemplateHelper
|
|
10
|
-
|
|
11
|
-
source_root File.expand_path("templates", __dir__)
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
|
+
|
|
13
|
+
desc 'Setup RLS Multi-tenant gem with tenant model and migrations'
|
|
14
14
|
|
|
15
15
|
def create_tenant_model
|
|
16
16
|
tenant_class_name = RlsMultiTenant.tenant_class_name
|
|
17
17
|
tenant_file_path = "app/models/#{tenant_class_name.underscore}.rb"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
template "tenant_model.rb", tenant_file_path
|
|
21
|
-
else
|
|
18
|
+
|
|
19
|
+
if File.exist?(File.join(destination_root, tenant_file_path))
|
|
22
20
|
say "#{tenant_class_name} model already exists, skipping creation", :yellow
|
|
21
|
+
else
|
|
22
|
+
template 'tenant_model.rb', tenant_file_path
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def create_uuid_migration
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if Dir.glob(File.join(destination_root, 'db/migrate/*_enable_uuid_extension.rb')).any?
|
|
28
|
+
say 'UUID extension migration already exists, skipping creation', :yellow
|
|
29
29
|
else
|
|
30
|
-
|
|
30
|
+
create_migration_with_timestamp('enable_uuid', 1)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def create_app_user_migration
|
|
35
|
-
create_app_user_migrations_for_all_databases
|
|
36
|
-
end
|
|
37
|
-
|
|
38
34
|
def create_tenant_migration
|
|
39
35
|
tenant_class_name = RlsMultiTenant.tenant_class_name
|
|
40
36
|
migration_pattern = "*_create_#{tenant_class_name.underscore.pluralize}.rb"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
create_migration_with_timestamp("create_tenant", 3)
|
|
44
|
-
else
|
|
37
|
+
|
|
38
|
+
if Dir.glob(File.join(destination_root, "db/migrate/#{migration_pattern}")).any?
|
|
45
39
|
say "#{tenant_class_name} migration already exists, skipping creation", :yellow
|
|
40
|
+
else
|
|
41
|
+
create_migration_with_timestamp('create_tenant', 3)
|
|
46
42
|
end
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
def show_instructions
|
|
50
46
|
tenant_class_name = RlsMultiTenant.tenant_class_name
|
|
51
|
-
say "\n
|
|
52
|
-
say
|
|
53
|
-
say
|
|
47
|
+
say "\n#{'=' * 60}", :green
|
|
48
|
+
say 'RLS Multi-tenant setup completed successfully!', :green
|
|
49
|
+
say '=' * 60, :green
|
|
54
50
|
say "\nCreated:", :yellow
|
|
55
51
|
say "• #{tenant_class_name} model", :green
|
|
56
|
-
say
|
|
57
|
-
say "• App user migration(s)", :green
|
|
52
|
+
say '• UUID extension migration', :green
|
|
58
53
|
say "• #{tenant_class_name} migration", :green
|
|
59
54
|
say "\nNext steps:", :yellow
|
|
60
|
-
say "
|
|
61
|
-
say
|
|
62
|
-
say " rails db_as:admin[migrate]", :yellow
|
|
63
|
-
say " Note: We must use the admin user because the app user doesn't have migration privileges", :yellow
|
|
64
|
-
say "\n3. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
|
|
65
|
-
say "="*60, :green
|
|
55
|
+
say "\n1. Use 'rails generate rls_multi_tenant:model <model_name>' for new multi-tenant models", :yellow
|
|
56
|
+
say '=' * 60, :green
|
|
66
57
|
end
|
|
67
58
|
|
|
68
59
|
private
|
|
69
60
|
|
|
70
|
-
def create_app_user_migrations_for_all_databases
|
|
71
|
-
# Get database configuration for current environment
|
|
72
|
-
db_config = Rails.application.config.database_configuration[Rails.env]
|
|
73
|
-
|
|
74
|
-
# Handle both single database and multiple databases configuration
|
|
75
|
-
databases_to_process = if db_config.is_a?(Hash) && db_config.key?('primary')
|
|
76
|
-
# Multiple databases configuration
|
|
77
|
-
db_config
|
|
78
|
-
else
|
|
79
|
-
# Single database configuration - treat as primary
|
|
80
|
-
{ 'primary' => db_config }
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
databases_to_process.each do |db_name, config|
|
|
84
|
-
next if db_name == 'primary' # Skip primary database, handle it separately
|
|
85
|
-
|
|
86
|
-
# Check if migrations_paths is defined for this database
|
|
87
|
-
if config['migrations_paths']
|
|
88
|
-
migration_paths = Array(config['migrations_paths'])
|
|
89
|
-
migration_paths.each do |migration_path|
|
|
90
|
-
migration_dir = File.join(destination_root, migration_path)
|
|
91
|
-
|
|
92
|
-
# Check if migration already exists in this path
|
|
93
|
-
unless Dir.glob(File.join(migration_dir, "*_create_app_user.rb")).any?
|
|
94
|
-
FileUtils.mkdir_p(migration_dir) unless File.directory?(migration_dir)
|
|
95
|
-
create_migration_with_timestamp_for_path("create_app_user", 2, migration_path)
|
|
96
|
-
say "Created app user migration for #{db_name} in #{migration_path}", :green
|
|
97
|
-
else
|
|
98
|
-
say "App user migration already exists for #{db_name} in #{migration_path}, skipping creation", :yellow
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
else
|
|
102
|
-
say "No migrations_paths defined for database '#{db_name}', skipping app user migration", :yellow
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Handle primary database (default behavior)
|
|
107
|
-
unless Dir.glob(File.join(destination_root, "db/migrate/*_create_app_user.rb")).any?
|
|
108
|
-
create_migration_with_timestamp("create_app_user", 2)
|
|
109
|
-
else
|
|
110
|
-
say "App user migration already exists for primary database, skipping creation", :yellow
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
61
|
def create_migration_with_timestamp(migration_type, order)
|
|
115
|
-
base_timestamp = Time.current.strftime(
|
|
116
|
-
timestamp = "#{base_timestamp}#{
|
|
117
|
-
|
|
118
|
-
case migration_type
|
|
119
|
-
when "enable_uuid"
|
|
120
|
-
render_shared_template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
|
|
121
|
-
when "create_app_user"
|
|
122
|
-
render_shared_template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
|
|
123
|
-
when "create_tenant"
|
|
124
|
-
tenant_class_name = RlsMultiTenant.tenant_class_name
|
|
125
|
-
render_shared_template "create_tenant.rb", "db/migrate/#{timestamp}_create_#{tenant_class_name.underscore.pluralize}.rb"
|
|
126
|
-
end
|
|
127
|
-
end
|
|
62
|
+
base_timestamp = Time.current.strftime('%Y%m%d%H%M')
|
|
63
|
+
timestamp = "#{base_timestamp}#{format('%02d', order)}"
|
|
128
64
|
|
|
129
|
-
def create_migration_with_timestamp_for_path(migration_type, order, migration_path)
|
|
130
|
-
base_timestamp = Time.current.strftime("%Y%m%d%H%M")
|
|
131
|
-
timestamp = "#{base_timestamp}#{sprintf('%02d', order)}"
|
|
132
|
-
|
|
133
65
|
case migration_type
|
|
134
|
-
when
|
|
135
|
-
render_shared_template
|
|
66
|
+
when 'enable_uuid'
|
|
67
|
+
render_shared_template 'enable_uuid_extension.rb', "db/migrate/#{timestamp}_enable_uuid_extension.rb"
|
|
68
|
+
when 'create_tenant'
|
|
69
|
+
tenant_class_name = RlsMultiTenant.tenant_class_name
|
|
70
|
+
render_shared_template 'create_tenant.rb',
|
|
71
|
+
"db/migrate/#{timestamp}_create_#{tenant_class_name.underscore.pluralize}.rb"
|
|
136
72
|
end
|
|
137
73
|
end
|
|
138
74
|
end
|
|
@@ -10,10 +10,12 @@ module RlsMultiTenant
|
|
|
10
10
|
def call(env)
|
|
11
11
|
request = ActionDispatch::Request.new(env)
|
|
12
12
|
tenant = resolve_tenant_from_subdomain(request)
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
if tenant
|
|
15
15
|
# Switch tenant context for the duration of the request
|
|
16
|
-
|
|
16
|
+
if defined?(Rails)
|
|
17
|
+
Rails.logger.info "[RLS Multi-Tenant] #{request.method} #{request.path} -> Tenant: #{tenant.name} (#{tenant.id})"
|
|
18
|
+
end
|
|
17
19
|
RlsMultiTenant.tenant_class.switch(tenant) do
|
|
18
20
|
@app.call(env)
|
|
19
21
|
end
|
|
@@ -22,12 +24,17 @@ module RlsMultiTenant
|
|
|
22
24
|
subdomain = extract_subdomain(request.host)
|
|
23
25
|
if subdomain.present? && subdomain != 'www'
|
|
24
26
|
# Subdomain exists but no tenant found - this is an error
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if defined?(Rails)
|
|
28
|
+
Rails.logger.warn "[RLS Multi-Tenant] #{request.method} #{request.path} -> No tenant found for subdomain '#{subdomain}'"
|
|
29
|
+
end
|
|
30
|
+
raise RlsMultiTenant::Error,
|
|
31
|
+
"No tenant found for subdomain '#{subdomain}'. Please ensure the tenant exists with the correct subdomain."
|
|
27
32
|
end
|
|
28
33
|
# If no subdomain, allow access to public models (models without TenantContext)
|
|
29
34
|
# Models that include TenantContext will automatically be constrained by RLS
|
|
30
|
-
|
|
35
|
+
if defined?(Rails)
|
|
36
|
+
Rails.logger.info "[RLS Multi-Tenant] #{request.method} #{request.path} -> Public access (no subdomain)"
|
|
37
|
+
end
|
|
31
38
|
@app.call(env)
|
|
32
39
|
end
|
|
33
40
|
end
|
|
@@ -41,37 +48,35 @@ module RlsMultiTenant
|
|
|
41
48
|
# Look up tenant by subdomain only
|
|
42
49
|
tenant_class = RlsMultiTenant.tenant_class
|
|
43
50
|
subdomain_field = RlsMultiTenant.subdomain_field
|
|
44
|
-
|
|
51
|
+
|
|
45
52
|
# Only allow subdomain-based lookup
|
|
46
53
|
unless tenant_class.column_names.include?(subdomain_field.to_s)
|
|
47
|
-
raise RlsMultiTenant::ConfigurationError,
|
|
54
|
+
raise RlsMultiTenant::ConfigurationError,
|
|
48
55
|
"Subdomain field '#{subdomain_field}' not found on #{tenant_class.name}. " \
|
|
49
56
|
"Please add a '#{subdomain_field}' column to your tenant model or configure a different subdomain_field."
|
|
50
57
|
end
|
|
51
|
-
|
|
58
|
+
|
|
52
59
|
tenant_class.find_by(subdomain_field => subdomain)
|
|
53
|
-
rescue => e
|
|
60
|
+
rescue StandardError => e
|
|
54
61
|
Rails.logger.error "Failed to resolve tenant from subdomain '#{subdomain}': #{e.message}" if defined?(Rails)
|
|
55
62
|
nil
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def extract_subdomain(host)
|
|
59
66
|
return nil if host.blank?
|
|
60
|
-
|
|
67
|
+
|
|
61
68
|
# Remove port if present
|
|
62
69
|
host = host.split(':').first
|
|
63
|
-
|
|
70
|
+
|
|
64
71
|
# Split by dots and get the first part (subdomain)
|
|
65
72
|
parts = host.split('.')
|
|
66
|
-
|
|
73
|
+
|
|
67
74
|
# Handle localhost development (e.g., foo.localhost:3000)
|
|
68
75
|
if parts.length == 2 && parts.last == 'localhost'
|
|
69
76
|
parts.first
|
|
70
77
|
# Handle standard domains (e.g., foo.example.com)
|
|
71
78
|
elsif parts.length >= 3
|
|
72
79
|
parts.first
|
|
73
|
-
else
|
|
74
|
-
nil
|
|
75
80
|
end
|
|
76
81
|
end
|
|
77
82
|
end
|
|
@@ -3,39 +3,34 @@
|
|
|
3
3
|
module RlsMultiTenant
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
5
|
# Load generators after Rails is fully initialized
|
|
6
|
-
initializer
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
11
|
-
require "rls_multi_tenant/generators/task/task_generator"
|
|
6
|
+
initializer 'rls_multi_tenant.load_generators', after: :set_routes_reloader do |_app|
|
|
7
|
+
require 'rls_multi_tenant/generators/install/install_generator'
|
|
8
|
+
require 'rls_multi_tenant/generators/setup/setup_generator'
|
|
9
|
+
require 'rls_multi_tenant/generators/migration/migration_generator'
|
|
10
|
+
require 'rls_multi_tenant/generators/model/model_generator'
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
initializer
|
|
13
|
+
initializer 'rls_multi_tenant.configure' do |_app|
|
|
15
14
|
# Configure the gem
|
|
16
15
|
RlsMultiTenant.configure do |config|
|
|
17
|
-
config.tenant_class_name =
|
|
16
|
+
config.tenant_class_name = 'Tenant'
|
|
18
17
|
config.tenant_id_column = :tenant_id
|
|
19
|
-
config.app_user_env_var = "POSTGRES_APP_USER"
|
|
20
18
|
config.enable_security_validation = true
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
initializer
|
|
22
|
+
initializer 'rls_multi_tenant.security_validation', after: :load_config_initializers do |app|
|
|
25
23
|
if RlsMultiTenant.enable_security_validation
|
|
26
24
|
app.config.after_initialize do
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Rails.logger.error "RLS Multi-tenant initialization failed: #{e.message}"
|
|
32
|
-
raise e
|
|
33
|
-
end
|
|
25
|
+
RlsMultiTenant::SecurityValidator.validate_database_user!
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
Rails.logger.error "RLS Multi-tenant initialization failed: #{e.message}"
|
|
28
|
+
raise e
|
|
34
29
|
end
|
|
35
30
|
end
|
|
36
31
|
end
|
|
37
32
|
|
|
38
|
-
initializer
|
|
33
|
+
initializer 'rls_multi_tenant.middleware', after: :load_config_initializers do |app|
|
|
39
34
|
if RlsMultiTenant.enable_subdomain_middleware
|
|
40
35
|
app.config.middleware.use RlsMultiTenant::Middleware::SubdomainTenantSelector
|
|
41
36
|
end
|
|
@@ -4,46 +4,32 @@ module RlsMultiTenant
|
|
|
4
4
|
module RlsHelper
|
|
5
5
|
class << self
|
|
6
6
|
# Enable RLS on a table with a policy
|
|
7
|
-
def enable_rls_for_table(table_name, tenant_column: RlsMultiTenant.tenant_id_column
|
|
8
|
-
app_user ||= ENV[RlsMultiTenant.app_user_env_var]
|
|
9
|
-
|
|
10
|
-
raise ConfigurationError, "App user not configured" if app_user.blank?
|
|
11
|
-
|
|
7
|
+
def enable_rls_for_table(table_name, tenant_column: RlsMultiTenant.tenant_id_column)
|
|
12
8
|
# Enable RLS
|
|
13
|
-
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY")
|
|
14
|
-
|
|
9
|
+
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY")
|
|
10
|
+
|
|
15
11
|
# Create policy (drop if exists first)
|
|
16
12
|
policy_name = "#{table_name}_app_user"
|
|
17
13
|
ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
|
|
18
|
-
|
|
14
|
+
|
|
19
15
|
tenant_session_var = "rls.#{RlsMultiTenant.tenant_id_column}"
|
|
20
|
-
policy_sql = "CREATE POLICY #{policy_name} ON #{table_name}
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
policy_sql = "CREATE POLICY #{policy_name} ON #{table_name} " \
|
|
17
|
+
"USING (#{tenant_column} = NULLIF(current_setting('#{tenant_session_var}', TRUE), '')::uuid)"
|
|
18
|
+
|
|
23
19
|
ActiveRecord::Base.connection.execute(policy_sql)
|
|
24
|
-
|
|
25
|
-
# Grant permissions
|
|
26
|
-
ActiveRecord::Base.connection.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON #{table_name} TO #{app_user}")
|
|
27
|
-
|
|
20
|
+
|
|
28
21
|
Rails.logger&.info "✅ RLS enabled for table #{table_name} with policy #{policy_name}"
|
|
29
22
|
end
|
|
30
23
|
|
|
31
24
|
# Disable RLS on a table
|
|
32
|
-
def disable_rls_for_table(table_name
|
|
33
|
-
app_user ||= ENV[RlsMultiTenant.app_user_env_var]
|
|
34
|
-
|
|
35
|
-
raise ConfigurationError, "App user not configured" if app_user.blank?
|
|
36
|
-
|
|
37
|
-
# Revoke permissions
|
|
38
|
-
ActiveRecord::Base.connection.execute("REVOKE SELECT, INSERT, UPDATE, DELETE ON #{table_name} FROM #{app_user}")
|
|
39
|
-
|
|
25
|
+
def disable_rls_for_table(table_name)
|
|
40
26
|
# Drop policy
|
|
41
27
|
policy_name = "#{table_name}_app_user"
|
|
42
28
|
ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
|
|
43
|
-
|
|
29
|
+
|
|
44
30
|
# Disable RLS
|
|
45
|
-
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} DISABLE ROW LEVEL SECURITY")
|
|
46
|
-
|
|
31
|
+
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} DISABLE ROW LEVEL SECURITY, NO FORCE ROW LEVEL SECURITY")
|
|
32
|
+
|
|
47
33
|
Rails.logger&.info "✅ RLS disabled for table #{table_name}"
|
|
48
34
|
end
|
|
49
35
|
|
|
@@ -52,8 +38,8 @@ module RlsMultiTenant
|
|
|
52
38
|
result = ActiveRecord::Base.connection.execute(
|
|
53
39
|
"SELECT relrowsecurity FROM pg_class WHERE relname = '#{table_name}'"
|
|
54
40
|
).first
|
|
55
|
-
|
|
56
|
-
result&.dig('relrowsecurity') == true
|
|
41
|
+
|
|
42
|
+
result&.dig('relrowsecurity') == true && result&.dig('relforcerowsecurity') == true
|
|
57
43
|
end
|
|
58
44
|
|
|
59
45
|
# Get all RLS policies for a table
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
3
|
+
require 'rls_multi_tenant/version'
|
|
4
|
+
require 'rls_multi_tenant/concerns/multi_tenant'
|
|
5
|
+
require 'rls_multi_tenant/concerns/tenant_context'
|
|
6
|
+
require 'rls_multi_tenant/security_validator'
|
|
7
|
+
require 'rls_multi_tenant/rls_helper'
|
|
8
|
+
require 'rls_multi_tenant/railtie' if defined?(Rails)
|
|
9
9
|
|
|
10
10
|
module RlsMultiTenant
|
|
11
11
|
class Error < StandardError; end
|
|
@@ -14,7 +14,8 @@ module RlsMultiTenant
|
|
|
14
14
|
|
|
15
15
|
# Configuration options
|
|
16
16
|
class << self
|
|
17
|
-
attr_accessor :tenant_class_name, :tenant_id_column, :
|
|
17
|
+
attr_accessor :tenant_class_name, :tenant_id_column, :enable_security_validation, :enable_subdomain_middleware,
|
|
18
|
+
:subdomain_field
|
|
18
19
|
|
|
19
20
|
def configure
|
|
20
21
|
yield self
|
|
@@ -28,18 +29,23 @@ module RlsMultiTenant
|
|
|
28
29
|
@tenant_id_column ||= :tenant_id
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def
|
|
32
|
-
@
|
|
32
|
+
def enable_security_validation
|
|
33
|
+
@enable_security_validation.nil? || @enable_security_validation
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
-
@
|
|
36
|
+
def enable_subdomain_middleware
|
|
37
|
+
@enable_subdomain_middleware.nil? ? false : @enable_subdomain_middleware
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def subdomain_field
|
|
41
|
+
@subdomain_field ||= :subdomain
|
|
37
42
|
end
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
# Default configuration
|
|
41
|
-
self.tenant_class_name =
|
|
46
|
+
self.tenant_class_name = 'Tenant'
|
|
42
47
|
self.tenant_id_column = :tenant_id
|
|
43
|
-
self.app_user_env_var = "POSTGRES_APP_USER"
|
|
44
48
|
self.enable_security_validation = true
|
|
49
|
+
self.enable_subdomain_middleware = true
|
|
50
|
+
self.subdomain_field = :subdomain
|
|
45
51
|
end
|
|
@@ -5,59 +5,29 @@ module RlsMultiTenant
|
|
|
5
5
|
class << self
|
|
6
6
|
def validate_database_user!
|
|
7
7
|
return unless RlsMultiTenant.enable_security_validation
|
|
8
|
-
return if skip_validation?
|
|
9
8
|
|
|
10
9
|
begin
|
|
11
10
|
# Get the current database configuration
|
|
12
11
|
db_config = ActiveRecord::Base.connection_db_config
|
|
13
12
|
username = db_config.configuration_hash[:username]
|
|
14
13
|
|
|
15
|
-
# Check if the
|
|
16
|
-
|
|
17
|
-
"SELECT
|
|
14
|
+
# Check if the user has bypassrls privilege
|
|
15
|
+
bypassrls_check = ActiveRecord::Base.connection.execute(
|
|
16
|
+
"SELECT rolbypassrls FROM pg_roles WHERE rolname = '#{username}'"
|
|
18
17
|
).first
|
|
19
18
|
|
|
20
|
-
if
|
|
21
|
-
raise SecurityError, "Database user '#{username}' has
|
|
22
|
-
|
|
23
|
-
"Did you remember to edit database.yml in order to use the POSTGRES_APP_USER and POSTGRES_APP_PASSWORD?"
|
|
19
|
+
if bypassrls_check && bypassrls_check['rolbypassrls']
|
|
20
|
+
raise SecurityError, "Database user '#{username}' has BYPASSRLS privilege. " \
|
|
21
|
+
'In order to use RLS Multi-tenant, you must use a non-privileged user without BYPASSRLS privilege.'
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
# Log the security check result
|
|
27
|
-
Rails.logger&.info "✅ RLS Multi-tenant security check passed: Using user '#{username}' without
|
|
28
|
-
|
|
29
|
-
rescue => e
|
|
25
|
+
Rails.logger&.info "✅ RLS Multi-tenant security check passed: Using user '#{username}' without BYPASSRLS privilege"
|
|
26
|
+
rescue StandardError => e
|
|
30
27
|
Rails.logger&.error "❌ RLS Multi-tenant security check failed: #{e.message}"
|
|
31
28
|
raise e
|
|
32
29
|
end
|
|
33
30
|
end
|
|
34
|
-
|
|
35
|
-
def validate_environment!
|
|
36
|
-
return if skip_validation?
|
|
37
|
-
return unless RlsMultiTenant.enable_security_validation
|
|
38
|
-
|
|
39
|
-
app_user = ENV[RlsMultiTenant.app_user_env_var]
|
|
40
|
-
|
|
41
|
-
if app_user.blank?
|
|
42
|
-
raise ConfigurationError, "#{RlsMultiTenant.app_user_env_var} environment variable must be set"
|
|
43
|
-
elsif ["postgres", "root"].include?(app_user)
|
|
44
|
-
raise SecurityError, "Cannot use privileged PostgreSQL user '#{app_user}'. " \
|
|
45
|
-
"In order to use RLS Multi-tenant, you must use a non-privileged user without SUPERUSER rights." \
|
|
46
|
-
"Did you remember to edit database.yml in order to use the POSTGRES_APP_USER and POSTGRES_APP_PASSWORD?"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def skip_validation?
|
|
53
|
-
# Skip validation if we're running an install or setup generator
|
|
54
|
-
return true if ARGV.any? { |arg| arg.include?('rls_multi_tenant:install') || arg.include?('rls_multi_tenant:setup') }
|
|
55
|
-
|
|
56
|
-
# Skip validation if we're in admin mode (set by db_as:admin task)
|
|
57
|
-
return true if ENV['RLS_MULTI_TENANT_ADMIN_MODE'] == 'true'
|
|
58
|
-
|
|
59
|
-
false
|
|
60
|
-
end
|
|
61
31
|
end
|
|
62
32
|
end
|
|
63
33
|
end
|
data/lib/rls_multi_tenant.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
3
|
+
require 'rls_multi_tenant/version'
|
|
4
|
+
require 'rls_multi_tenant/concerns/multi_tenant'
|
|
5
|
+
require 'rls_multi_tenant/concerns/tenant_context'
|
|
6
|
+
require 'rls_multi_tenant/security_validator'
|
|
7
|
+
require 'rls_multi_tenant/rls_helper'
|
|
8
|
+
require 'rls_multi_tenant/middleware/subdomain_tenant_selector'
|
|
9
|
+
require 'rls_multi_tenant/generators/shared/template_helper'
|
|
10
|
+
require 'rls_multi_tenant/railtie' if defined?(Rails)
|
|
11
11
|
|
|
12
12
|
module RlsMultiTenant
|
|
13
13
|
class Error < StandardError; end
|
|
@@ -16,7 +16,8 @@ module RlsMultiTenant
|
|
|
16
16
|
|
|
17
17
|
# Configuration options
|
|
18
18
|
class << self
|
|
19
|
-
attr_accessor :tenant_class_name, :tenant_id_column, :
|
|
19
|
+
attr_accessor :tenant_class_name, :tenant_id_column, :enable_security_validation, :enable_subdomain_middleware,
|
|
20
|
+
:subdomain_field
|
|
20
21
|
|
|
21
22
|
def configure
|
|
22
23
|
yield self
|
|
@@ -30,12 +31,8 @@ module RlsMultiTenant
|
|
|
30
31
|
@tenant_id_column ||= :tenant_id
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def app_user_env_var
|
|
34
|
-
@app_user_env_var ||= "POSTGRES_APP_USER"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
34
|
def enable_security_validation
|
|
38
|
-
@enable_security_validation.nil?
|
|
35
|
+
@enable_security_validation.nil? || @enable_security_validation
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
def enable_subdomain_middleware
|
|
@@ -48,9 +45,8 @@ module RlsMultiTenant
|
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
# Default configuration
|
|
51
|
-
self.tenant_class_name =
|
|
48
|
+
self.tenant_class_name = 'Tenant'
|
|
52
49
|
self.tenant_id_column = :tenant_id
|
|
53
|
-
self.app_user_env_var = "POSTGRES_APP_USER"
|
|
54
50
|
self.enable_security_validation = true
|
|
55
51
|
self.enable_subdomain_middleware = true
|
|
56
52
|
self.subdomain_field = :subdomain
|
data/rls_multi_tenant.gemspec
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'lib/rls_multi_tenant/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = 'rls_multi_tenant'
|
|
7
7
|
spec.version = RlsMultiTenant::VERSION
|
|
8
|
-
spec.authors = [
|
|
9
|
-
spec.email = [
|
|
8
|
+
spec.authors = ['Coding Ways']
|
|
9
|
+
spec.email = ['info@codingways.com']
|
|
10
10
|
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.description =
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
15
|
-
spec.required_ruby_version =
|
|
11
|
+
spec.summary = 'Rails gem for PostgreSQL Row Level Security (RLS) multi-tenant applications'
|
|
12
|
+
spec.description = 'A comprehensive gem that provides RLS-based multi-tenancy for Rails applications using PostgreSQL, including automatic tenant context switching, security validations, and migration helpers.'
|
|
13
|
+
spec.homepage = 'https://github.com/codingways/rls_multi_tenant'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
16
16
|
|
|
17
|
-
spec.metadata[
|
|
18
|
-
spec.metadata[
|
|
19
|
-
spec.metadata[
|
|
20
|
-
spec.metadata[
|
|
21
|
-
spec.metadata[
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/codingways/rls_multi_tenant'
|
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/codingways/rls_multi_tenant/blob/main/CHANGELOG.md'
|
|
20
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/codingways/rls_multi_tenant/issues'
|
|
21
|
+
spec.metadata['documentation_uri'] = 'https://github.com/codingways/rls_multi_tenant#readme'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
22
23
|
|
|
23
24
|
# Specify which files should be added to the gem when it is released.
|
|
24
25
|
spec.files = Dir.chdir(__dir__) do
|
|
@@ -27,21 +28,20 @@ Gem::Specification.new do |spec|
|
|
|
27
28
|
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
|
-
spec.bindir =
|
|
31
|
+
spec.bindir = 'exe'
|
|
31
32
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
|
-
spec.require_paths = [
|
|
33
|
+
spec.require_paths = ['lib']
|
|
33
34
|
|
|
34
35
|
# Dependencies
|
|
35
|
-
spec.add_dependency
|
|
36
|
-
spec.add_dependency
|
|
37
|
-
spec.add_dependency "ostruct", ">= 0.1.0"
|
|
36
|
+
spec.add_dependency 'pg', '>= 1.0', '< 3.0'
|
|
37
|
+
spec.add_dependency 'rails', '>= 6.0', '< 9.0'
|
|
38
38
|
|
|
39
39
|
# Development dependencies
|
|
40
|
-
spec.add_development_dependency
|
|
41
|
-
spec.add_development_dependency
|
|
42
|
-
spec.add_development_dependency
|
|
43
|
-
spec.add_development_dependency
|
|
44
|
-
spec.add_development_dependency
|
|
45
|
-
spec.add_development_dependency
|
|
46
|
-
spec.add_development_dependency
|
|
40
|
+
spec.add_development_dependency 'bundler-audit', '~> 0.9'
|
|
41
|
+
spec.add_development_dependency 'generator_spec', '~> 0.9'
|
|
42
|
+
spec.add_development_dependency 'rspec-rails', '~> 6.1.0'
|
|
43
|
+
spec.add_development_dependency 'rubocop', '~> 1.80.0'
|
|
44
|
+
spec.add_development_dependency 'rubocop-rails', '~> 2.33'
|
|
45
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 3.7'
|
|
46
|
+
spec.add_development_dependency 'simplecov', '~> 0.22'
|
|
47
47
|
end
|