rls_multi_tenant 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +46 -0
- data/README.md +141 -0
- data/lib/rls_multi_tenant/concerns/multi_tenant.rb +41 -0
- data/lib/rls_multi_tenant/concerns/tenant_context.rb +74 -0
- data/lib/rls_multi_tenant/generators/install/install_generator.rb +91 -0
- data/lib/rls_multi_tenant/generators/install/templates/create_app_user.rb +33 -0
- data/lib/rls_multi_tenant/generators/install/templates/create_tenant.rb +20 -0
- data/lib/rls_multi_tenant/generators/install/templates/db_admin.rake +27 -0
- data/lib/rls_multi_tenant/generators/install/templates/enable_rls.rb +27 -0
- data/lib/rls_multi_tenant/generators/install/templates/enable_uuid_extension.rb +5 -0
- data/lib/rls_multi_tenant/generators/install/templates/rls_multi_tenant.rb +15 -0
- data/lib/rls_multi_tenant/generators/install/templates/tenant_model.rb +7 -0
- data/lib/rls_multi_tenant/generators/migration/migration_generator.rb +57 -0
- data/lib/rls_multi_tenant/generators/migration/templates/create_app_user.rb +33 -0
- data/lib/rls_multi_tenant/generators/migration/templates/create_tenant.rb +20 -0
- data/lib/rls_multi_tenant/generators/migration/templates/enable_rls.rb +27 -0
- data/lib/rls_multi_tenant/generators/migration/templates/enable_uuid_extension.rb +5 -0
- data/lib/rls_multi_tenant/generators/model/model_generator.rb +81 -0
- data/lib/rls_multi_tenant/generators/model/templates/migration.rb +35 -0
- data/lib/rls_multi_tenant/generators/model/templates/model.rb +5 -0
- data/lib/rls_multi_tenant/generators/task/task_generator.rb +31 -0
- data/lib/rls_multi_tenant/generators/task/templates/db_admin.rake +24 -0
- data/lib/rls_multi_tenant/railtie.rb +37 -0
- data/lib/rls_multi_tenant/rls_helper.rb +66 -0
- data/lib/rls_multi_tenant/rls_multi_tenant.rb +45 -0
- data/lib/rls_multi_tenant/security_validator.rb +61 -0
- data/lib/rls_multi_tenant/version.rb +5 -0
- data/lib/rls_multi_tenant.rb +48 -0
- data/rls_multi_tenant.gemspec +44 -0
- metadata +203 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class EnableRlsFor<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
reversible do |dir|
|
|
4
|
+
dir.up do
|
|
5
|
+
# Enable Row Level Security
|
|
6
|
+
execute 'ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY'
|
|
7
|
+
|
|
8
|
+
# Create RLS policy
|
|
9
|
+
execute "CREATE POLICY <%= table_name %>_app_user ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']} USING (<%= RlsMultiTenant.tenant_id_column %> = NULLIF(current_setting('rls.tenant_id', TRUE), '')::uuid)"
|
|
10
|
+
|
|
11
|
+
# Grant permissions
|
|
12
|
+
execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
dir.down do
|
|
16
|
+
# Revoke permissions
|
|
17
|
+
execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
|
|
18
|
+
|
|
19
|
+
# Drop policy
|
|
20
|
+
execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
|
|
21
|
+
|
|
22
|
+
# Disable RLS
|
|
23
|
+
execute "ALTER TABLE <%= table_name %> DISABLE ROW LEVEL SECURITY"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'ostruct'
|
|
5
|
+
|
|
6
|
+
module RlsMultiTenant
|
|
7
|
+
module Generators
|
|
8
|
+
class ModelGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
argument :name, type: :string, desc: "Model name"
|
|
12
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
13
|
+
|
|
14
|
+
def initialize(args, *options)
|
|
15
|
+
super
|
|
16
|
+
@attributes = parse_attributes!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "Generate a multi-tenant model with RLS policies"
|
|
20
|
+
|
|
21
|
+
def create_model_file
|
|
22
|
+
template "model.rb", File.join("app/models", "#{model_name.underscore}.rb")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_migration
|
|
26
|
+
template "migration.rb", File.join("db/migrate", "#{migration_file_name}.rb")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def show_instructions
|
|
30
|
+
say "\n" + "="*60, :green
|
|
31
|
+
say "Multi-tenant model '#{model_name}' created successfully!", :green
|
|
32
|
+
say "="*60, :green
|
|
33
|
+
say "\nWhat was created:", :yellow
|
|
34
|
+
say "1. Model: app/models/#{model_name.underscore}.rb (with MultiTenant concern included)", :yellow
|
|
35
|
+
say "2. Migration: #{migration_file_name}.rb (with tenant_id column and RLS policies)", :yellow
|
|
36
|
+
say "\nNext steps:", :yellow
|
|
37
|
+
say "1. Run migrations: rails db_as:admin[migrate]", :yellow
|
|
38
|
+
say "2. The model is ready to use with multi-tenant functionality", :yellow
|
|
39
|
+
say "3. RLS policies are automatically configured", :yellow
|
|
40
|
+
say "="*60, :green
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def model_name
|
|
46
|
+
@model_name ||= name.classify
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def table_name
|
|
50
|
+
@table_name ||= name.underscore.pluralize
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def migration_file_name
|
|
54
|
+
"#{migration_timestamp}_create_#{table_name}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def migration_timestamp
|
|
58
|
+
Time.current.strftime("%Y%m%d%H%M%S")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def tenant_id_column
|
|
62
|
+
RlsMultiTenant.tenant_id_column
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tenant_class_name
|
|
66
|
+
RlsMultiTenant.tenant_class_name
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def migration_class_name
|
|
70
|
+
"Create#{model_name.pluralize}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_attributes!
|
|
74
|
+
attributes.map do |attr|
|
|
75
|
+
name, type = attr.split(':')
|
|
76
|
+
OpenStruct.new(name: name, type: type || 'string')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
# Create the table
|
|
4
|
+
create_table :<%= table_name %>, id: :uuid do |t|
|
|
5
|
+
t.references :<%= tenant_id_column.to_s.gsub('_id', '') %>, null: false, foreign_key: { to_table: :<%= tenant_class_name.underscore.pluralize %> }, type: :uuid
|
|
6
|
+
<% @attributes.each do |attribute| -%>
|
|
7
|
+
t.<%= attribute.type %> :<%= attribute.name %>
|
|
8
|
+
<% end -%>
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Enable Row Level Security
|
|
14
|
+
execute "ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY"
|
|
15
|
+
|
|
16
|
+
reversible do |dir|
|
|
17
|
+
dir.up do
|
|
18
|
+
execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
|
|
19
|
+
end
|
|
20
|
+
dir.down do
|
|
21
|
+
execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Define RLS policy
|
|
26
|
+
reversible do |dir|
|
|
27
|
+
dir.up do
|
|
28
|
+
execute "CREATE POLICY <%= table_name %>_app_user ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']} USING (<%= tenant_id_column %> = NULLIF(current_setting('rls.<%= tenant_id_column %>', TRUE), '')::uuid)"
|
|
29
|
+
end
|
|
30
|
+
dir.down do
|
|
31
|
+
execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module RlsMultiTenant
|
|
6
|
+
module Generators
|
|
7
|
+
class TaskGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate RLS Multi-tenant rake tasks"
|
|
11
|
+
|
|
12
|
+
def create_db_admin_task
|
|
13
|
+
template "db_admin.rake", "lib/tasks/db_admin.rake"
|
|
14
|
+
show_instructions
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show_instructions
|
|
18
|
+
say "\n" + "="*60, :green
|
|
19
|
+
say "RLS Multi-tenant rake tasks created successfully!", :green
|
|
20
|
+
say "="*60, :green
|
|
21
|
+
say "\nWhat was created:", :yellow
|
|
22
|
+
say "1. lib/tasks/db_admin.rake - Database admin tasks", :yellow
|
|
23
|
+
say "\nUsage:", :yellow
|
|
24
|
+
say "rails db_as:admin[migrate] # Run migrations with admin privileges", :yellow
|
|
25
|
+
say "rails db_as:admin[seed] # Run seeds with admin privileges", :yellow
|
|
26
|
+
say "\nNote: This is required because the app user doesn't have migration privileges", :yellow
|
|
27
|
+
say "="*60, :green
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
namespace :db_as do
|
|
2
|
+
desc "Run a db: task with admin privileges. Usage: `rails db_as:admin[migrate]`"
|
|
3
|
+
task :admin, [:sub_task] do |t, args|
|
|
4
|
+
sub_task = args[:sub_task]
|
|
5
|
+
|
|
6
|
+
# Check if a sub-task was provided
|
|
7
|
+
if sub_task.nil? || !Rake::Task.task_defined?("db:#{sub_task}")
|
|
8
|
+
puts "Usage: rails db_as:admin[sub_task]"
|
|
9
|
+
puts "Valid sub-tasks: #{Rake.application.tasks.map(&:name).select { |name| name.start_with?("db:") }.map { |name| name.split(':', 2).last }.uniq.sort.join(', ')}"
|
|
10
|
+
exit
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
database_url = "postgresql://#{ENV['POSTGRES_USER']}:#{ENV['POSTGRES_PASSWORD']}@#{ENV['POSTGRES_HOST']}:5432/#{ENV['POSTGRES_DB']}"
|
|
14
|
+
|
|
15
|
+
# Set the DATABASE_URL with admin user details.
|
|
16
|
+
ENV['DATABASE_URL'] = database_url
|
|
17
|
+
ENV['CACHE_DATABASE_URL'] = "#{database_url}_cache"
|
|
18
|
+
ENV['QUEUE_DATABASE_URL'] = "#{database_url}_queue"
|
|
19
|
+
ENV['CABLE_DATABASE_URL'] = "#{database_url}_cable"
|
|
20
|
+
|
|
21
|
+
puts "Executing db:#{sub_task} with admin privileges..."
|
|
22
|
+
Rake::Task["db:#{sub_task}"].invoke
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RlsMultiTenant
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
# Load generators after Rails is fully initialized
|
|
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/migration/migration_generator"
|
|
9
|
+
require "rls_multi_tenant/generators/model/model_generator"
|
|
10
|
+
require "rls_multi_tenant/generators/task/task_generator"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "rls_multi_tenant.configure" do |app|
|
|
14
|
+
# Configure the gem
|
|
15
|
+
RlsMultiTenant.configure do |config|
|
|
16
|
+
config.tenant_class_name = "Tenant"
|
|
17
|
+
config.tenant_id_column = :tenant_id
|
|
18
|
+
config.app_user_env_var = "POSTGRES_APP_USER"
|
|
19
|
+
config.enable_security_validation = true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "rls_multi_tenant.security_validation", after: :load_config_initializers do |app|
|
|
24
|
+
if RlsMultiTenant.enable_security_validation
|
|
25
|
+
app.config.after_initialize do
|
|
26
|
+
begin
|
|
27
|
+
RlsMultiTenant::SecurityValidator.validate_environment!
|
|
28
|
+
RlsMultiTenant::SecurityValidator.validate_database_user!
|
|
29
|
+
rescue => e
|
|
30
|
+
Rails.logger.error "RLS Multi-tenant initialization failed: #{e.message}"
|
|
31
|
+
raise e
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RlsMultiTenant
|
|
4
|
+
module RlsHelper
|
|
5
|
+
class << self
|
|
6
|
+
# Enable RLS on a table with a policy
|
|
7
|
+
def enable_rls_for_table(table_name, tenant_column: RlsMultiTenant.tenant_id_column, app_user: nil)
|
|
8
|
+
app_user ||= ENV[RlsMultiTenant.app_user_env_var]
|
|
9
|
+
|
|
10
|
+
raise ConfigurationError, "App user not configured" if app_user.blank?
|
|
11
|
+
|
|
12
|
+
# Enable RLS
|
|
13
|
+
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY")
|
|
14
|
+
|
|
15
|
+
# Create policy (drop if exists first)
|
|
16
|
+
policy_name = "#{table_name}_app_user"
|
|
17
|
+
ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
|
|
18
|
+
|
|
19
|
+
policy_sql = "CREATE POLICY #{policy_name} ON #{table_name} TO #{app_user} " \
|
|
20
|
+
"USING (#{tenant_column} = NULLIF(current_setting('rls.tenant_id', TRUE), '')::uuid)"
|
|
21
|
+
|
|
22
|
+
ActiveRecord::Base.connection.execute(policy_sql)
|
|
23
|
+
|
|
24
|
+
# Grant permissions
|
|
25
|
+
ActiveRecord::Base.connection.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON #{table_name} TO #{app_user}")
|
|
26
|
+
|
|
27
|
+
Rails.logger&.info "✅ RLS enabled for table #{table_name} with policy #{policy_name}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Disable RLS on a table
|
|
31
|
+
def disable_rls_for_table(table_name, app_user: nil)
|
|
32
|
+
app_user ||= ENV[RlsMultiTenant.app_user_env_var]
|
|
33
|
+
|
|
34
|
+
raise ConfigurationError, "App user not configured" if app_user.blank?
|
|
35
|
+
|
|
36
|
+
# Revoke permissions
|
|
37
|
+
ActiveRecord::Base.connection.execute("REVOKE SELECT, INSERT, UPDATE, DELETE ON #{table_name} FROM #{app_user}")
|
|
38
|
+
|
|
39
|
+
# Drop policy
|
|
40
|
+
policy_name = "#{table_name}_app_user"
|
|
41
|
+
ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
|
|
42
|
+
|
|
43
|
+
# Disable RLS
|
|
44
|
+
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} DISABLE ROW LEVEL SECURITY")
|
|
45
|
+
|
|
46
|
+
Rails.logger&.info "✅ RLS disabled for table #{table_name}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if RLS is enabled on a table
|
|
50
|
+
def rls_enabled?(table_name)
|
|
51
|
+
result = ActiveRecord::Base.connection.execute(
|
|
52
|
+
"SELECT relrowsecurity FROM pg_class WHERE relname = '#{table_name}'"
|
|
53
|
+
).first
|
|
54
|
+
|
|
55
|
+
result&.dig('relrowsecurity') == true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get all RLS policies for a table
|
|
59
|
+
def rls_policies(table_name)
|
|
60
|
+
ActiveRecord::Base.connection.execute(
|
|
61
|
+
"SELECT policyname, permissive, roles, cmd, qual FROM pg_policies WHERE tablename = '#{table_name}'"
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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
|
+
|
|
10
|
+
module RlsMultiTenant
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class ConfigurationError < Error; end
|
|
13
|
+
class SecurityError < Error; end
|
|
14
|
+
|
|
15
|
+
# Configuration options
|
|
16
|
+
class << self
|
|
17
|
+
attr_accessor :tenant_class_name, :tenant_id_column, :app_user_env_var, :enable_security_validation
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
yield self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tenant_class
|
|
24
|
+
@tenant_class ||= tenant_class_name.constantize
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tenant_id_column
|
|
28
|
+
@tenant_id_column ||= :tenant_id
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def app_user_env_var
|
|
32
|
+
@app_user_env_var ||= "POSTGRES_APP_USER"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def enable_security_validation
|
|
36
|
+
@enable_security_validation.nil? ? true : @enable_security_validation
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Default configuration
|
|
41
|
+
self.tenant_class_name = "Tenant"
|
|
42
|
+
self.tenant_id_column = :tenant_id
|
|
43
|
+
self.app_user_env_var = "POSTGRES_APP_USER"
|
|
44
|
+
self.enable_security_validation = true
|
|
45
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RlsMultiTenant
|
|
4
|
+
class SecurityValidator
|
|
5
|
+
class << self
|
|
6
|
+
def validate_database_user!
|
|
7
|
+
return unless RlsMultiTenant.enable_security_validation
|
|
8
|
+
return if skip_validation?
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
# Get the current database configuration
|
|
12
|
+
db_config = ActiveRecord::Base.connection_db_config
|
|
13
|
+
username = db_config.configuration_hash[:username]
|
|
14
|
+
|
|
15
|
+
# Check if the current user has SUPERUSER privileges
|
|
16
|
+
superuser_check = ActiveRecord::Base.connection.execute(
|
|
17
|
+
"SELECT rolname, rolsuper FROM pg_roles WHERE rolname = current_user"
|
|
18
|
+
).first
|
|
19
|
+
|
|
20
|
+
if superuser_check && superuser_check['rolsuper']
|
|
21
|
+
raise SecurityError, "Database user '#{username}' has SUPERUSER privileges. " \
|
|
22
|
+
"In order to use RLS Multi-tenant, you must use a non-privileged user without SUPERUSER rights."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Log the security check result
|
|
26
|
+
Rails.logger&.info "✅ RLS Multi-tenant security check passed: Using user '#{username}' without SUPERUSER privileges"
|
|
27
|
+
|
|
28
|
+
rescue => e
|
|
29
|
+
Rails.logger&.error "❌ RLS Multi-tenant security check failed: #{e.message}"
|
|
30
|
+
raise e
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_environment!
|
|
35
|
+
return if skip_validation?
|
|
36
|
+
return unless RlsMultiTenant.enable_security_validation
|
|
37
|
+
|
|
38
|
+
app_user = ENV[RlsMultiTenant.app_user_env_var]
|
|
39
|
+
|
|
40
|
+
if app_user.blank?
|
|
41
|
+
raise ConfigurationError, "#{RlsMultiTenant.app_user_env_var} environment variable must be set"
|
|
42
|
+
elsif ["postgres", "root"].include?(app_user)
|
|
43
|
+
raise SecurityError, "Cannot use privileged PostgreSQL user '#{app_user}'. " \
|
|
44
|
+
"In order to use RLS Multi-tenant, you must use a non-privileged user without SUPERUSER rights."
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def skip_validation?
|
|
51
|
+
# Skip validation if we're running install generator
|
|
52
|
+
return true if ARGV.any? { |arg| arg.include?('rls_multi_tenant:install') }
|
|
53
|
+
|
|
54
|
+
# Skip validation if we're in admin mode (set by db_as:admin task)
|
|
55
|
+
return true if ENV['RLS_MULTI_TENANT_ADMIN_MODE'] == 'true'
|
|
56
|
+
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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/generators/install/install_generator" if defined?(Rails)
|
|
9
|
+
# require "rls_multi_tenant/generators/migration/migration_generator" if defined?(Rails)
|
|
10
|
+
# require "rls_multi_tenant/generators/model/model_generator" if defined?(Rails)
|
|
11
|
+
require "rls_multi_tenant/railtie" if defined?(Rails)
|
|
12
|
+
|
|
13
|
+
module RlsMultiTenant
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class ConfigurationError < Error; end
|
|
16
|
+
class SecurityError < Error; end
|
|
17
|
+
|
|
18
|
+
# Configuration options
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :tenant_class_name, :tenant_id_column, :app_user_env_var, :enable_security_validation
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def tenant_class
|
|
27
|
+
@tenant_class ||= tenant_class_name.constantize
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def tenant_id_column
|
|
31
|
+
@tenant_id_column ||= :tenant_id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def app_user_env_var
|
|
35
|
+
@app_user_env_var ||= "POSTGRES_APP_USER"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def enable_security_validation
|
|
39
|
+
@enable_security_validation.nil? ? true : @enable_security_validation
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Default configuration
|
|
44
|
+
self.tenant_class_name = "Tenant"
|
|
45
|
+
self.tenant_id_column = :tenant_id
|
|
46
|
+
self.app_user_env_var = "POSTGRES_APP_USER"
|
|
47
|
+
self.enable_security_validation = true
|
|
48
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/rls_multi_tenant/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "rls_multi_tenant"
|
|
7
|
+
spec.version = RlsMultiTenant::VERSION
|
|
8
|
+
spec.authors = ["Coding Ways"]
|
|
9
|
+
spec.email = ["info@codingways.com"]
|
|
10
|
+
|
|
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
|
+
|
|
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
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(File.expand_path(f) == __FILE__) ||
|
|
25
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = "exe"
|
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
# Dependencies
|
|
33
|
+
spec.add_dependency "rails", ">= 7.0"
|
|
34
|
+
spec.add_dependency "pg", ">= 1.0"
|
|
35
|
+
|
|
36
|
+
# Development dependencies
|
|
37
|
+
spec.add_development_dependency "rspec-rails", "~> 6.0"
|
|
38
|
+
spec.add_development_dependency "rubocop", "~> 1.50"
|
|
39
|
+
spec.add_development_dependency "rubocop-rails", "~> 2.20"
|
|
40
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.20"
|
|
41
|
+
spec.add_development_dependency "simplecov", "~> 0.22"
|
|
42
|
+
spec.add_development_dependency "generator_spec", "~> 0.9"
|
|
43
|
+
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
|
44
|
+
end
|