rls_multi_tenant 0.1.6 → 0.1.8

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: 8c3310349a1bab642ea0facd9a4199d3b63e075e9eb15905b00a86075285f891
4
- data.tar.gz: '06494c0144141a2ad9ac97b57091ebfc7198472342d9b7a9e5da2609e5928c9a'
3
+ metadata.gz: 3fb27124134b3487ef6f83347eca26c0ac64431e44a01d92cf70bd3dd5a03e20
4
+ data.tar.gz: a4fceedb11cdc42c788aad2c8c8ac11f6344d497ec2f62b002719c7cfc5a6179
5
5
  SHA512:
6
- metadata.gz: 4231b3ec6287ef0bb2223284b991c7951149ab2f608c755a823b451a6527134ce64a11a24b988a74e62ebc692585aaedb99d738b8a7055d01b570cff4cc354d9
7
- data.tar.gz: 640196b111037946feee80e8a516d32f0a09b00512c277cff68dd1c011794d3f7b827490b0c003810642c90151b06ef14ab02223bd8b1bc85fae741803b161f3
6
+ metadata.gz: adff1ce5bde6c15d358e67041982e0506c97fa5b1e7eee02fd69fbf9eab3799fa67be034a8b584ea8cf5864a556751d1d8a679e3adf49cddbedc995d988b98d7
7
+ data.tar.gz: 0c125b52250a2fce049e4f0965af09c4ae13f75f8a7516b58f6a6d136179821471a2f7ff24340ba3654f2cfa1ec4bcf2af29d4ab76ddb58ddad2e3643edc5dbd
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --color
3
+ --format documentation
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-rails
3
3
  - rubocop-rspec
4
4
 
@@ -22,7 +22,7 @@ Style/FrozenStringLiteralComment:
22
22
  Style/ClassAndModuleChildren:
23
23
  Enabled: false
24
24
 
25
- Metrics/LineLength:
25
+ Layout/LineLength:
26
26
  Max: 120
27
27
 
28
28
  Metrics/BlockLength:
data/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # RLS Multi-Tenant
2
2
 
3
+ [![CI](https://github.com/codingways/rls_multi_tenant/actions/workflows/simple.yml/badge.svg)](https://github.com/codingways/rls_multi_tenant/actions/workflows/simple.yml)
4
+ [![Ruby](https://img.shields.io/badge/ruby-3.0%2B-red.svg)](https://www.ruby-lang.org/)
5
+ [![Rails](https://img.shields.io/badge/rails-6.0%2B-red.svg)](https://rubyonrails.org/)
6
+ [![Gem Version](https://badge.fury.io/rb/rls_multi_tenant.svg)](https://badge.fury.io/rb/rls_multi_tenant)
7
+ [![Downloads](https://img.shields.io/gem/dt/rls_multi_tenant.svg)](https://rubygems.org/gems/rls_multi_tenant)
8
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
9
+
3
10
  A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenancy for Rails applications.
4
11
 
5
12
  ## Features
@@ -10,6 +17,7 @@ A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenanc
10
17
  - 📦 **Auto-inclusion**: Automatic model configuration
11
18
  - 🚀 **Generators**: Rails generators for quick setup
12
19
  - ⚙️ **Configurable**: Flexible configuration options
20
+ - 🌐 **Subdomain Middleware**: Automatic tenant switching based on subdomain
13
21
 
14
22
  ## Installation
15
23
 
@@ -40,6 +48,8 @@ bundle install
40
48
  config.tenant_id_column = :tenant_id # Tenant ID column name
41
49
  config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
42
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)
43
53
  end
44
54
  ```
45
55
 
@@ -75,32 +85,78 @@ Your models automatically include the `MultiTenant` concern:
75
85
  ```ruby
76
86
  class User < ApplicationRecord
77
87
  # Automatically includes MultiTenant concern
78
- # include RlsMultiTenant::Concerns::MultiTenant
88
+ include RlsMultiTenant::Concerns::MultiTenant
79
89
  end
80
90
  ```
81
91
 
82
92
  ### Tenant Context Switching
83
93
 
84
94
  ```ruby
85
- # Create a new tenant
86
- tenant = Tenant.create!(name: "Tenant 1")
95
+ # Create a new tenant with subdomain
96
+ tenant = Tenant.create!(name: "Company A", subdomain: "company-a")
87
97
  ```
88
98
 
89
99
  ```ruby
90
100
  # Switch tenant context for a block
91
101
  Tenant.switch(tenant) do
92
- User.create!(name: "User from Tenant 1", email: "user@example.com") # Automatically assigned to current tenant
102
+ User.create!(name: "User from Company A", email: "user@company-a.com") # Automatically assigned to current tenant
93
103
  end
94
104
 
95
105
  # Switch tenant context permanently
96
106
  Tenant.switch!(tenant)
97
- User.create!(name: "User from Tenant 1", email: "user@example.com")
107
+ User.create!(name: "User from Company A", email: "user@company-a.com")
98
108
  Tenant.reset! # Reset context
99
109
 
100
110
  # Get current tenant
101
111
  current_tenant = Tenant.current
102
112
  ```
103
113
 
114
+ ### Automatic Subdomain-Based Tenant Switching
115
+
116
+ 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.
117
+
118
+ **The middleware automatically:**
119
+ - Extracts the subdomain from the request host
120
+ - Finds the matching tenant by the subdomain field
121
+ - Switches the tenant context for the duration of the request
122
+ - Resets the context after the request completes
123
+
124
+ **Usage:**
125
+ ```ruby
126
+ # Create tenants with subdomains
127
+ tenant1 = Tenant.create!(name: "Company A", subdomain: "company-a")
128
+ tenant2 = Tenant.create!(name: "Company B", subdomain: "company-b")
129
+
130
+ # Users visiting company-a.yourdomain.com will automatically be in tenant1's context
131
+ # Users visiting company-b.yourdomain.com will automatically be in tenant2's context
132
+ # Users visiting yourdomain.com (no subdomain) will have no tenant context
133
+ ```
134
+
135
+ ### Public Access (Non-Tenanted Models)
136
+
137
+ 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.
138
+
139
+ **Example:**
140
+ ```ruby
141
+ # Public models (no tenant association)
142
+ class PublicPost < ApplicationRecord
143
+ # No TenantContext concern included
144
+ # These models are accessible without tenant context
145
+ end
146
+
147
+ # Tenant-specific models (automatically generated)
148
+ class User < ApplicationRecord
149
+ # Automatically includes MultiTenant concern
150
+ # These models require tenant context and are constrained by RLS
151
+ include RlsMultiTenant::Concerns::MultiTenant
152
+ end
153
+ ```
154
+
155
+ **Security Benefits:**
156
+ - **Explicit Intent**: Models must explicitly include `TenantContext` to be tenant-constrained
157
+ - **Fail-Safe**: Public models are clearly separated from tenant models
158
+ - **No Configuration Drift**: Can't accidentally expose tenant data through misconfiguration
159
+
104
160
  ## Configuration
105
161
 
106
162
  The gem is configured in `config/initializers/rls_multi_tenant.rb` (created by the install generator). You can customize the following options:
@@ -111,6 +167,8 @@ RlsMultiTenant.configure do |config|
111
167
  config.tenant_id_column = :tenant_id # Tenant ID column name
112
168
  config.app_user_env_var = "POSTGRES_APP_USER" # Environment variable for app user
113
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)
114
172
  end
115
173
  ```
116
174
 
@@ -126,6 +184,9 @@ rails db_as:admin[seed]
126
184
 
127
185
  # Create database with admin privileges
128
186
  rails db_as:admin[create]
187
+
188
+ # Drop database with admin privileges
189
+ rails db_as:admin[drop]
129
190
  ```
130
191
 
131
192
  ## 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)
@@ -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
@@ -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 %>
@@ -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
@@ -48,8 +48,8 @@ module RlsMultiTenant
48
48
  private
49
49
 
50
50
  def skip_validation?
51
- # Skip validation if we're running install generator
52
- return true if ARGV.any? { |arg| arg.include?('rls_multi_tenant:install') }
51
+ # Skip validation if we're running an install or setup generator
52
+ return true if ARGV.any? { |arg| arg.include?('rls_multi_tenant:install') || arg.include?('rls_multi_tenant:setup') }
53
53
 
54
54
  # Skip validation if we're in admin mode (set by db_as:admin task)
55
55
  return true if ENV['RLS_MULTI_TENANT_ADMIN_MODE'] == 'true'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RlsMultiTenant
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.8"
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
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  # Dependencies
35
35
  spec.add_dependency "rails", ">= 6.0", "< 9.0"
36
36
  spec.add_dependency "pg", ">= 1.0", "< 3.0"
37
+ spec.add_dependency "ostruct", ">= 0.1.0"
37
38
 
38
39
  # Development dependencies
39
40
  spec.add_development_dependency "rspec-rails", "~> 6.0"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rls_multi_tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Coding Ways
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-09-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -50,6 +49,20 @@ dependencies:
50
49
  - - "<"
51
50
  - !ruby/object:Gem::Version
52
51
  version: '3.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: ostruct
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 0.1.0
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 0.1.0
53
66
  - !ruby/object:Gem::Dependency
54
67
  name: rspec-rails
55
68
  requirement: !ruby/object:Gem::Requirement
@@ -157,6 +170,7 @@ executables: []
157
170
  extensions: []
158
171
  extra_rdoc_files: []
159
172
  files:
173
+ - ".rspec"
160
174
  - ".rubocop.yml"
161
175
  - README.md
162
176
  - lib/rls_multi_tenant.rb
@@ -177,6 +191,7 @@ files:
177
191
  - lib/rls_multi_tenant/generators/shared/templates/db_admin.rake
178
192
  - lib/rls_multi_tenant/generators/shared/templates/enable_uuid_extension.rb
179
193
  - lib/rls_multi_tenant/generators/task/task_generator.rb
194
+ - lib/rls_multi_tenant/middleware/subdomain_tenant_selector.rb
180
195
  - lib/rls_multi_tenant/railtie.rb
181
196
  - lib/rls_multi_tenant/rls_helper.rb
182
197
  - lib/rls_multi_tenant/rls_multi_tenant.rb
@@ -192,7 +207,6 @@ metadata:
192
207
  changelog_uri: https://github.com/codingways/rls_multi_tenant/blob/main/CHANGELOG.md
193
208
  bug_tracker_uri: https://github.com/codingways/rls_multi_tenant/issues
194
209
  documentation_uri: https://github.com/codingways/rls_multi_tenant#readme
195
- post_install_message:
196
210
  rdoc_options: []
197
211
  require_paths:
198
212
  - lib
@@ -207,8 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
221
  - !ruby/object:Gem::Version
208
222
  version: '0'
209
223
  requirements: []
210
- rubygems_version: 3.5.23
211
- signing_key:
224
+ rubygems_version: 3.6.9
212
225
  specification_version: 4
213
226
  summary: Rails gem for PostgreSQL Row Level Security (RLS) multi-tenant applications
214
227
  test_files: []