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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +46 -0
  3. data/README.md +141 -0
  4. data/lib/rls_multi_tenant/concerns/multi_tenant.rb +41 -0
  5. data/lib/rls_multi_tenant/concerns/tenant_context.rb +74 -0
  6. data/lib/rls_multi_tenant/generators/install/install_generator.rb +91 -0
  7. data/lib/rls_multi_tenant/generators/install/templates/create_app_user.rb +33 -0
  8. data/lib/rls_multi_tenant/generators/install/templates/create_tenant.rb +20 -0
  9. data/lib/rls_multi_tenant/generators/install/templates/db_admin.rake +27 -0
  10. data/lib/rls_multi_tenant/generators/install/templates/enable_rls.rb +27 -0
  11. data/lib/rls_multi_tenant/generators/install/templates/enable_uuid_extension.rb +5 -0
  12. data/lib/rls_multi_tenant/generators/install/templates/rls_multi_tenant.rb +15 -0
  13. data/lib/rls_multi_tenant/generators/install/templates/tenant_model.rb +7 -0
  14. data/lib/rls_multi_tenant/generators/migration/migration_generator.rb +57 -0
  15. data/lib/rls_multi_tenant/generators/migration/templates/create_app_user.rb +33 -0
  16. data/lib/rls_multi_tenant/generators/migration/templates/create_tenant.rb +20 -0
  17. data/lib/rls_multi_tenant/generators/migration/templates/enable_rls.rb +27 -0
  18. data/lib/rls_multi_tenant/generators/migration/templates/enable_uuid_extension.rb +5 -0
  19. data/lib/rls_multi_tenant/generators/model/model_generator.rb +81 -0
  20. data/lib/rls_multi_tenant/generators/model/templates/migration.rb +35 -0
  21. data/lib/rls_multi_tenant/generators/model/templates/model.rb +5 -0
  22. data/lib/rls_multi_tenant/generators/task/task_generator.rb +31 -0
  23. data/lib/rls_multi_tenant/generators/task/templates/db_admin.rake +24 -0
  24. data/lib/rls_multi_tenant/railtie.rb +37 -0
  25. data/lib/rls_multi_tenant/rls_helper.rb +66 -0
  26. data/lib/rls_multi_tenant/rls_multi_tenant.rb +45 -0
  27. data/lib/rls_multi_tenant/security_validator.rb +61 -0
  28. data/lib/rls_multi_tenant/version.rb +5 -0
  29. data/lib/rls_multi_tenant.rb +48 -0
  30. data/rls_multi_tenant.gemspec +44 -0
  31. metadata +203 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e90e7f280c785aa09f9f643da2420a38423f888592a750d4587dcc961ede1de1
4
+ data.tar.gz: cb1ec28221f70d26bcf6444be404feccf6098417325921692f8c95ba4d945a3c
5
+ SHA512:
6
+ metadata.gz: e28c40e201630a87955c890b4f417d43c0e7418674a6bf776d44cd4a39b28cb6c49b4873c6807e0e21247b8e120682a986e1bccf95cdc47d1be29f2b63c213c6
7
+ data.tar.gz: 986900e03f1234f1f19813f1680af7675861c35a437b64582512854bdc9f7d05828a78a6bf6755f656a2b05fa7e47447312c83d53796bea4929962f1ed18b70c
data/.rubocop.yml ADDED
@@ -0,0 +1,46 @@
1
+ require:
2
+ - rubocop-rails
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.0
7
+ NewCops: enable
8
+ Exclude:
9
+ - 'vendor/**/*'
10
+ - 'spec/tmp/**/*'
11
+ - 'tmp/**/*'
12
+
13
+ Style/Documentation:
14
+ Enabled: false
15
+
16
+ Style/StringLiterals:
17
+ EnforcedStyle: single_quotes
18
+
19
+ Style/FrozenStringLiteralComment:
20
+ Enabled: true
21
+
22
+ Style/ClassAndModuleChildren:
23
+ Enabled: false
24
+
25
+ Metrics/LineLength:
26
+ Max: 120
27
+
28
+ Metrics/BlockLength:
29
+ Exclude:
30
+ - 'spec/**/*'
31
+ - 'lib/rls_multi_tenant/generators/**/*'
32
+
33
+ RSpec/ExampleLength:
34
+ Max: 20
35
+
36
+ RSpec/MultipleExpectations:
37
+ Max: 5
38
+
39
+ RSpec/NestedGroups:
40
+ Max: 4
41
+
42
+ RSpec/DescribeClass:
43
+ Enabled: false
44
+
45
+ RSpec/DescribeMethod:
46
+ Enabled: false
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # RLS Multi-Tenant
2
+
3
+ A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenancy for Rails applications.
4
+
5
+ ## Features
6
+
7
+ - 🔒 **Row Level Security**: Automatic tenant isolation using PostgreSQL RLS
8
+ - 🛡️ **Security Validation**: Prevents running with privileged database users
9
+ - 🔄 **Context Switching**: Easy tenant context management
10
+ - 📦 **Auto-inclusion**: Automatic model configuration
11
+ - 🚀 **Generators**: Rails generators for quick setup
12
+ - ⚙️ **Configurable**: Flexible configuration options
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'rls_multi_tenant', path: 'gems/rls_multi_tenant'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ 1. **Install the gem configuration:**
31
+ ```bash
32
+ rails generate rls_multi_tenant:install
33
+ ```
34
+
35
+ 2. **Configure environment variables:**
36
+ ```bash
37
+ POSTGRES_USER=your_admin_user # This is the user that will run the migrations
38
+ POSTGRES_PASSWORD=your_admin_user_password
39
+ POSTGRES_APP_USER=your_app_user # This is the user that will run the app
40
+ POSTGRES_APP_PASSWORD=your_password
41
+ ```
42
+
43
+ 3. **Run migrations:**
44
+ ```bash
45
+ rails db_as:admin[migrate] # Custom rake task to run migrations with admin privileges
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### Basic Multi-Tenant Models
51
+
52
+ Create a new model:
53
+ ```bash
54
+ rails generate rls_multi_tenant:model User name email
55
+ ```
56
+
57
+ Your models automatically include the `MultiTenant` concern:
58
+
59
+ ```ruby
60
+ class User < ApplicationRecord
61
+ # Automatically includes MultiTenant concern
62
+ # include RlsMultiTenant::Concerns::MultiTenant
63
+ end
64
+ ```
65
+
66
+ ### Tenant Context Switching
67
+
68
+ ```ruby
69
+ # Create a new tenant
70
+ tenant = Tenant.create!(name: "Tenant 1")
71
+ ```
72
+
73
+ ```ruby
74
+ # Switch tenant context for a block
75
+ Tenant.switch(tenant) do
76
+ User.create!(name: "User from Tenant 1", email: "user@example.com") # Automatically assigned to current tenant
77
+ end
78
+
79
+ # Switch tenant context permanently
80
+ Tenant.switch!(tenant)
81
+ User.create!(name: "User from Tenant 1", email: "user@example.com")
82
+ Tenant.reset! # Reset context
83
+
84
+ # Get current tenant
85
+ current_tenant = Tenant.current
86
+ ```
87
+
88
+ ## Configuration
89
+
90
+ Configure the gem in `config/initializers/rls_multi_tenant.rb`:
91
+
92
+ ```ruby
93
+ RlsMultiTenant.configure do |config|
94
+ config.tenant_class_name = "Tenant" # Your tenant model class
95
+ config.tenant_id_column = :tenant_id # Tenant ID column name
96
+ config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
97
+ config.enable_security_validation = true # Enable security checks. This will check if the app user is set without superuser privileges.
98
+ end
99
+ ```
100
+
101
+ ### Database Admin Task
102
+ ```bash
103
+ # Run migrations with admin privileges (required because app user can't run migrations)
104
+ rails db_as:admin[migrate]
105
+
106
+ # Run seeds with admin privileges
107
+ rails db_as:admin[seed]
108
+
109
+ # Create database with admin privileges
110
+ rails db_as:admin[create]
111
+ ```
112
+
113
+ ## Security Features
114
+
115
+ The gem includes multiple security layers:
116
+
117
+ 1. **Environment Validation**: Ensures `POSTGRES_APP_USER` is set and not privileged
118
+ 2. **Database User Validation**: Checks that the database user doesn't have SUPERUSER privileges
119
+ 3. **RLS Policies**: Automatic tenant isolation at the database level
120
+
121
+ ## Requirements
122
+
123
+ - Rails 7.0+
124
+ - PostgreSQL 12+ (with UUID extension support)
125
+ - Ruby 3.0+
126
+
127
+ ## UUID Support
128
+
129
+ This gem uses UUIDs for the tenant model by default to ensure proper multi-tenant isolation. The `enable_uuid` migration must be run before creating tenant tables.
130
+
131
+ ## Contributing
132
+
133
+ 1. Fork the repository
134
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
135
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
136
+ 4. Push to the branch (`git push origin my-new-feature`)
137
+ 5. Create a Pull Request
138
+
139
+ ## License
140
+
141
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ module Concerns
5
+ module MultiTenant
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ belongs_to :tenant, class_name: RlsMultiTenant.tenant_class_name, foreign_key: RlsMultiTenant.tenant_id_column
10
+
11
+ validates RlsMultiTenant.tenant_id_column, presence: true
12
+
13
+ before_validation :set_tenant_id
14
+
15
+ private
16
+
17
+ def set_tenant_id
18
+ current_tenant = RlsMultiTenant.tenant_class.current
19
+ if current_tenant && self.send(RlsMultiTenant.tenant_id_column).blank?
20
+ self.send("#{RlsMultiTenant.tenant_id_column}=", current_tenant.id)
21
+ end
22
+ end
23
+ end
24
+
25
+ class_methods do
26
+ private
27
+
28
+ def extract_tenant_id(tenant_or_id)
29
+ case tenant_or_id
30
+ when RlsMultiTenant.tenant_class
31
+ tenant_or_id.id
32
+ when String, Integer
33
+ tenant_or_id
34
+ else
35
+ raise ArgumentError, "Expected #{RlsMultiTenant.tenant_class_name} object or tenant_id, got #{tenant_or_id.class}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ module Concerns
5
+ module TenantContext
6
+ extend ActiveSupport::Concern
7
+
8
+ SET_TENANT_ID_SQL = 'SET rls.tenant_id = %s'.freeze
9
+ RESET_TENANT_ID_SQL = 'RESET rls.tenant_id'.freeze
10
+
11
+ class_methods do
12
+ # Switch tenant context for a block
13
+ def switch(tenant_or_id)
14
+ tenant_id = extract_tenant_id(tenant_or_id)
15
+ connection.execute format(SET_TENANT_ID_SQL, connection.quote(tenant_id))
16
+ yield
17
+ ensure
18
+ reset!
19
+ end
20
+
21
+ # Switch tenant context permanently (until reset)
22
+ def switch!(tenant_or_id)
23
+ tenant_id = extract_tenant_id(tenant_or_id)
24
+ connection.execute format(SET_TENANT_ID_SQL, connection.quote(tenant_id))
25
+ end
26
+
27
+ # Reset tenant context
28
+ def reset!
29
+ connection.execute RESET_TENANT_ID_SQL
30
+ end
31
+
32
+ # Get current tenant from context
33
+ def current
34
+ return nil unless connection.active?
35
+
36
+ result = connection.execute("SHOW rls.tenant_id")
37
+ tenant_id = result.first&.dig('rls.tenant_id')
38
+
39
+ return nil if tenant_id.blank?
40
+
41
+ RlsMultiTenant.tenant_class.find_by(id: tenant_id)
42
+ rescue ActiveRecord::StatementInvalid, PG::Error
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def extract_tenant_id(tenant_or_id)
49
+ case tenant_or_id
50
+ when RlsMultiTenant.tenant_class
51
+ tenant_or_id.id
52
+ when String, Integer
53
+ tenant_or_id
54
+ else
55
+ raise ArgumentError, "Expected #{RlsMultiTenant.tenant_class_name} object or tenant_id, got #{tenant_or_id.class}"
56
+ end
57
+ end
58
+ end
59
+
60
+ # Instance methods
61
+ def switch(tenant_or_id)
62
+ self.class.switch(tenant_or_id) { yield }
63
+ end
64
+
65
+ def switch!(tenant_or_id)
66
+ self.class.switch!(tenant_or_id)
67
+ end
68
+
69
+ def reset!
70
+ self.class.reset!
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RlsMultiTenant
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Install RLS Multi-tenant gem configuration and initial setup"
11
+
12
+ def create_initializer
13
+ template "rls_multi_tenant.rb", "config/initializers/rls_multi_tenant.rb"
14
+ end
15
+
16
+ def create_tenant_model
17
+ unless File.exist?(File.join(destination_root, "app/models/tenant.rb"))
18
+ template "tenant_model.rb", "app/models/tenant.rb"
19
+ else
20
+ say "Tenant model already exists, skipping creation", :yellow
21
+ end
22
+ end
23
+
24
+ def create_db_admin_task
25
+ unless File.exist?(File.join(destination_root, "lib/tasks/db_admin.rake"))
26
+ template "db_admin.rake", "lib/tasks/db_admin.rake"
27
+ else
28
+ say "Database admin task already exists, skipping creation", :yellow
29
+ end
30
+ end
31
+
32
+ def create_uuid_migration
33
+ unless Dir.glob(File.join(destination_root, "db/migrate/*_enable_uuid_extension.rb")).any?
34
+ create_migration_with_timestamp("enable_uuid", 1)
35
+ else
36
+ say "UUID extension migration already exists, skipping creation", :yellow
37
+ end
38
+ end
39
+
40
+ def create_app_user_migration
41
+ unless Dir.glob(File.join(destination_root, "db/migrate/*_create_app_user.rb")).any?
42
+ create_migration_with_timestamp("create_app_user", 2)
43
+ else
44
+ say "App user migration already exists, skipping creation", :yellow
45
+ end
46
+ end
47
+
48
+ def create_tenant_migration
49
+ unless Dir.glob(File.join(destination_root, "db/migrate/*_create_tenants.rb")).any?
50
+ create_migration_with_timestamp("create_tenant", 3)
51
+ else
52
+ say "Tenant migration already exists, skipping creation", :yellow
53
+ end
54
+ end
55
+
56
+ def show_instructions
57
+ say "\n" + "="*60, :green
58
+ say "RLS Multi-tenant gem installed successfully!", :green
59
+ say "="*60, :green
60
+ say "\nNext steps:", :yellow
61
+ say "1. Configure your environment variables:\n", :yellow
62
+ say " POSTGRES_USER=your_admin_user # This is the user that will run the migrations", :yellow
63
+ say " POSTGRES_PASSWORD=your_admin_user_password", :yellow
64
+ say " POSTGRES_APP_USER=your_app_user # This is the user that will run the app", :yellow
65
+ say " POSTGRES_APP_PASSWORD=your_app_user_password", :yellow
66
+ say "\n2. Make sure to use the POSTGRES_APP_USER in your database.yml.", :yellow
67
+ say "\n3. Run the migrations with admin privileges:\n", :yellow
68
+ say " rails db_as:admin[migrate]", :yellow
69
+ say " Note: We must use the admin user because the app user doesn't have migration privileges", :yellow
70
+ say "\n4. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
71
+ say "="*60, :green
72
+ end
73
+
74
+ private
75
+
76
+ def create_migration_with_timestamp(migration_type, order)
77
+ base_timestamp = Time.current.strftime("%Y%m%d%H%M")
78
+ timestamp = "#{base_timestamp}#{sprintf('%02d', order)}"
79
+
80
+ case migration_type
81
+ when "enable_uuid"
82
+ template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
83
+ when "create_app_user"
84
+ template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
85
+ when "create_tenant"
86
+ template "create_tenant.rb", "db/migrate/#{timestamp}_create_tenants.rb"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,33 @@
1
+ class CreateAppUser < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def up
3
+ app_user = ENV['<%= RlsMultiTenant.app_user_env_var %>']
4
+ app_password = ENV['POSTGRES_APP_PASSWORD']
5
+
6
+ # Create user with RLS privileges in PostgreSQL
7
+ execute <<-SQL
8
+ DO $$
9
+ BEGIN
10
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{app_user}') THEN
11
+ CREATE ROLE #{app_user} WITH LOGIN PASSWORD '#{app_password}';
12
+ END IF;
13
+ END
14
+ $$;
15
+ SQL
16
+
17
+ # Grant basic permissions to the user
18
+ execute "GRANT CONNECT ON DATABASE #{ActiveRecord::Base.connection.current_database} TO #{app_user};"
19
+ execute "GRANT USAGE ON SCHEMA public TO #{app_user};"
20
+ execute "GRANT CREATE ON SCHEMA public TO #{app_user};"
21
+ end
22
+
23
+ def down
24
+ app_user = ENV['<%= RlsMultiTenant.app_user_env_var %>']
25
+
26
+ # Revoke permissions
27
+ execute "REVOKE ALL ON SCHEMA public FROM #{app_user};"
28
+ execute "REVOKE CONNECT ON DATABASE #{ActiveRecord::Base.connection.current_database} FROM #{app_user};"
29
+
30
+ # Drop user
31
+ execute "DROP ROLE IF EXISTS #{app_user};"
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ class CreateTenants < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ create_table :tenants, id: :uuid do |t|
4
+ t.string :name, null: false
5
+
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :tenants, :name, unique: true
10
+
11
+ reversible do |dir|
12
+ dir.up do
13
+ execute "GRANT SELECT, INSERT, UPDATE, DELETE ON tenants TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
14
+ end
15
+ dir.down do
16
+ execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON tenants FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
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
+ # Set a flag to indicate we're running with admin privileges
22
+ ENV['RLS_MULTI_TENANT_ADMIN_MODE'] = 'true'
23
+
24
+ puts "Executing db:#{sub_task} with admin privileges..."
25
+ Rake::Task["db:#{sub_task}"].invoke
26
+ end
27
+ end
@@ -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,5 @@
1
+ class EnableUuidExtension < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ enable_extension 'uuid-ossp'
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ RlsMultiTenant.configure do |config|
4
+ # Configure the tenant model class name
5
+ config.tenant_class_name = "Tenant"
6
+
7
+ # Configure the tenant ID column name
8
+ config.tenant_id_column = :tenant_id
9
+
10
+ # Configure the environment variable for the app user
11
+ config.app_user_env_var = "POSTGRES_APP_USER"
12
+
13
+ # Enable/disable security validation
14
+ config.enable_security_validation = true
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tenant < ApplicationRecord
4
+ include RlsMultiTenant::Concerns::TenantContext
5
+
6
+ validates :name, presence: true
7
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RlsMultiTenant
6
+ module Generators
7
+ class MigrationGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :migration_type, type: :string, desc: "Type of migration to generate"
11
+
12
+ desc "Generate RLS Multi-tenant migrations"
13
+
14
+ def create_migration
15
+ case migration_type
16
+ when "create_tenant"
17
+ create_tenant_migration
18
+ when "create_app_user"
19
+ create_app_user_migration
20
+ when "enable_rls"
21
+ create_enable_rls_migration
22
+ when "enable_uuid"
23
+ create_enable_uuid_migration
24
+ else
25
+ say "Unknown migration type: #{migration_type}", :red
26
+ say "Available types: create_tenant, create_app_user, enable_rls, enable_uuid", :yellow
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def create_app_user_migration
33
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
34
+ template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
35
+ end
36
+
37
+ def create_enable_rls_migration
38
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
39
+ template "enable_rls.rb", "db/migrate/#{timestamp}_enable_rls_for_#{table_name}.rb"
40
+ end
41
+
42
+ def create_tenant_migration
43
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
44
+ template "create_tenant.rb", "db/migrate/#{timestamp}_create_tenant.rb"
45
+ end
46
+
47
+ def create_enable_uuid_migration
48
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
49
+ template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
50
+ end
51
+
52
+ def table_name
53
+ @table_name ||= ask("Enter table name for RLS:")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ class CreateAppUser < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def up
3
+ app_user = ENV['<%= RlsMultiTenant.app_user_env_var %>']
4
+ app_password = ENV['POSTGRES_APP_PASSWORD']
5
+
6
+ # Create user with RLS privileges in PostgreSQL
7
+ execute <<-SQL
8
+ DO $$
9
+ BEGIN
10
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{app_user}') THEN
11
+ CREATE ROLE #{app_user} WITH LOGIN PASSWORD '#{app_password}';
12
+ END IF;
13
+ END
14
+ $$;
15
+ SQL
16
+
17
+ # Grant basic permissions to the user
18
+ execute "GRANT CONNECT ON DATABASE #{ActiveRecord::Base.connection.current_database} TO #{app_user};"
19
+ execute "GRANT USAGE ON SCHEMA public TO #{app_user};"
20
+ execute "GRANT CREATE ON SCHEMA public TO #{app_user};"
21
+ end
22
+
23
+ def down
24
+ app_user = ENV['<%= RlsMultiTenant.app_user_env_var %>']
25
+
26
+ # Revoke permissions
27
+ execute "REVOKE ALL ON SCHEMA public FROM #{app_user};"
28
+ execute "REVOKE CONNECT ON DATABASE #{ActiveRecord::Base.connection.current_database} FROM #{app_user};"
29
+
30
+ # Drop user
31
+ execute "DROP ROLE IF EXISTS #{app_user};"
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ class CreateTenants < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ create_table :tenants, id: :uuid do |t|
4
+ t.string :name, null: false
5
+
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :tenants, :name, unique: true
10
+
11
+ reversible do |dir|
12
+ dir.up do
13
+ execute "GRANT SELECT, INSERT, UPDATE, DELETE ON tenants TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
14
+ end
15
+ dir.down do
16
+ execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON tenants FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
17
+ end
18
+ end
19
+ end
20
+ end