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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa585ee59105f64a9b7469703907c8cf24b4ed80873a7cb72476d3bb0bd2082a
4
- data.tar.gz: b9676fcf8125dd896ad04d66b9832cb52ea0b08664e117e4d55cfd26a8309139
3
+ metadata.gz: 397cee677030faceb292bbfbb2eda3bb87f269d6b1b834966b7e6601472b8bcc
4
+ data.tar.gz: 42dff9f53f1d0ce4e1a59fd44e081bf33b2459083ecc63d6a19560a9c80fd4c9
5
5
  SHA512:
6
- metadata.gz: 1d43085f2d1b25d28aec9380e6d30993bb59738ede4b4259457cdcd63577d8f1f996216717de8b16f78eeae531b6c96579b5b5f49e7f83895bcbacbf1538cff1
7
- data.tar.gz: 4caa1cc6a5d697dda6dea0f2feab0460d0436c7d4c16bd82b979000510e66cacb482e493021af0d1356a0285455ad8a499dd55a9b64a6051dc04a0fe8db53f91
6
+ metadata.gz: f99ba4e9128a74857c708e8393d3c8302c900c79c2809c0ad0a8dd1723d7daf1bf5f0ac064376709eb0b73983e14ec4dd3228db053c4f3f063d62c5d7adb4e1e
7
+ data.tar.gz: 65d310a74e59647771b1d3e37508d2e8b23050ceb501a64b6663c9c1532f5d43bb91e84a4c73c673f91e8a4a06d380db8c0b010ee411cf4b4dea3a746be5b084
data/README.md CHANGED
@@ -9,6 +9,12 @@
9
9
 
10
10
  A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenancy for Rails applications.
11
11
 
12
+ > 📚 **Learn more about PostgreSQL Row Level Security**: [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
13
+
14
+ ## ⚠️ IMPORTANT: Database Security Setup for RLS
15
+
16
+ **This gem relies on PostgreSQL Row-Level Security (RLS) for tenant isolation. You MUST create a dedicated database role with proper RLS permissions before using this gem in production.**
17
+
12
18
  ## Features
13
19
 
14
20
  - 🔒 **Row Level Security**: Automatic tenant isolation using PostgreSQL RLS
@@ -19,6 +25,50 @@ A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenanc
19
25
  - ⚙️ **Configurable**: Flexible configuration options
20
26
  - 🌐 **Subdomain Middleware**: Automatic tenant switching based on subdomain
21
27
 
28
+ ### Create Application Database Role
29
+
30
+ Create a dedicated role for your Rails application:
31
+
32
+ ```sql
33
+ CREATE ROLE app_user
34
+ WITH LOGIN
35
+ CREATEDB -- can create databases
36
+ CREATEROLE -- can create/modify other roles (except superuser)
37
+ NOINHERIT -- does not inherit privileges from roles it belongs to
38
+ NOREPLICATION -- cannot use replication
39
+ NOBYPASSRLS -- cannot bypass Row-Level Security
40
+ NOSUPERUSER -- is not a superuser
41
+ PASSWORD 'strong_password';
42
+ ```
43
+
44
+ If you don't have a database created, you can create one with the new role:
45
+
46
+ ```sql
47
+ CREATE DATABASE your_db_name OWNER app_user;
48
+ ```
49
+
50
+ If you already have a database created, make sure to grant ownership of the database to the new role:
51
+
52
+ ```sql
53
+ ALTER DATABASE your_db_name OWNER TO app_user;
54
+ ```
55
+
56
+ ### Why This Role Configuration is Critical for RLS
57
+
58
+ - **`NOBYPASSRLS`**: **ESSENTIAL for RLS security** - prevents bypassing Row-Level Security policies that enforce tenant isolation
59
+ - **`NOSUPERUSER`**: Prevents superuser privileges that could compromise RLS policies
60
+ - **`LOGIN`**: Allows the role to connect to the database
61
+ - **`CREATEDB`**: Enables database creation for development/testing environments
62
+ - **`CREATEROLE`**: Allows creating other roles for application-specific users
63
+ - **`NOINHERIT`**: Ensures the role does not inherit privileges from parent roles
64
+ - **`NOREPLICATION`**: Prevents the role from being used for replication (security)
65
+
66
+ **Without `NOBYPASSRLS`, Row-Level Security policies can be bypassed, completely breaking tenant isolation and exposing data across tenants.**
67
+
68
+ ### Update Database Configuration
69
+
70
+ Update your `config/database.yml` to use the new role:
71
+
22
72
  ## Installation
23
73
 
24
74
  Add this line to your application's Gemfile:
@@ -42,33 +92,23 @@ bundle install
42
92
 
43
93
  2. **Configure the gem settings:**
44
94
  Edit `config/initializers/rls_multi_tenant.rb` to customize your tenant model:
45
- ```ruby
46
- RlsMultiTenant.configure do |config|
47
- config.tenant_class_name = "Tenant" # Change to your preferred tenant model name
48
- config.tenant_id_column = :tenant_id # Tenant ID column name
49
- config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
50
- config.enable_security_validation = true # Enable security checks
51
- config.enable_subdomain_middleware = true # Enable subdomain-based tenant switching (default: true)
52
- config.subdomain_field = :subdomain # Field to use for subdomain matching (default: :subdomain)
53
- end
54
- ```
55
-
56
- 3. **Configure environment variables:**
57
- ```bash
58
- POSTGRES_USER=your_admin_user # This is the user that will run the migrations
59
- POSTGRES_PASSWORD=your_admin_user_password
60
- POSTGRES_APP_USER=your_app_user # This is the user that will run the app
61
- POSTGRES_APP_PASSWORD=your_app_user_password
62
- ```
63
-
64
- 4. **Setup the tenant model and migrations:**
95
+ ```ruby
96
+ RlsMultiTenant.configure do |config|
97
+ config.tenant_class_name = "Tenant" # Your tenant model class (e.g., "Organization", "Company")
98
+ config.tenant_id_column = :tenant_id # Tenant ID column name
99
+ config.enable_security_validation = true # Enable security checks (prevents running with superuser privileges)
100
+ config.enable_subdomain_middleware = true # Enable subdomain-based tenant switching (default: true)
101
+ config.subdomain_field = :subdomain # Field to use for subdomain matching (default: :subdomain)
102
+ end
103
+ ```
104
+ 3. **Setup the tenant model and migrations:**
65
105
  ```bash
66
106
  rails generate rls_multi_tenant:setup
67
107
  ```
68
108
 
69
- 5. **Run migrations:**
109
+ 4. **Run migrations:**
70
110
  ```bash
71
- rails db_as:admin[migrate] # Custom rake task to run migrations with admin privileges
111
+ rails db:migrate
72
112
  ```
73
113
 
74
114
  ## Usage
@@ -157,46 +197,6 @@ end
157
197
  - **Fail-Safe**: Public models are clearly separated from tenant models
158
198
  - **No Configuration Drift**: Can't accidentally expose tenant data through misconfiguration
159
199
 
160
- ## Configuration
161
-
162
- The gem is configured in `config/initializers/rls_multi_tenant.rb` (created by the install generator). You can customize the following options:
163
-
164
- ```ruby
165
- RlsMultiTenant.configure do |config|
166
- config.tenant_class_name = "Tenant" # Your tenant model class (e.g., "Organization", "Company")
167
- config.tenant_id_column = :tenant_id # Tenant ID column name
168
- config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
169
- config.enable_security_validation = true # Enable security checks (prevents running with superuser privileges)
170
- config.enable_subdomain_middleware = true # Enable subdomain-based tenant switching (default: true)
171
- config.subdomain_field = :subdomain # Field to use for subdomain matching (default: :subdomain)
172
- end
173
- ```
174
-
175
- **Important**: Configure these settings **before** running `rails generate rls_multi_tenant:setup`, as the setup generator will use these values to create the appropriate model and migrations.
176
-
177
- ### Database Admin Task
178
- ```bash
179
- # Run migrations with admin privileges (required because app user can't run migrations)
180
- rails db_as:admin[migrate]
181
-
182
- # Run seeds with admin privileges
183
- rails db_as:admin[seed]
184
-
185
- # Create database with admin privileges
186
- rails db_as:admin[create]
187
-
188
- # Drop database with admin privileges
189
- rails db_as:admin[drop]
190
- ```
191
-
192
- ## Security Features
193
-
194
- The gem includes multiple security layers:
195
-
196
- 1. **Environment Validation**: Ensures `POSTGRES_APP_USER` is set and not privileged
197
- 2. **Database User Validation**: Checks that the database user doesn't have SUPERUSER privileges
198
- 3. **RLS Policies**: Automatic tenant isolation at the database level
199
-
200
200
  ## Requirements
201
201
 
202
202
  - Rails 6.0+
@@ -7,7 +7,7 @@ module RlsMultiTenant
7
7
 
8
8
  included do
9
9
  belongs_to :tenant, class_name: RlsMultiTenant.tenant_class_name, foreign_key: RlsMultiTenant.tenant_id_column
10
-
10
+
11
11
  validates RlsMultiTenant.tenant_id_column, presence: true
12
12
 
13
13
  before_validation :set_tenant_id
@@ -17,12 +17,12 @@ module RlsMultiTenant
17
17
  def set_tenant_id
18
18
  current_tenant = RlsMultiTenant.tenant_class.current
19
19
 
20
- if current_tenant && self.send(RlsMultiTenant.tenant_id_column).blank?
21
- self.send("#{RlsMultiTenant.tenant_id_column}=", current_tenant.id)
20
+ if current_tenant && send(RlsMultiTenant.tenant_id_column).blank?
21
+ send("#{RlsMultiTenant.tenant_id_column}=", current_tenant.id)
22
22
  elsif current_tenant.nil?
23
- raise RlsMultiTenant::Error,
23
+ raise RlsMultiTenant::Error,
24
24
  "Cannot create #{self.class.name} without tenant context. " \
25
- "This model requires a tenant context. "
25
+ 'This model requires a tenant context. '
26
26
  end
27
27
  end
28
28
  end
@@ -37,7 +37,8 @@ module RlsMultiTenant
37
37
  when String, Integer
38
38
  tenant_or_id
39
39
  else
40
- raise ArgumentError, "Expected #{RlsMultiTenant.tenant_class_name} object or tenant_id, got #{tenant_or_id.class}"
40
+ raise ArgumentError,
41
+ "Expected #{RlsMultiTenant.tenant_class_name} object or tenant_id, got #{tenant_or_id.class}"
41
42
  end
42
43
  end
43
44
  end
@@ -5,8 +5,8 @@ module RlsMultiTenant
5
5
  module TenantContext
6
6
  extend ActiveSupport::Concern
7
7
 
8
- SET_TENANT_ID_SQL = 'SET %s = %s'.freeze
9
- RESET_TENANT_ID_SQL = 'RESET %s'.freeze
8
+ SET_TENANT_ID_SQL = 'SET %s = %s'
9
+ RESET_TENANT_ID_SQL = 'RESET %s'
10
10
 
11
11
  class_methods do
12
12
  def tenant_session_var
@@ -41,9 +41,9 @@ module RlsMultiTenant
41
41
 
42
42
  result = connection.execute("SHOW #{tenant_session_var}")
43
43
  tenant_id = result.first&.dig(tenant_session_var)
44
-
44
+
45
45
  return nil if tenant_id.blank?
46
-
46
+
47
47
  RlsMultiTenant.tenant_class.find_by(id: tenant_id)
48
48
  rescue ActiveRecord::StatementInvalid, PG::Error
49
49
  nil
@@ -58,31 +58,28 @@ module RlsMultiTenant
58
58
  when String, Integer
59
59
  tenant_or_id
60
60
  else
61
- raise ArgumentError, "Expected #{RlsMultiTenant.tenant_class_name} object or tenant_id, got #{tenant_or_id.class}"
61
+ raise ArgumentError,
62
+ "Expected #{RlsMultiTenant.tenant_class_name} object or tenant_id, got #{tenant_or_id.class}"
62
63
  end
63
64
  end
64
65
 
65
66
  def validate_tenant_exists!(tenant_id)
66
67
  return if tenant_id.blank?
67
-
68
- unless RlsMultiTenant.tenant_class.exists?(id: tenant_id)
69
- raise StandardError, "#{RlsMultiTenant.tenant_class_name} with id '#{tenant_id}' not found"
70
- end
68
+
69
+ return if RlsMultiTenant.tenant_class.exists?(id: tenant_id)
70
+
71
+ raise StandardError, "#{RlsMultiTenant.tenant_class_name} with id '#{tenant_id}' not found"
71
72
  end
72
73
  end
73
74
 
74
75
  # Instance methods
75
- def switch(tenant_or_id)
76
- self.class.switch(tenant_or_id) { yield }
76
+ def switch(tenant_or_id, &block)
77
+ self.class.switch(tenant_or_id, &block)
77
78
  end
78
79
 
79
- def switch!(tenant_or_id)
80
- self.class.switch!(tenant_or_id)
81
- end
80
+ delegate :switch!, to: :class
82
81
 
83
- def reset!
84
- self.class.reset!
85
- end
82
+ delegate :reset!, to: :class
86
83
  end
87
84
  end
88
85
  end
@@ -8,44 +8,27 @@ module RlsMultiTenant
8
8
  class InstallGenerator < Rails::Generators::Base
9
9
  include Shared::TemplateHelper
10
10
 
11
- source_root File.expand_path("templates", __dir__)
11
+ source_root File.expand_path('templates', __dir__)
12
12
 
13
- desc "Install RLS Multi-tenant gem configuration"
13
+ desc 'Install RLS Multi-tenant gem configuration'
14
14
 
15
15
  def create_initializer
16
- template "rls_multi_tenant.rb", "config/initializers/rls_multi_tenant.rb"
17
- end
18
-
19
- def create_db_admin_task
20
- unless File.exist?(File.join(destination_root, "lib/tasks/db_admin.rake"))
21
- copy_shared_template "db_admin.rake", "lib/tasks/db_admin.rake"
22
- else
23
- say "Database admin task already exists, skipping creation", :yellow
24
- end
16
+ template 'rls_multi_tenant.rb', 'config/initializers/rls_multi_tenant.rb'
25
17
  end
26
18
 
27
19
  def show_instructions
28
- say "\n" + "="*60, :green
29
- say "RLS Multi-tenant gem configuration installed successfully!", :green
30
- say "="*60, :green
20
+ say "\n#{'=' * 60}", :green
21
+ say 'RLS Multi-tenant gem configuration installed successfully!', :green
22
+ say '=' * 60, :green
31
23
  say "\nNext steps:", :yellow
32
- say "1. Configure your environment variables:\n", :yellow
33
- say " POSTGRES_USER=your_admin_user # This is the user that will run the migrations", :yellow
34
- say " POSTGRES_PASSWORD=your_admin_user_password", :yellow
35
- say " POSTGRES_APP_USER=your_app_user # This is the user that will run the app", :yellow
36
- say " POSTGRES_APP_PASSWORD=your_app_user_password", :yellow
37
- say "\n2. Configure the gem settings in config/initializers/rls_multi_tenant.rb", :yellow
38
- say " (tenant_class_name, tenant_id_column, app_user_env_var, enable_security_validation)", :yellow
39
- say "\n3. Run the setup generator to create the tenant model and migrations:\n", :yellow
40
- say " rails generate rls_multi_tenant:setup", :yellow
41
- say "\n4. Make sure to use the POSTGRES_APP_USER in your database.yml.", :yellow
42
- say "\n5. Run the migrations with admin privileges:\n", :yellow
43
- say " rails db_as:admin[migrate]", :yellow
44
- say " Note: We must use the admin user because the app user doesn't have migration privileges", :yellow
45
- say "\n6. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
46
- say "="*60, :green
24
+ say "\n1. Configure the gem settings in config/initializers/rls_multi_tenant.rb", :yellow
25
+ say "\n2. Run the setup generator to create the tenant model and migrations:\n", :yellow
26
+ say ' rails generate rls_multi_tenant:setup', :yellow
27
+ say "\n3. Run migrations:\n", :yellow
28
+ say ' rails db:migrate', :yellow
29
+ say "\n4. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
30
+ say '=' * 60, :green
47
31
  end
48
-
49
32
  end
50
33
  end
51
34
  end
@@ -2,20 +2,17 @@
2
2
 
3
3
  RlsMultiTenant.configure do |config|
4
4
  # Configure the tenant model class name
5
- config.tenant_class_name = "Tenant"
6
-
5
+ config.tenant_class_name = 'Tenant'
6
+
7
7
  # Configure the tenant ID column name
8
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
-
9
+
13
10
  # Enable/disable security validation
14
11
  config.enable_security_validation = true
15
-
12
+
16
13
  # Enable/disable subdomain-based tenant switching middleware
17
14
  config.enable_subdomain_middleware = true
18
-
15
+
19
16
  # Configure the field to use for subdomain matching (default: :subdomain)
20
17
  # This should be a field on your tenant model that contains the subdomain
21
18
  config.subdomain_field = :subdomain
@@ -7,101 +7,58 @@ module RlsMultiTenant
7
7
  module Generators
8
8
  class MigrationGenerator < Rails::Generators::Base
9
9
  include Shared::TemplateHelper
10
-
11
- source_root File.expand_path("templates", __dir__)
12
10
 
13
- argument :migration_type, type: :string, desc: "Type of migration to generate"
11
+ source_root File.expand_path('templates', __dir__)
14
12
 
15
- desc "Generate RLS Multi-tenant migrations"
13
+ argument :migration_type, type: :string, desc: 'Type of migration to generate'
14
+
15
+ desc 'Generate RLS Multi-tenant migrations'
16
16
 
17
17
  def create_migration
18
18
  case migration_type
19
- when "create_tenant"
19
+ when 'create_tenant'
20
20
  create_tenant_migration
21
- when "create_app_user"
22
- create_app_user_migration
23
- when "enable_rls"
21
+ when 'enable_rls'
24
22
  create_enable_rls_migration
25
- when "enable_uuid"
23
+ when 'enable_uuid'
26
24
  create_enable_uuid_migration
27
25
  else
28
26
  say "Unknown migration type: #{migration_type}", :red
29
- say "Available types: create_tenant, create_app_user, enable_rls, enable_uuid", :yellow
27
+ say 'Available types: create_tenant, enable_rls, enable_uuid', :yellow
30
28
  end
31
29
  end
32
30
 
33
31
  private
34
32
 
35
- def create_app_user_migration
36
- create_app_user_migrations_for_all_databases
37
- end
38
-
39
33
  def create_enable_rls_migration
40
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
41
- template "enable_rls.rb", "db/migrate/#{timestamp}_enable_rls_for_#{table_name}.rb"
34
+ timestamp = Time.current.strftime('%Y%m%d%H%M%S')
35
+ template 'enable_rls.rb', "db/migrate/#{timestamp}_enable_rls_for_#{table_name}.rb"
42
36
  end
43
37
 
44
38
  def create_tenant_migration
45
39
  tenant_class_name = RlsMultiTenant.tenant_class_name
46
40
  migration_pattern = "*_create_#{tenant_class_name.underscore.pluralize}.rb"
47
-
48
- unless Dir.glob(File.join(destination_root, "db/migrate/#{migration_pattern}")).any?
49
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
50
- render_shared_template "create_tenant.rb", "db/migrate/#{timestamp}_create_#{tenant_class_name.underscore.pluralize}.rb"
51
- else
52
- say "#{tenant_class_name} migration already exists, skipping creation", :yellow
53
- end
54
- end
55
41
 
56
- def create_enable_uuid_migration
57
- unless Dir.glob(File.join(destination_root, "db/migrate/*_enable_uuid_extension.rb")).any?
58
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
59
- render_shared_template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
42
+ if Dir.glob(File.join(destination_root, "db/migrate/#{migration_pattern}")).any?
43
+ say "#{tenant_class_name} migration already exists, skipping creation", :yellow
60
44
  else
61
- say "UUID extension migration already exists, skipping creation", :yellow
45
+ timestamp = Time.current.strftime('%Y%m%d%H%M%S')
46
+ render_shared_template 'create_tenant.rb',
47
+ "db/migrate/#{timestamp}_create_#{tenant_class_name.underscore.pluralize}.rb"
62
48
  end
63
49
  end
64
50
 
65
- def create_app_user_migrations_for_all_databases
66
- # Get database configuration for current environment
67
- db_config = Rails.application.config.database_configuration[Rails.env]
68
-
69
- # Handle both single database and multiple databases configuration
70
- databases_to_process = if db_config.is_a?(Hash) && db_config.key?('primary')
71
- # Multiple databases configuration
72
- db_config
51
+ def create_enable_uuid_migration
52
+ if Dir.glob(File.join(destination_root, 'db/migrate/*_enable_uuid_extension.rb')).any?
53
+ say 'UUID extension migration already exists, skipping creation', :yellow
73
54
  else
74
- # Single database configuration - treat as primary
75
- { 'primary' => db_config }
55
+ timestamp = Time.current.strftime('%Y%m%d%H%M%S')
56
+ render_shared_template 'enable_uuid_extension.rb', "db/migrate/#{timestamp}_enable_uuid_extension.rb"
76
57
  end
77
-
78
- databases_to_process.each do |db_name, config|
79
- next if db_name == 'primary' # Skip primary database, handle it separately
80
-
81
- # Check if migrations_paths is defined for this database
82
- if config['migrations_paths']
83
- migration_paths = Array(config['migrations_paths'])
84
- migration_paths.each do |migration_path|
85
- migration_dir = File.join(destination_root, migration_path)
86
- FileUtils.mkdir_p(migration_dir) unless File.directory?(migration_dir)
87
-
88
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
89
- render_shared_template "create_app_user.rb", "#{migration_path}/#{timestamp}_create_app_user.rb"
90
- say "Created app user migration for #{db_name} in #{migration_path}", :green
91
- end
92
- else
93
- say "No migrations_paths defined for database '#{db_name}', skipping app user migration", :yellow
94
- end
95
- end
96
-
97
- # Handle primary database (default behavior)
98
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
99
- render_shared_template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
100
- say "Created app user migration for primary database", :green
101
58
  end
102
59
 
103
60
  def table_name
104
- @table_name ||= ask("Enter table name for RLS:")
61
+ @table_name ||= ask('Enter table name for RLS:')
105
62
  end
106
63
  end
107
64
  end
@@ -2,11 +2,11 @@ class EnableRlsFor<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails
2
2
  def change
3
3
  reversible do |dir|
4
4
  dir.up do
5
- # Enable Row Level Security
6
- execute 'ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY'
5
+ # Enable and force Row Level Security
6
+ execute 'ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY'
7
7
 
8
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.<%= RlsMultiTenant.tenant_id_column %>', TRUE), '')::uuid)"
9
+ execute "CREATE POLICY <%= table_name %>_app_user ON <%= table_name %> USING (<%= RlsMultiTenant.tenant_id_column %> = NULLIF(current_setting('rls.<%= RlsMultiTenant.tenant_id_column %>', TRUE), '')::uuid)"
10
10
  end
11
11
 
12
12
  dir.down do
@@ -14,7 +14,7 @@ class EnableRlsFor<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails
14
14
  execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
15
15
 
16
16
  # Disable RLS
17
- execute "ALTER TABLE <%= table_name %> DISABLE ROW LEVEL SECURITY"
17
+ execute "ALTER TABLE <%= table_name %> DISABLE ROW LEVEL SECURITY, NO FORCE ROW LEVEL SECURITY"
18
18
  end
19
19
  end
20
20
  end
@@ -1,43 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/generators'
4
- require 'ostruct'
5
4
 
6
5
  module RlsMultiTenant
7
6
  module Generators
8
7
  class ModelGenerator < Rails::Generators::Base
9
- source_root File.expand_path("templates", __dir__)
8
+ source_root File.expand_path('templates', __dir__)
10
9
 
11
- argument :name, type: :string, desc: "Model name"
12
- argument :attributes, type: :array, default: [], banner: "field:type field:type"
10
+ argument :name, type: :string, desc: 'Model name'
11
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
13
12
 
14
13
  def initialize(args, *options)
15
14
  super
16
15
  @attributes = parse_attributes!
17
16
  end
18
17
 
19
- desc "Generate a multi-tenant model with RLS policies"
18
+ desc 'Generate a multi-tenant model with RLS policies'
20
19
 
21
20
  def create_model_file
22
- template "model.rb", File.join("app/models", "#{model_name.underscore}.rb")
21
+ template 'model.rb', File.join('app/models', "#{model_name.underscore}.rb")
23
22
  end
24
23
 
25
24
  def create_migration
26
- template "migration.rb", File.join("db/migrate", "#{migration_file_name}.rb")
25
+ template 'migration.rb', File.join('db/migrate', "#{migration_file_name}.rb")
27
26
  end
28
27
 
29
28
  def show_instructions
30
- say "\n" + "="*60, :green
29
+ say "\n#{'=' * 60}", :green
31
30
  say "Multi-tenant model '#{model_name}' created successfully!", :green
32
- say "="*60, :green
31
+ say '=' * 60, :green
33
32
  say "\nWhat was created:", :yellow
34
33
  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
34
+ say "2. Migration: #{migration_file_name}.rb (with RLS policies)", :yellow
35
+ say '=' * 60, :green
41
36
  end
42
37
 
43
38
  private
@@ -55,7 +50,7 @@ module RlsMultiTenant
55
50
  end
56
51
 
57
52
  def migration_timestamp
58
- Time.current.strftime("%Y%m%d%H%M%S")
53
+ Time.current.strftime('%Y%m%d%H%M%S')
59
54
  end
60
55
 
61
56
  def tenant_id_column
@@ -73,9 +68,9 @@ module RlsMultiTenant
73
68
  def parse_attributes!
74
69
  attributes.map do |attr|
75
70
  name, type = attr.split(':')
76
- OpenStruct.new(name: name, type: type || 'string')
71
+ { name: name, type: type || 'string' }
77
72
  end
78
73
  end
79
74
  end
80
75
  end
81
- end
76
+ end
@@ -4,21 +4,20 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= Rails.version.to
4
4
  create_table :<%= table_name %> do |t|
5
5
  t.references :<%= tenant_id_column.to_s.gsub('_id', '') %>, null: false, foreign_key: { to_table: :<%= tenant_class_name.underscore.pluralize %> }, type: :uuid
6
6
  <% @attributes.each do |attribute| -%>
7
- t.<%= attribute.type %> :<%= attribute.name %>
7
+ t.<%= attribute[:type] %> :<%= attribute[:name] %>
8
8
  <% end -%>
9
9
 
10
10
  t.timestamps
11
11
  end
12
12
 
13
- # Enable Row Level Security
14
- execute "ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY"
15
-
16
13
  # Define RLS policy
17
14
  reversible do |dir|
18
15
  dir.up do
19
- 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)"
16
+ execute "ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY"
17
+ execute "CREATE POLICY <%= table_name %>_app_user ON <%= table_name %> USING (<%= tenant_id_column %> = NULLIF(current_setting('rls.<%= tenant_id_column %>', TRUE), '')::uuid)"
20
18
  end
21
19
  dir.down do
20
+ execute "ALTER TABLE <%= table_name %> DISABLE ROW LEVEL SECURITY, NO FORCE ROW LEVEL SECURITY"
22
21
  execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
23
22
  end
24
23
  end