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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 397cee677030faceb292bbfbb2eda3bb87f269d6b1b834966b7e6601472b8bcc
|
|
4
|
+
data.tar.gz: 42dff9f53f1d0ce4e1a59fd44e081bf33b2459083ecc63d6a19560a9c80fd4c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
109
|
+
4. **Run migrations:**
|
|
70
110
|
```bash
|
|
71
|
-
rails
|
|
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 &&
|
|
21
|
-
|
|
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
|
-
|
|
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,
|
|
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'
|
|
9
|
-
RESET_TENANT_ID_SQL = 'RESET %s'
|
|
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,
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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)
|
|
76
|
+
def switch(tenant_or_id, &block)
|
|
77
|
+
self.class.switch(tenant_or_id, &block)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
self.class.switch!(tenant_or_id)
|
|
81
|
-
end
|
|
80
|
+
delegate :switch!, to: :class
|
|
82
81
|
|
|
83
|
-
|
|
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(
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
12
|
|
|
13
|
-
desc
|
|
13
|
+
desc 'Install RLS Multi-tenant gem configuration'
|
|
14
14
|
|
|
15
15
|
def create_initializer
|
|
16
|
-
template
|
|
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
|
|
29
|
-
say
|
|
30
|
-
say
|
|
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 "
|
|
33
|
-
say "
|
|
34
|
-
say
|
|
35
|
-
say "
|
|
36
|
-
say
|
|
37
|
-
say "\
|
|
38
|
-
say
|
|
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 =
|
|
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
|
-
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
14
12
|
|
|
15
|
-
desc
|
|
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
|
|
19
|
+
when 'create_tenant'
|
|
20
20
|
create_tenant_migration
|
|
21
|
-
when
|
|
22
|
-
create_app_user_migration
|
|
23
|
-
when "enable_rls"
|
|
21
|
+
when 'enable_rls'
|
|
24
22
|
create_enable_rls_migration
|
|
25
|
-
when
|
|
23
|
+
when 'enable_uuid'
|
|
26
24
|
create_enable_uuid_migration
|
|
27
25
|
else
|
|
28
26
|
say "Unknown migration type: #{migration_type}", :red
|
|
29
|
-
say
|
|
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(
|
|
41
|
-
template
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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(
|
|
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 %>
|
|
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(
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
9
|
|
|
11
|
-
argument :name, type: :string, desc:
|
|
12
|
-
argument :attributes, type: :array, default: [], banner:
|
|
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
|
|
18
|
+
desc 'Generate a multi-tenant model with RLS policies'
|
|
20
19
|
|
|
21
20
|
def create_model_file
|
|
22
|
-
template
|
|
21
|
+
template 'model.rb', File.join('app/models', "#{model_name.underscore}.rb")
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
def create_migration
|
|
26
|
-
template
|
|
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
|
|
29
|
+
say "\n#{'=' * 60}", :green
|
|
31
30
|
say "Multi-tenant model '#{model_name}' created successfully!", :green
|
|
32
|
-
say
|
|
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
|
|
36
|
-
say
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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
|