rls_multi_tenant 0.1.4 → 0.1.5

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -9
  3. data/lib/rls_multi_tenant/concerns/tenant_context.rb +10 -7
  4. data/lib/rls_multi_tenant/generators/install/install_generator.rb +9 -110
  5. data/lib/rls_multi_tenant/generators/migration/migration_generator.rb +20 -6
  6. data/lib/rls_multi_tenant/generators/migration/templates/enable_rls.rb +1 -1
  7. data/lib/rls_multi_tenant/generators/setup/setup_generator.rb +149 -0
  8. data/lib/rls_multi_tenant/generators/{install → setup}/templates/tenant_model.rb +1 -1
  9. data/lib/rls_multi_tenant/generators/shared/template_helper.rb +31 -0
  10. data/lib/rls_multi_tenant/generators/shared/templates/create_tenant.rb +11 -0
  11. data/lib/rls_multi_tenant/generators/task/task_generator.rb +4 -1
  12. data/lib/rls_multi_tenant/railtie.rb +1 -0
  13. data/lib/rls_multi_tenant/rls_helper.rb +2 -1
  14. data/lib/rls_multi_tenant/version.rb +1 -1
  15. data/lib/rls_multi_tenant.rb +1 -3
  16. data/rls_multi_tenant.gemspec +4 -2
  17. metadata +25 -14
  18. data/lib/rls_multi_tenant/generators/install/templates/create_tenant.rb +0 -20
  19. data/lib/rls_multi_tenant/generators/install/templates/enable_rls.rb +0 -27
  20. data/lib/rls_multi_tenant/generators/migration/templates/create_app_user.rb +0 -45
  21. data/lib/rls_multi_tenant/generators/migration/templates/create_tenant.rb +0 -20
  22. data/lib/rls_multi_tenant/generators/migration/templates/enable_uuid_extension.rb +0 -5
  23. data/lib/rls_multi_tenant/generators/task/templates/db_admin.rake +0 -24
  24. /data/lib/rls_multi_tenant/generators/{install → shared}/templates/create_app_user.rb +0 -0
  25. /data/lib/rls_multi_tenant/generators/{install → shared}/templates/db_admin.rake +0 -0
  26. /data/lib/rls_multi_tenant/generators/{install → shared}/templates/enable_uuid_extension.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67facbb6334d74a015aebaf68da5616a49345aafbb56da7000a1685d6cbc941b
4
- data.tar.gz: 7923881ec5e324f2e080a83a11303bceaa510f301d86725c314dfdb33964284a
3
+ metadata.gz: f313946d33d5edb4a96fec341e7ab62fc325ab0d6700bad0aef7b05d6f240ec4
4
+ data.tar.gz: 0bbbdf115873c98b5487569cd21e63822fd1b2419b7eec55bbb875a5c131a417
5
5
  SHA512:
6
- metadata.gz: 6d051f19c12738ca80800d753103dbbb783f1d521eb9453cffb553a988a19b3ae81327be0aae62a376b358668bfc394f97a4e1afd4fb242fe48214a8995fac11
7
- data.tar.gz: 4674cc8484191244f3c7534de226a147a18bca9e8af0199749820f805338c1a32fa412054d12cc27c007f094724a5fb8fa1418feb68a4f74124b0ebe17ba3683
6
+ metadata.gz: fe257329e8fff95796cc6b4510ce1672ddf16e86b572d0113fedcc902afa69fc7e13d925b4d2d5e0a0bb1a6bcc5692e1bdc477fe8aab365b4b838d3fbedf124f
7
+ data.tar.gz: 4ad4b27347d478b42e720c33c178eb655860d93809f0403fa58ff5197dbae98e563064e654ea51bdbabf0c3ae272e5871de402e1f6785fac0b3e39971a890341
data/README.md CHANGED
@@ -32,15 +32,31 @@ bundle install
32
32
  rails generate rls_multi_tenant:install
33
33
  ```
34
34
 
35
- 2. **Configure environment variables:**
35
+ 2. **Configure the gem settings:**
36
+ Edit `config/initializers/rls_multi_tenant.rb` to customize your tenant model:
37
+ ```ruby
38
+ RlsMultiTenant.configure do |config|
39
+ config.tenant_class_name = "Tenant" # Change to your preferred tenant model name
40
+ config.tenant_id_column = :tenant_id # Tenant ID column name
41
+ config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
42
+ config.enable_security_validation = true # Enable security checks
43
+ end
44
+ ```
45
+
46
+ 3. **Configure environment variables:**
36
47
  ```bash
37
48
  POSTGRES_USER=your_admin_user # This is the user that will run the migrations
38
49
  POSTGRES_PASSWORD=your_admin_user_password
39
50
  POSTGRES_APP_USER=your_app_user # This is the user that will run the app
40
- POSTGRES_APP_PASSWORD=your_password
51
+ POSTGRES_APP_PASSWORD=your_app_user_password
52
+ ```
53
+
54
+ 4. **Setup the tenant model and migrations:**
55
+ ```bash
56
+ rails generate rls_multi_tenant:setup
41
57
  ```
42
58
 
43
- 3. **Run migrations:**
59
+ 5. **Run migrations:**
44
60
  ```bash
45
61
  rails db_as:admin[migrate] # Custom rake task to run migrations with admin privileges
46
62
  ```
@@ -87,17 +103,19 @@ current_tenant = Tenant.current
87
103
 
88
104
  ## Configuration
89
105
 
90
- Configure the gem in `config/initializers/rls_multi_tenant.rb`:
106
+ The gem is configured in `config/initializers/rls_multi_tenant.rb` (created by the install generator). You can customize the following options:
91
107
 
92
108
  ```ruby
93
109
  RlsMultiTenant.configure do |config|
94
- config.tenant_class_name = "Tenant" # Your tenant model class
110
+ config.tenant_class_name = "Tenant" # Your tenant model class (e.g., "Organization", "Company")
95
111
  config.tenant_id_column = :tenant_id # Tenant ID column name
96
112
  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.
113
+ config.enable_security_validation = true # Enable security checks (prevents running with superuser privileges)
98
114
  end
99
115
  ```
100
116
 
117
+ **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.
118
+
101
119
  ### Database Admin Task
102
120
  ```bash
103
121
  # Run migrations with admin privileges (required because app user can't run migrations)
@@ -120,9 +138,9 @@ The gem includes multiple security layers:
120
138
 
121
139
  ## Requirements
122
140
 
123
- - Rails 7.0+
124
- - PostgreSQL 12+ (with UUID extension support)
125
- - Ruby 3.0+
141
+ - Rails 6.0+
142
+ - PostgreSQL 9.5+ (with UUID extension support)
143
+ - Ruby 2.7+
126
144
 
127
145
  ## UUID Support
128
146
 
@@ -5,14 +5,17 @@ module RlsMultiTenant
5
5
  module TenantContext
6
6
  extend ActiveSupport::Concern
7
7
 
8
- SET_TENANT_ID_SQL = 'SET rls.tenant_id = %s'.freeze
9
- RESET_TENANT_ID_SQL = 'RESET rls.tenant_id'.freeze
8
+ SET_TENANT_ID_SQL = 'SET %s = %s'.freeze
9
+ RESET_TENANT_ID_SQL = 'RESET %s'.freeze
10
10
 
11
11
  class_methods do
12
+ def tenant_session_var
13
+ "rls.#{RlsMultiTenant.tenant_id_column}"
14
+ end
12
15
  # Switch tenant context for a block
13
16
  def switch(tenant_or_id)
14
17
  tenant_id = extract_tenant_id(tenant_or_id)
15
- connection.execute format(SET_TENANT_ID_SQL, connection.quote(tenant_id))
18
+ connection.execute format(SET_TENANT_ID_SQL, tenant_session_var, connection.quote(tenant_id))
16
19
  yield
17
20
  ensure
18
21
  reset!
@@ -21,20 +24,20 @@ module RlsMultiTenant
21
24
  # Switch tenant context permanently (until reset)
22
25
  def switch!(tenant_or_id)
23
26
  tenant_id = extract_tenant_id(tenant_or_id)
24
- connection.execute format(SET_TENANT_ID_SQL, connection.quote(tenant_id))
27
+ connection.execute format(SET_TENANT_ID_SQL, tenant_session_var, connection.quote(tenant_id))
25
28
  end
26
29
 
27
30
  # Reset tenant context
28
31
  def reset!
29
- connection.execute RESET_TENANT_ID_SQL
32
+ connection.execute format(RESET_TENANT_ID_SQL, tenant_session_var)
30
33
  end
31
34
 
32
35
  # Get current tenant from context
33
36
  def current
34
37
  return nil unless connection.active?
35
38
 
36
- result = connection.execute("SHOW rls.tenant_id")
37
- tenant_id = result.first&.dig('rls.tenant_id')
39
+ result = connection.execute("SHOW #{tenant_session_var}")
40
+ tenant_id = result.first&.dig(tenant_session_var)
38
41
 
39
42
  return nil if tenant_id.blank?
40
43
 
@@ -7,51 +7,15 @@ module RlsMultiTenant
7
7
  class InstallGenerator < Rails::Generators::Base
8
8
  source_root File.expand_path("templates", __dir__)
9
9
 
10
- desc "Install RLS Multi-tenant gem configuration and initial setup"
10
+ desc "Install RLS Multi-tenant gem configuration"
11
11
 
12
12
  def create_initializer
13
13
  template "rls_multi_tenant.rb", "config/initializers/rls_multi_tenant.rb"
14
14
  end
15
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
- create_app_user_migrations_for_all_databases
42
- end
43
-
44
- def create_tenant_migration
45
- unless Dir.glob(File.join(destination_root, "db/migrate/*_create_tenants.rb")).any?
46
- create_migration_with_timestamp("create_tenant", 3)
47
- else
48
- say "Tenant migration already exists, skipping creation", :yellow
49
- end
50
- end
51
-
52
16
  def show_instructions
53
17
  say "\n" + "="*60, :green
54
- say "RLS Multi-tenant gem installed successfully!", :green
18
+ say "RLS Multi-tenant gem configuration installed successfully!", :green
55
19
  say "="*60, :green
56
20
  say "\nNext steps:", :yellow
57
21
  say "1. Configure your environment variables:\n", :yellow
@@ -59,83 +23,18 @@ module RlsMultiTenant
59
23
  say " POSTGRES_PASSWORD=your_admin_user_password", :yellow
60
24
  say " POSTGRES_APP_USER=your_app_user # This is the user that will run the app", :yellow
61
25
  say " POSTGRES_APP_PASSWORD=your_app_user_password", :yellow
62
- say "\n2. Make sure to use the POSTGRES_APP_USER in your database.yml.", :yellow
63
- say "\n3. Run the migrations with admin privileges:\n", :yellow
26
+ say "\n2. Configure the gem settings in config/initializers/rls_multi_tenant.rb", :yellow
27
+ say " (tenant_class_name, tenant_id_column, app_user_env_var, enable_security_validation)", :yellow
28
+ say "\n3. Run the setup generator to create the tenant model and migrations:\n", :yellow
29
+ say " rails generate rls_multi_tenant:setup", :yellow
30
+ say "\n4. Make sure to use the POSTGRES_APP_USER in your database.yml.", :yellow
31
+ say "\n5. Run the migrations with admin privileges:\n", :yellow
64
32
  say " rails db_as:admin[migrate]", :yellow
65
33
  say " Note: We must use the admin user because the app user doesn't have migration privileges", :yellow
66
- say "\n4. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
34
+ say "\n6. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
67
35
  say "="*60, :green
68
36
  end
69
37
 
70
- def create_app_user_migrations_for_all_databases
71
- # Get database configuration for current environment
72
- db_config = Rails.application.config.database_configuration[Rails.env]
73
-
74
- # Handle both single database and multiple databases configuration
75
- databases_to_process = if db_config.is_a?(Hash) && db_config.key?('primary')
76
- # Multiple databases configuration
77
- db_config
78
- else
79
- # Single database configuration - treat as primary
80
- { 'primary' => db_config }
81
- end
82
-
83
- databases_to_process.each do |db_name, config|
84
- next if db_name == 'primary' # Skip primary database, handle it separately
85
-
86
- # Check if migrations_paths is defined for this database
87
- if config['migrations_paths']
88
- migration_paths = Array(config['migrations_paths'])
89
- migration_paths.each do |migration_path|
90
- migration_dir = File.join(destination_root, migration_path)
91
-
92
- # Check if migration already exists in this path
93
- unless Dir.glob(File.join(migration_dir, "*_create_app_user.rb")).any?
94
- FileUtils.mkdir_p(migration_dir) unless File.directory?(migration_dir)
95
- create_migration_with_timestamp_for_path("create_app_user", 2, migration_path)
96
- say "Created app user migration for #{db_name} in #{migration_path}", :green
97
- else
98
- say "App user migration already exists for #{db_name} in #{migration_path}, skipping creation", :yellow
99
- end
100
- end
101
- else
102
- say "No migrations_paths defined for database '#{db_name}', skipping app user migration", :yellow
103
- end
104
- end
105
-
106
- # Handle primary database (default behavior)
107
- unless Dir.glob(File.join(destination_root, "db/migrate/*_create_app_user.rb")).any?
108
- create_migration_with_timestamp("create_app_user", 2)
109
- else
110
- say "App user migration already exists for primary database, skipping creation", :yellow
111
- end
112
- end
113
-
114
- private
115
-
116
- def create_migration_with_timestamp(migration_type, order)
117
- base_timestamp = Time.current.strftime("%Y%m%d%H%M")
118
- timestamp = "#{base_timestamp}#{sprintf('%02d', order)}"
119
-
120
- case migration_type
121
- when "enable_uuid"
122
- template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
123
- when "create_app_user"
124
- template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
125
- when "create_tenant"
126
- template "create_tenant.rb", "db/migrate/#{timestamp}_create_tenants.rb"
127
- end
128
- end
129
-
130
- def create_migration_with_timestamp_for_path(migration_type, order, migration_path)
131
- base_timestamp = Time.current.strftime("%Y%m%d%H%M")
132
- timestamp = "#{base_timestamp}#{sprintf('%02d', order)}"
133
-
134
- case migration_type
135
- when "create_app_user"
136
- template "create_app_user.rb", "#{migration_path}/#{timestamp}_create_app_user.rb"
137
- end
138
- end
139
38
  end
140
39
  end
141
40
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/generators'
4
+ require 'rls_multi_tenant/generators/shared/template_helper'
4
5
 
5
6
  module RlsMultiTenant
6
7
  module Generators
7
8
  class MigrationGenerator < Rails::Generators::Base
9
+ include Shared::TemplateHelper
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
12
 
10
13
  argument :migration_type, type: :string, desc: "Type of migration to generate"
@@ -39,13 +42,24 @@ module RlsMultiTenant
39
42
  end
40
43
 
41
44
  def create_tenant_migration
42
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
43
- template "create_tenant.rb", "db/migrate/#{timestamp}_create_tenant.rb"
45
+ tenant_class_name = RlsMultiTenant.tenant_class_name
46
+ 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
44
54
  end
45
55
 
46
56
  def create_enable_uuid_migration
47
- timestamp = Time.current.strftime("%Y%m%d%H%M%S")
48
- template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
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"
60
+ else
61
+ say "UUID extension migration already exists, skipping creation", :yellow
62
+ end
49
63
  end
50
64
 
51
65
  def create_app_user_migrations_for_all_databases
@@ -72,7 +86,7 @@ module RlsMultiTenant
72
86
  FileUtils.mkdir_p(migration_dir) unless File.directory?(migration_dir)
73
87
 
74
88
  timestamp = Time.current.strftime("%Y%m%d%H%M%S")
75
- template "create_app_user.rb", "#{migration_path}/#{timestamp}_create_app_user.rb"
89
+ render_shared_template "create_app_user.rb", "#{migration_path}/#{timestamp}_create_app_user.rb"
76
90
  say "Created app user migration for #{db_name} in #{migration_path}", :green
77
91
  end
78
92
  else
@@ -82,7 +96,7 @@ module RlsMultiTenant
82
96
 
83
97
  # Handle primary database (default behavior)
84
98
  timestamp = Time.current.strftime("%Y%m%d%H%M%S")
85
- template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
99
+ render_shared_template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
86
100
  say "Created app user migration for primary database", :green
87
101
  end
88
102
 
@@ -6,7 +6,7 @@ class EnableRlsFor<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails
6
6
  execute 'ALTER TABLE <%= table_name %> ENABLE 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.tenant_id', TRUE), '')::uuid)"
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)"
10
10
 
11
11
  # Grant permissions
12
12
  execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rls_multi_tenant/generators/shared/template_helper'
5
+
6
+ module RlsMultiTenant
7
+ module Generators
8
+ class SetupGenerator < Rails::Generators::Base
9
+ include Shared::TemplateHelper
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Setup RLS Multi-tenant gem with tenant model and migrations"
14
+
15
+ def create_tenant_model
16
+ tenant_class_name = RlsMultiTenant.tenant_class_name
17
+ tenant_file_path = "app/models/#{tenant_class_name.underscore}.rb"
18
+
19
+ unless File.exist?(File.join(destination_root, tenant_file_path))
20
+ template "tenant_model.rb", tenant_file_path
21
+ else
22
+ say "#{tenant_class_name} model already exists, skipping creation", :yellow
23
+ end
24
+ end
25
+
26
+ def create_db_admin_task
27
+ unless File.exist?(File.join(destination_root, "lib/tasks/db_admin.rake"))
28
+ copy_shared_template "db_admin.rake", "lib/tasks/db_admin.rake"
29
+ else
30
+ say "Database admin task already exists, skipping creation", :yellow
31
+ end
32
+ end
33
+
34
+ def create_uuid_migration
35
+ unless Dir.glob(File.join(destination_root, "db/migrate/*_enable_uuid_extension.rb")).any?
36
+ create_migration_with_timestamp("enable_uuid", 1)
37
+ else
38
+ say "UUID extension migration already exists, skipping creation", :yellow
39
+ end
40
+ end
41
+
42
+ def create_app_user_migration
43
+ create_app_user_migrations_for_all_databases
44
+ end
45
+
46
+ def create_tenant_migration
47
+ tenant_class_name = RlsMultiTenant.tenant_class_name
48
+ migration_pattern = "*_create_#{tenant_class_name.underscore.pluralize}.rb"
49
+
50
+ unless Dir.glob(File.join(destination_root, "db/migrate/#{migration_pattern}")).any?
51
+ create_migration_with_timestamp("create_tenant", 3)
52
+ else
53
+ say "#{tenant_class_name} migration already exists, skipping creation", :yellow
54
+ end
55
+ end
56
+
57
+ def show_instructions
58
+ tenant_class_name = RlsMultiTenant.tenant_class_name
59
+ say "\n" + "="*60, :green
60
+ say "RLS Multi-tenant setup completed successfully!", :green
61
+ say "="*60, :green
62
+ say "\nCreated:", :yellow
63
+ say "• #{tenant_class_name} model", :green
64
+ say "• Database admin task", :green
65
+ say "• UUID extension migration", :green
66
+ say "• App user migration(s)", :green
67
+ say "• #{tenant_class_name} migration", :green
68
+ say "\nNext steps:", :yellow
69
+ say "1. Make sure to use the POSTGRES_APP_USER in your database.yml.", :yellow
70
+ say "\n2. Run the migrations with admin privileges:\n", :yellow
71
+ say " rails db_as:admin[migrate]", :yellow
72
+ say " Note: We must use the admin user because the app user doesn't have migration privileges", :yellow
73
+ say "\n3. Use 'rails generate rls_multi_tenant:model' for new multi-tenant models", :yellow
74
+ say "="*60, :green
75
+ end
76
+
77
+ private
78
+
79
+ def create_app_user_migrations_for_all_databases
80
+ # Get database configuration for current environment
81
+ db_config = Rails.application.config.database_configuration[Rails.env]
82
+
83
+ # Handle both single database and multiple databases configuration
84
+ databases_to_process = if db_config.is_a?(Hash) && db_config.key?('primary')
85
+ # Multiple databases configuration
86
+ db_config
87
+ else
88
+ # Single database configuration - treat as primary
89
+ { 'primary' => db_config }
90
+ end
91
+
92
+ databases_to_process.each do |db_name, config|
93
+ next if db_name == 'primary' # Skip primary database, handle it separately
94
+
95
+ # Check if migrations_paths is defined for this database
96
+ if config['migrations_paths']
97
+ migration_paths = Array(config['migrations_paths'])
98
+ migration_paths.each do |migration_path|
99
+ migration_dir = File.join(destination_root, migration_path)
100
+
101
+ # Check if migration already exists in this path
102
+ unless Dir.glob(File.join(migration_dir, "*_create_app_user.rb")).any?
103
+ FileUtils.mkdir_p(migration_dir) unless File.directory?(migration_dir)
104
+ create_migration_with_timestamp_for_path("create_app_user", 2, migration_path)
105
+ say "Created app user migration for #{db_name} in #{migration_path}", :green
106
+ else
107
+ say "App user migration already exists for #{db_name} in #{migration_path}, skipping creation", :yellow
108
+ end
109
+ end
110
+ else
111
+ say "No migrations_paths defined for database '#{db_name}', skipping app user migration", :yellow
112
+ end
113
+ end
114
+
115
+ # Handle primary database (default behavior)
116
+ unless Dir.glob(File.join(destination_root, "db/migrate/*_create_app_user.rb")).any?
117
+ create_migration_with_timestamp("create_app_user", 2)
118
+ else
119
+ say "App user migration already exists for primary database, skipping creation", :yellow
120
+ end
121
+ end
122
+
123
+ def create_migration_with_timestamp(migration_type, order)
124
+ base_timestamp = Time.current.strftime("%Y%m%d%H%M")
125
+ timestamp = "#{base_timestamp}#{sprintf('%02d', order)}"
126
+
127
+ case migration_type
128
+ when "enable_uuid"
129
+ render_shared_template "enable_uuid_extension.rb", "db/migrate/#{timestamp}_enable_uuid_extension.rb"
130
+ when "create_app_user"
131
+ render_shared_template "create_app_user.rb", "db/migrate/#{timestamp}_create_app_user.rb"
132
+ when "create_tenant"
133
+ tenant_class_name = RlsMultiTenant.tenant_class_name
134
+ render_shared_template "create_tenant.rb", "db/migrate/#{timestamp}_create_#{tenant_class_name.underscore.pluralize}.rb"
135
+ end
136
+ end
137
+
138
+ def create_migration_with_timestamp_for_path(migration_type, order, migration_path)
139
+ base_timestamp = Time.current.strftime("%Y%m%d%H%M")
140
+ timestamp = "#{base_timestamp}#{sprintf('%02d', order)}"
141
+
142
+ case migration_type
143
+ when "create_app_user"
144
+ render_shared_template "create_app_user.rb", "#{migration_path}/#{timestamp}_create_app_user.rb"
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Tenant < ApplicationRecord
3
+ class <%= RlsMultiTenant.tenant_class_name %> < ApplicationRecord
4
4
  include RlsMultiTenant::Concerns::TenantContext
5
5
 
6
6
  validates :name, presence: true
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ module Generators
5
+ module Shared
6
+ module TemplateHelper
7
+ extend ActiveSupport::Concern
8
+
9
+ private
10
+
11
+ def shared_template_path
12
+ File.expand_path("templates", File.dirname(__FILE__))
13
+ end
14
+
15
+ def copy_shared_template(template_name, destination_path)
16
+ template_path = File.join(shared_template_path, template_name)
17
+ copy_file template_path, destination_path
18
+ end
19
+
20
+ def shared_template_exists?(template_name)
21
+ File.exist?(File.join(shared_template_path, template_name))
22
+ end
23
+
24
+ def render_shared_template(template_name, destination_path, context = {})
25
+ template_path = File.join(shared_template_path, template_name)
26
+ template template_path, destination_path, context
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ class Create<%= RlsMultiTenant.tenant_class_name.pluralize %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ create_table :<%= RlsMultiTenant.tenant_class_name.underscore.pluralize %>, id: :uuid do |t|
4
+ t.string :name, null: false
5
+
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :<%= RlsMultiTenant.tenant_class_name.underscore.pluralize %>, :name, unique: true
10
+ end
11
+ end
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/generators'
4
+ require 'rls_multi_tenant/generators/shared/template_helper'
4
5
 
5
6
  module RlsMultiTenant
6
7
  module Generators
7
8
  class TaskGenerator < Rails::Generators::Base
9
+ include Shared::TemplateHelper
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
12
 
10
13
  desc "Generate RLS Multi-tenant rake tasks"
11
14
 
12
15
  def create_db_admin_task
13
- template "db_admin.rake", "lib/tasks/db_admin.rake"
16
+ copy_shared_template "db_admin.rake", "lib/tasks/db_admin.rake"
14
17
  show_instructions
15
18
  end
16
19
 
@@ -5,6 +5,7 @@ module RlsMultiTenant
5
5
  # Load generators after Rails is fully initialized
6
6
  initializer "rls_multi_tenant.load_generators", after: :set_routes_reloader do |app|
7
7
  require "rls_multi_tenant/generators/install/install_generator"
8
+ require "rls_multi_tenant/generators/setup/setup_generator"
8
9
  require "rls_multi_tenant/generators/migration/migration_generator"
9
10
  require "rls_multi_tenant/generators/model/model_generator"
10
11
  require "rls_multi_tenant/generators/task/task_generator"
@@ -16,8 +16,9 @@ module RlsMultiTenant
16
16
  policy_name = "#{table_name}_app_user"
17
17
  ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
18
18
 
19
+ tenant_session_var = "rls.#{RlsMultiTenant.tenant_id_column}"
19
20
  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
+ "USING (#{tenant_column} = NULLIF(current_setting('#{tenant_session_var}', TRUE), '')::uuid)"
21
22
 
22
23
  ActiveRecord::Base.connection.execute(policy_sql)
23
24
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RlsMultiTenant
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -5,9 +5,7 @@ require "rls_multi_tenant/concerns/multi_tenant"
5
5
  require "rls_multi_tenant/concerns/tenant_context"
6
6
  require "rls_multi_tenant/security_validator"
7
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)
8
+ require "rls_multi_tenant/generators/shared/template_helper"
11
9
  require "rls_multi_tenant/railtie" if defined?(Rails)
12
10
 
13
11
  module RlsMultiTenant
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "https://github.com/codingways/rls_multi_tenant"
19
19
  spec.metadata["changelog_uri"] = "https://github.com/codingways/rls_multi_tenant/blob/main/CHANGELOG.md"
20
+ spec.metadata["bug_tracker_uri"] = "https://github.com/codingways/rls_multi_tenant/issues"
21
+ spec.metadata["documentation_uri"] = "https://github.com/codingways/rls_multi_tenant#readme"
20
22
 
21
23
  # Specify which files should be added to the gem when it is released.
22
24
  spec.files = Dir.chdir(__dir__) do
@@ -30,8 +32,8 @@ Gem::Specification.new do |spec|
30
32
  spec.require_paths = ["lib"]
31
33
 
32
34
  # Dependencies
33
- spec.add_dependency "rails", ">= 7.0"
34
- spec.add_dependency "pg", ">= 1.0"
35
+ spec.add_dependency "rails", ">= 6.0", "< 9.0"
36
+ spec.add_dependency "pg", ">= 1.0", "< 3.0"
35
37
 
36
38
  # Development dependencies
37
39
  spec.add_development_dependency "rspec-rails", "~> 6.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rls_multi_tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Coding Ways
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-10 00:00:00.000000000 Z
11
+ date: 2025-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '7.0'
19
+ version: '6.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '7.0'
29
+ version: '6.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: pg
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -31,6 +37,9 @@ dependencies:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
39
  version: '1.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '3.0'
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +47,9 @@ dependencies:
38
47
  - - ">="
39
48
  - !ruby/object:Gem::Version
40
49
  version: '1.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
41
53
  - !ruby/object:Gem::Dependency
42
54
  name: rspec-rails
43
55
  requirement: !ruby/object:Gem::Requirement
@@ -151,23 +163,20 @@ files:
151
163
  - lib/rls_multi_tenant/concerns/multi_tenant.rb
152
164
  - lib/rls_multi_tenant/concerns/tenant_context.rb
153
165
  - lib/rls_multi_tenant/generators/install/install_generator.rb
154
- - lib/rls_multi_tenant/generators/install/templates/create_app_user.rb
155
- - lib/rls_multi_tenant/generators/install/templates/create_tenant.rb
156
- - lib/rls_multi_tenant/generators/install/templates/db_admin.rake
157
- - lib/rls_multi_tenant/generators/install/templates/enable_rls.rb
158
- - lib/rls_multi_tenant/generators/install/templates/enable_uuid_extension.rb
159
166
  - lib/rls_multi_tenant/generators/install/templates/rls_multi_tenant.rb
160
- - lib/rls_multi_tenant/generators/install/templates/tenant_model.rb
161
167
  - lib/rls_multi_tenant/generators/migration/migration_generator.rb
162
- - lib/rls_multi_tenant/generators/migration/templates/create_app_user.rb
163
- - lib/rls_multi_tenant/generators/migration/templates/create_tenant.rb
164
168
  - lib/rls_multi_tenant/generators/migration/templates/enable_rls.rb
165
- - lib/rls_multi_tenant/generators/migration/templates/enable_uuid_extension.rb
166
169
  - lib/rls_multi_tenant/generators/model/model_generator.rb
167
170
  - lib/rls_multi_tenant/generators/model/templates/migration.rb
168
171
  - lib/rls_multi_tenant/generators/model/templates/model.rb
172
+ - lib/rls_multi_tenant/generators/setup/setup_generator.rb
173
+ - lib/rls_multi_tenant/generators/setup/templates/tenant_model.rb
174
+ - lib/rls_multi_tenant/generators/shared/template_helper.rb
175
+ - lib/rls_multi_tenant/generators/shared/templates/create_app_user.rb
176
+ - lib/rls_multi_tenant/generators/shared/templates/create_tenant.rb
177
+ - lib/rls_multi_tenant/generators/shared/templates/db_admin.rake
178
+ - lib/rls_multi_tenant/generators/shared/templates/enable_uuid_extension.rb
169
179
  - lib/rls_multi_tenant/generators/task/task_generator.rb
170
- - lib/rls_multi_tenant/generators/task/templates/db_admin.rake
171
180
  - lib/rls_multi_tenant/railtie.rb
172
181
  - lib/rls_multi_tenant/rls_helper.rb
173
182
  - lib/rls_multi_tenant/rls_multi_tenant.rb
@@ -181,6 +190,8 @@ metadata:
181
190
  homepage_uri: https://github.com/codingways/rls_multi_tenant
182
191
  source_code_uri: https://github.com/codingways/rls_multi_tenant
183
192
  changelog_uri: https://github.com/codingways/rls_multi_tenant/blob/main/CHANGELOG.md
193
+ bug_tracker_uri: https://github.com/codingways/rls_multi_tenant/issues
194
+ documentation_uri: https://github.com/codingways/rls_multi_tenant#readme
184
195
  post_install_message:
185
196
  rdoc_options: []
186
197
  require_paths:
@@ -1,20 +0,0 @@
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
@@ -1,27 +0,0 @@
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
@@ -1,45 +0,0 @@
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
-
22
- # Grant default permissions for future tables in public schema
23
- execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO #{app_user};"
24
- execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO #{app_user};"
25
-
26
- # Grant permissions on system tables
27
- execute "GRANT SELECT ON TABLE schema_migrations TO #{app_user};"
28
- execute "GRANT SELECT ON TABLE ar_internal_metadata TO #{app_user};"
29
- end
30
-
31
- def down
32
- app_user = ENV['<%= RlsMultiTenant.app_user_env_var %>']
33
-
34
- # Revoke permissions
35
- execute "REVOKE ALL ON SCHEMA public FROM #{app_user};"
36
- execute "REVOKE CONNECT ON DATABASE #{ActiveRecord::Base.connection.current_database} FROM #{app_user};"
37
-
38
- # Revoke default permissions for future tables in public schema
39
- execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM #{app_user};"
40
- execute "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE, SELECT ON SEQUENCES FROM #{app_user};"
41
-
42
- # Drop user
43
- execute "DROP ROLE IF EXISTS #{app_user};"
44
- end
45
- end
@@ -1,20 +0,0 @@
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
@@ -1,5 +0,0 @@
1
- class EnableUuidExtension < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
- def change
3
- enable_extension 'uuid-ossp'
4
- end
5
- end
@@ -1,24 +0,0 @@
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