rls_multi_tenant 0.1.5 → 0.1.7

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: f313946d33d5edb4a96fec341e7ab62fc325ab0d6700bad0aef7b05d6f240ec4
4
- data.tar.gz: 0bbbdf115873c98b5487569cd21e63822fd1b2419b7eec55bbb875a5c131a417
3
+ metadata.gz: ef549a91c89a2d4c0b4ee20af9711d31fb807b3b61d115c6655e491a35ef1bcc
4
+ data.tar.gz: 4aabdf36264d22878785a8e737b67c7dca87a4700989d149dda0e72b13cbc6be
5
5
  SHA512:
6
- metadata.gz: fe257329e8fff95796cc6b4510ce1672ddf16e86b572d0113fedcc902afa69fc7e13d925b4d2d5e0a0bb1a6bcc5692e1bdc477fe8aab365b4b838d3fbedf124f
7
- data.tar.gz: 4ad4b27347d478b42e720c33c178eb655860d93809f0403fa58ff5197dbae98e563064e654ea51bdbabf0c3ae272e5871de402e1f6785fac0b3e39971a890341
6
+ metadata.gz: 31152dc5b94a7f4a9c12ec3eaf3aed96ca8fc8e1080e2918b68b85a65b9d9d8fa34f7ae8c7472b2daa77dafa5d00f368a362d2473732c940c26e860bc4a93155
7
+ data.tar.gz: 1bbab04fd97a40d5f07254f0f75e1ed113950309d1029840238dc572a5739a4f76e808b1e14b95f936334079c1776112401be9bdbff76ece4e794d2d30c8ad8a
data/README.md CHANGED
@@ -10,6 +10,7 @@ A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenanc
10
10
  - 📦 **Auto-inclusion**: Automatic model configuration
11
11
  - 🚀 **Generators**: Rails generators for quick setup
12
12
  - ⚙️ **Configurable**: Flexible configuration options
13
+ - 🌐 **Subdomain Middleware**: Automatic tenant switching based on subdomain
13
14
 
14
15
  ## Installation
15
16
 
@@ -40,6 +41,8 @@ bundle install
40
41
  config.tenant_id_column = :tenant_id # Tenant ID column name
41
42
  config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
42
43
  config.enable_security_validation = true # Enable security checks
44
+ config.enable_subdomain_middleware = true # Enable subdomain-based tenant switching (default: true)
45
+ config.subdomain_field = :subdomain # Field to use for subdomain matching (default: :subdomain)
43
46
  end
44
47
  ```
45
48
 
@@ -75,32 +78,78 @@ Your models automatically include the `MultiTenant` concern:
75
78
  ```ruby
76
79
  class User < ApplicationRecord
77
80
  # Automatically includes MultiTenant concern
78
- # include RlsMultiTenant::Concerns::MultiTenant
81
+ include RlsMultiTenant::Concerns::MultiTenant
79
82
  end
80
83
  ```
81
84
 
82
85
  ### Tenant Context Switching
83
86
 
84
87
  ```ruby
85
- # Create a new tenant
86
- tenant = Tenant.create!(name: "Tenant 1")
88
+ # Create a new tenant with subdomain
89
+ tenant = Tenant.create!(name: "Company A", subdomain: "company-a")
87
90
  ```
88
91
 
89
92
  ```ruby
90
93
  # Switch tenant context for a block
91
94
  Tenant.switch(tenant) do
92
- User.create!(name: "User from Tenant 1", email: "user@example.com") # Automatically assigned to current tenant
95
+ User.create!(name: "User from Company A", email: "user@company-a.com") # Automatically assigned to current tenant
93
96
  end
94
97
 
95
98
  # Switch tenant context permanently
96
99
  Tenant.switch!(tenant)
97
- User.create!(name: "User from Tenant 1", email: "user@example.com")
100
+ User.create!(name: "User from Company A", email: "user@company-a.com")
98
101
  Tenant.reset! # Reset context
99
102
 
100
103
  # Get current tenant
101
104
  current_tenant = Tenant.current
102
105
  ```
103
106
 
107
+ ### Automatic Subdomain-Based Tenant Switching
108
+
109
+ The gem includes middleware that automatically switches tenants based on the request subdomain. This is enabled by default and works seamlessly with your tenant model.
110
+
111
+ **The middleware automatically:**
112
+ - Extracts the subdomain from the request host
113
+ - Finds the matching tenant by the subdomain field
114
+ - Switches the tenant context for the duration of the request
115
+ - Resets the context after the request completes
116
+
117
+ **Usage:**
118
+ ```ruby
119
+ # Create tenants with subdomains
120
+ tenant1 = Tenant.create!(name: "Company A", subdomain: "company-a")
121
+ tenant2 = Tenant.create!(name: "Company B", subdomain: "company-b")
122
+
123
+ # Users visiting company-a.yourdomain.com will automatically be in tenant1's context
124
+ # Users visiting company-b.yourdomain.com will automatically be in tenant2's context
125
+ # Users visiting yourdomain.com (no subdomain) will have no tenant context
126
+ ```
127
+
128
+ ### Public Access (Non-Tenanted Models)
129
+
130
+ Models that don't include `RlsMultiTenant::Concerns::TenantContext` are automatically treated as public models and can be accessed without tenant context. This provides a secure, explicit way to separate tenant-specific and public models.
131
+
132
+ **Example:**
133
+ ```ruby
134
+ # Public models (no tenant association)
135
+ class PublicPost < ApplicationRecord
136
+ # No TenantContext concern included
137
+ # These models are accessible without tenant context
138
+ end
139
+
140
+ # Tenant-specific models (automatically generated)
141
+ class User < ApplicationRecord
142
+ # Automatically includes MultiTenant concern
143
+ # These models require tenant context and are constrained by RLS
144
+ include RlsMultiTenant::Concerns::MultiTenant
145
+ end
146
+ ```
147
+
148
+ **Security Benefits:**
149
+ - **Explicit Intent**: Models must explicitly include `TenantContext` to be tenant-constrained
150
+ - **Fail-Safe**: Public models are clearly separated from tenant models
151
+ - **No Configuration Drift**: Can't accidentally expose tenant data through misconfiguration
152
+
104
153
  ## Configuration
105
154
 
106
155
  The gem is configured in `config/initializers/rls_multi_tenant.rb` (created by the install generator). You can customize the following options:
@@ -111,6 +160,8 @@ RlsMultiTenant.configure do |config|
111
160
  config.tenant_id_column = :tenant_id # Tenant ID column name
112
161
  config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
113
162
  config.enable_security_validation = true # Enable security checks (prevents running with superuser privileges)
163
+ config.enable_subdomain_middleware = true # Enable subdomain-based tenant switching (default: true)
164
+ config.subdomain_field = :subdomain # Field to use for subdomain matching (default: :subdomain)
114
165
  end
115
166
  ```
116
167
 
@@ -126,6 +177,9 @@ rails db_as:admin[seed]
126
177
 
127
178
  # Create database with admin privileges
128
179
  rails db_as:admin[create]
180
+
181
+ # Drop database with admin privileges
182
+ rails db_as:admin[drop]
129
183
  ```
130
184
 
131
185
  ## Security Features
@@ -16,8 +16,13 @@ module RlsMultiTenant
16
16
 
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?
20
21
  self.send("#{RlsMultiTenant.tenant_id_column}=", current_tenant.id)
22
+ elsif current_tenant.nil?
23
+ raise RlsMultiTenant::Error,
24
+ "Cannot create #{self.class.name} without tenant context. " \
25
+ "This model requires a tenant context. "
21
26
  end
22
27
  end
23
28
  end
@@ -12,6 +12,7 @@ module RlsMultiTenant
12
12
  def tenant_session_var
13
13
  "rls.#{RlsMultiTenant.tenant_id_column}"
14
14
  end
15
+
15
16
  # Switch tenant context for a block
16
17
  def switch(tenant_or_id)
17
18
  tenant_id = extract_tenant_id(tenant_or_id)
@@ -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 InstallGenerator < Rails::Generators::Base
9
+ include Shared::TemplateHelper
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
12
 
10
13
  desc "Install RLS Multi-tenant gem configuration"
@@ -13,6 +16,14 @@ module RlsMultiTenant
13
16
  template "rls_multi_tenant.rb", "config/initializers/rls_multi_tenant.rb"
14
17
  end
15
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
25
+ end
26
+
16
27
  def show_instructions
17
28
  say "\n" + "="*60, :green
18
29
  say "RLS Multi-tenant gem configuration installed successfully!", :green
@@ -12,4 +12,11 @@ RlsMultiTenant.configure do |config|
12
12
 
13
13
  # Enable/disable security validation
14
14
  config.enable_security_validation = true
15
+
16
+ # Enable/disable subdomain-based tenant switching middleware
17
+ config.enable_subdomain_middleware = true
18
+
19
+ # Configure the field to use for subdomain matching (default: :subdomain)
20
+ # This should be a field on your tenant model that contains the subdomain
21
+ config.subdomain_field = :subdomain
15
22
  end
@@ -7,15 +7,9 @@ class EnableRlsFor<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails
7
7
 
8
8
  # Create RLS policy
9
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
-
11
- # Grant permissions
12
- execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
13
10
  end
14
11
 
15
12
  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
13
  # Drop policy
20
14
  execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
21
15
 
@@ -1,7 +1,7 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
2
  def change
3
3
  # Create the table
4
- create_table :<%= table_name %>, id: :uuid do |t|
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
7
  t.<%= attribute.type %> :<%= attribute.name %>
@@ -13,15 +13,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= Rails.version.to
13
13
  # Enable Row Level Security
14
14
  execute "ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY"
15
15
 
16
- reversible do |dir|
17
- dir.up do
18
- execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
19
- end
20
- dir.down do
21
- execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
22
- end
23
- end
24
-
25
16
  # Define RLS policy
26
17
  reversible do |dir|
27
18
  dir.up do
@@ -23,14 +23,6 @@ module RlsMultiTenant
23
23
  end
24
24
  end
25
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
26
  def create_uuid_migration
35
27
  unless Dir.glob(File.join(destination_root, "db/migrate/*_enable_uuid_extension.rb")).any?
36
28
  create_migration_with_timestamp("enable_uuid", 1)
@@ -61,7 +53,6 @@ module RlsMultiTenant
61
53
  say "="*60, :green
62
54
  say "\nCreated:", :yellow
63
55
  say "• #{tenant_class_name} model", :green
64
- say "• Database admin task", :green
65
56
  say "• UUID extension migration", :green
66
57
  say "• App user migration(s)", :green
67
58
  say "• #{tenant_class_name} migration", :green
@@ -2,10 +2,12 @@ class Create<%= RlsMultiTenant.tenant_class_name.pluralize %> < ActiveRecord::Mi
2
2
  def change
3
3
  create_table :<%= RlsMultiTenant.tenant_class_name.underscore.pluralize %>, id: :uuid do |t|
4
4
  t.string :name, null: false
5
+ t.string :subdomain, null: false
5
6
 
6
7
  t.timestamps
7
8
  end
8
9
 
9
10
  add_index :<%= RlsMultiTenant.tenant_class_name.underscore.pluralize %>, :name, unique: true
11
+ add_index :<%= RlsMultiTenant.tenant_class_name.underscore.pluralize %>, :subdomain, unique: true
10
12
  end
11
13
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ module Middleware
5
+ class SubdomainTenantSelector
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ request = ActionDispatch::Request.new(env)
12
+ tenant = resolve_tenant_from_subdomain(request)
13
+
14
+ if tenant
15
+ # Switch tenant context for the duration of the request
16
+ Rails.logger.info "[RLS Multi-Tenant] #{request.method} #{request.path} -> Tenant: #{tenant.name} (#{tenant.id})" if defined?(Rails)
17
+ RlsMultiTenant.tenant_class.switch(tenant) do
18
+ @app.call(env)
19
+ end
20
+ else
21
+ # No tenant found - check if we need tenant context
22
+ subdomain = extract_subdomain(request.host)
23
+ if subdomain.present? && subdomain != 'www'
24
+ # Subdomain exists but no tenant found - this is an error
25
+ Rails.logger.warn "[RLS Multi-Tenant] #{request.method} #{request.path} -> No tenant found for subdomain '#{subdomain}'" if defined?(Rails)
26
+ raise RlsMultiTenant::Error, "No tenant found for subdomain '#{subdomain}'. Please ensure the tenant exists with the correct subdomain."
27
+ end
28
+ # If no subdomain, allow access to public models (models without TenantContext)
29
+ # Models that include TenantContext will automatically be constrained by RLS
30
+ Rails.logger.info "[RLS Multi-Tenant] #{request.method} #{request.path} -> Public access (no subdomain)" if defined?(Rails)
31
+ @app.call(env)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def resolve_tenant_from_subdomain(request)
38
+ subdomain = extract_subdomain(request.host)
39
+ return nil if subdomain.blank? || subdomain == 'www'
40
+
41
+ # Look up tenant by subdomain only
42
+ tenant_class = RlsMultiTenant.tenant_class
43
+ subdomain_field = RlsMultiTenant.subdomain_field
44
+
45
+ # Only allow subdomain-based lookup
46
+ unless tenant_class.column_names.include?(subdomain_field.to_s)
47
+ raise RlsMultiTenant::ConfigurationError,
48
+ "Subdomain field '#{subdomain_field}' not found on #{tenant_class.name}. " \
49
+ "Please add a '#{subdomain_field}' column to your tenant model or configure a different subdomain_field."
50
+ end
51
+
52
+ tenant_class.find_by(subdomain_field => subdomain)
53
+ rescue => e
54
+ Rails.logger.error "Failed to resolve tenant from subdomain '#{subdomain}': #{e.message}" if defined?(Rails)
55
+ nil
56
+ end
57
+
58
+ def extract_subdomain(host)
59
+ return nil if host.blank?
60
+
61
+ # Remove port if present
62
+ host = host.split(':').first
63
+
64
+ # Split by dots and get the first part (subdomain)
65
+ parts = host.split('.')
66
+
67
+ # Handle localhost development (e.g., foo.localhost:3000)
68
+ if parts.length == 2 && parts.last == 'localhost'
69
+ parts.first
70
+ # Handle standard domains (e.g., foo.example.com)
71
+ elsif parts.length >= 3
72
+ parts.first
73
+ else
74
+ nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -34,5 +34,11 @@ module RlsMultiTenant
34
34
  end
35
35
  end
36
36
  end
37
+
38
+ initializer "rls_multi_tenant.middleware", after: :load_config_initializers do |app|
39
+ if RlsMultiTenant.enable_subdomain_middleware
40
+ app.config.middleware.use RlsMultiTenant::Middleware::SubdomainTenantSelector
41
+ end
42
+ end
37
43
  end
38
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RlsMultiTenant
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
@@ -5,6 +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/middleware/subdomain_tenant_selector"
8
9
  require "rls_multi_tenant/generators/shared/template_helper"
9
10
  require "rls_multi_tenant/railtie" if defined?(Rails)
10
11
 
@@ -15,7 +16,7 @@ module RlsMultiTenant
15
16
 
16
17
  # Configuration options
17
18
  class << self
18
- attr_accessor :tenant_class_name, :tenant_id_column, :app_user_env_var, :enable_security_validation
19
+ attr_accessor :tenant_class_name, :tenant_id_column, :app_user_env_var, :enable_security_validation, :enable_subdomain_middleware, :subdomain_field
19
20
 
20
21
  def configure
21
22
  yield self
@@ -36,6 +37,14 @@ module RlsMultiTenant
36
37
  def enable_security_validation
37
38
  @enable_security_validation.nil? ? true : @enable_security_validation
38
39
  end
40
+
41
+ def enable_subdomain_middleware
42
+ @enable_subdomain_middleware.nil? ? false : @enable_subdomain_middleware
43
+ end
44
+
45
+ def subdomain_field
46
+ @subdomain_field ||= :subdomain
47
+ end
39
48
  end
40
49
 
41
50
  # Default configuration
@@ -43,4 +52,6 @@ module RlsMultiTenant
43
52
  self.tenant_id_column = :tenant_id
44
53
  self.app_user_env_var = "POSTGRES_APP_USER"
45
54
  self.enable_security_validation = true
55
+ self.enable_subdomain_middleware = true
56
+ self.subdomain_field = :subdomain
46
57
  end
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.5
4
+ version: 0.1.7
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-15 00:00:00.000000000 Z
11
+ date: 2025-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -177,6 +177,7 @@ files:
177
177
  - lib/rls_multi_tenant/generators/shared/templates/db_admin.rake
178
178
  - lib/rls_multi_tenant/generators/shared/templates/enable_uuid_extension.rb
179
179
  - lib/rls_multi_tenant/generators/task/task_generator.rb
180
+ - lib/rls_multi_tenant/middleware/subdomain_tenant_selector.rb
180
181
  - lib/rls_multi_tenant/railtie.rb
181
182
  - lib/rls_multi_tenant/rls_helper.rb
182
183
  - lib/rls_multi_tenant/rls_multi_tenant.rb