Use a custom method to detect the current tenant
The subdomain elevator we were using, which is included in apartment, didn't work on hosts already including a subdomain (like demo.consul.dev, for instance). In those cases, we would manually add the subdomain to the list of excluded subdomains. Since these subdomains will be different for different CONSUL installations, it meant each installation had to customize the code. Furthermore, existing installations using subdomains would stop working. So we're using a custom method to find the current tenant, based on the host defined in `default_url_options`. In order to avoid any side-effects on single-tenant applications, we're adding a new configuration option to enable multitenancy We're enabling two ways to handle this configuration option: a) Change the application_custom.rb file, which is under version control b) Change the secrets.yml file, which is not under version control This way people prefering to handle configuration options through version control can do so, while people who prefer handling configuration options through te secrets.yml file can do so as well. We're also disabling the super-annoying warnings mentioning there are no tenants which we got every time we run migrations on single-tenant applications. These messages will only be enabled when the multitenancy feature is enabled too. For this reason, we're also disabling the multitenancy feature in the development environment by default.
This commit is contained in:
@@ -10,8 +10,25 @@ class Tenant < ApplicationRecord
|
|||||||
after_update :rename_schema
|
after_update :rename_schema
|
||||||
after_destroy :destroy_schema
|
after_destroy :destroy_schema
|
||||||
|
|
||||||
|
def self.resolve_host(host)
|
||||||
|
return nil unless Rails.application.config.multitenancy.present?
|
||||||
|
return nil if host.blank? || host.match?(Resolv::AddressRegex)
|
||||||
|
|
||||||
|
host_domain = allowed_domains.find { |domain| host == domain || host.ends_with?(".#{domain}") }
|
||||||
|
host.delete_prefix("www.").sub(/\.?#{host_domain}\Z/, "").presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.allowed_domains
|
||||||
|
dev_domains = %w[localhost lvh.me example.com]
|
||||||
|
dev_domains + [default_host]
|
||||||
|
end
|
||||||
|
|
||||||
def self.excluded_subdomains
|
def self.excluded_subdomains
|
||||||
Apartment::Elevators::Subdomain.excluded_subdomains + %w[mail shared_extensions]
|
%w[mail public shared_extensions www]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_host
|
||||||
|
ActionMailer::Base.default_url_options[:host]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.switch(...)
|
def self.switch(...)
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ module Consul
|
|||||||
config.autoload_paths << "#{Rails.root}/app/graphql/custom"
|
config.autoload_paths << "#{Rails.root}/app/graphql/custom"
|
||||||
config.autoload_paths << "#{Rails.root}/app/models/custom"
|
config.autoload_paths << "#{Rails.root}/app/models/custom"
|
||||||
config.paths["app/views"].unshift(Rails.root.join("app", "views", "custom"))
|
config.paths["app/views"].unshift(Rails.root.join("app", "views", "custom"))
|
||||||
|
|
||||||
|
# Set to true to enable managing different tenants using the same application
|
||||||
|
config.multitenancy = Rails.application.secrets.multitenancy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ Rails.application.configure do
|
|||||||
Bullet.raise = true # raise an error if n+1 query occurs
|
Bullet.raise = true # raise an error if n+1 query occurs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Allow managing different tenants using the same application
|
||||||
|
config.multitenancy = true
|
||||||
end
|
end
|
||||||
|
|
||||||
require Rails.root.join("config", "environments", "custom", "test")
|
require Rails.root.join("config", "environments", "custom", "test")
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# Apartment can support many different "Elevators" that can take care of this routing to your data.
|
# Apartment can support many different "Elevators" that can take care of this routing to your data.
|
||||||
# Require whichever Elevator you're using below or none if you have a custom one.
|
# Require whichever Elevator you're using below or none if you have a custom one.
|
||||||
#
|
#
|
||||||
# require "apartment/elevators/generic"
|
require "apartment/elevators/generic"
|
||||||
# require "apartment/elevators/domain"
|
# require "apartment/elevators/domain"
|
||||||
require "apartment/elevators/subdomain"
|
# require "apartment/elevators/subdomain"
|
||||||
# require "apartment/elevators/first_subdomain"
|
# require "apartment/elevators/first_subdomain"
|
||||||
# require "apartment/elevators/host"
|
# require "apartment/elevators/host"
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ require "apartment/elevators/subdomain"
|
|||||||
# Apartment Configuration
|
# Apartment Configuration
|
||||||
#
|
#
|
||||||
Apartment.configure do |config|
|
Apartment.configure do |config|
|
||||||
|
ENV["IGNORE_EMPTY_TENANTS"] = "true" if Rails.env.test? || Rails.application.config.multitenancy.blank?
|
||||||
config.seed_after_create = true
|
config.seed_after_create = true
|
||||||
|
|
||||||
# Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.
|
# Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.
|
||||||
@@ -106,12 +107,11 @@ end
|
|||||||
|
|
||||||
# Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
|
# Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
|
||||||
# you want to switch to.
|
# you want to switch to.
|
||||||
# Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
|
Rails.application.config.middleware.use Apartment::Elevators::Generic, ->(request) do
|
||||||
# request.host.split(".").first
|
Tenant.resolve_host(request.host)
|
||||||
# }
|
end
|
||||||
|
|
||||||
# Rails.application.config.middleware.use Apartment::Elevators::Domain
|
# Rails.application.config.middleware.use Apartment::Elevators::Domain
|
||||||
Rails.application.config.middleware.use Apartment::Elevators::Subdomain
|
# Rails.application.config.middleware.use Apartment::Elevators::Subdomain
|
||||||
# Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain
|
# Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain
|
||||||
# Rails.application.config.middleware.use Apartment::Elevators::Host
|
# Rails.application.config.middleware.use Apartment::Elevators::Host
|
||||||
Apartment::Elevators::Subdomain.excluded_subdomains = %w[public www]
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ http_basic_auth: &http_basic_auth
|
|||||||
development:
|
development:
|
||||||
http_basic_username: "dev"
|
http_basic_username: "dev"
|
||||||
http_basic_password: "pass"
|
http_basic_password: "pass"
|
||||||
|
multitenancy: false
|
||||||
secret_key_base: 56792feef405a59b18ea7db57b4777e855103882b926413d4afdfb8c0ea8aa86ea6649da4e729c5f5ae324c0ab9338f789174cf48c544173bc18fdc3b14262e4
|
secret_key_base: 56792feef405a59b18ea7db57b4777e855103882b926413d4afdfb8c0ea8aa86ea6649da4e729c5f5ae324c0ab9338f789174cf48c544173bc18fdc3b14262e4
|
||||||
<<: *maps
|
<<: *maps
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ staging:
|
|||||||
http_basic_password: ""
|
http_basic_password: ""
|
||||||
managers_url: ""
|
managers_url: ""
|
||||||
managers_application_key: ""
|
managers_application_key: ""
|
||||||
|
multitenancy: false
|
||||||
<<: *maps
|
<<: *maps
|
||||||
<<: *apis
|
<<: *apis
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ preproduction:
|
|||||||
http_basic_password: ""
|
http_basic_password: ""
|
||||||
managers_url: ""
|
managers_url: ""
|
||||||
managers_application_key: ""
|
managers_application_key: ""
|
||||||
|
multitenancy: false
|
||||||
twitter_key: ""
|
twitter_key: ""
|
||||||
twitter_secret: ""
|
twitter_secret: ""
|
||||||
facebook_key: ""
|
facebook_key: ""
|
||||||
@@ -105,6 +108,7 @@ production:
|
|||||||
http_basic_password: ""
|
http_basic_password: ""
|
||||||
managers_url: ""
|
managers_url: ""
|
||||||
managers_application_key: ""
|
managers_application_key: ""
|
||||||
|
multitenancy: false
|
||||||
twitter_key: ""
|
twitter_key: ""
|
||||||
twitter_secret: ""
|
twitter_secret: ""
|
||||||
facebook_key: ""
|
facebook_key: ""
|
||||||
|
|||||||
@@ -1,6 +1,138 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
describe Tenant do
|
describe Tenant do
|
||||||
|
describe ".resolve_host" do
|
||||||
|
before do
|
||||||
|
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for empty hosts" do
|
||||||
|
expect(Tenant.resolve_host("")).to be nil
|
||||||
|
expect(Tenant.resolve_host(nil)).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for IP addresses" do
|
||||||
|
expect(Tenant.resolve_host("127.0.0.1")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil using development and test domains" do
|
||||||
|
expect(Tenant.resolve_host("localhost")).to be nil
|
||||||
|
expect(Tenant.resolve_host("lvh.me")).to be nil
|
||||||
|
expect(Tenant.resolve_host("example.com")).to be nil
|
||||||
|
expect(Tenant.resolve_host("www.example.com")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "treats lvh.me as localhost" do
|
||||||
|
expect(Tenant.resolve_host("jupiter.lvh.me")).to eq "jupiter"
|
||||||
|
expect(Tenant.resolve_host("www.lvh.me")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for the default host" do
|
||||||
|
expect(Tenant.resolve_host("consul.dev")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix" do
|
||||||
|
expect(Tenant.resolve_host("www.consul.dev")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns subdomains when present" do
|
||||||
|
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix when subdomains are present" do
|
||||||
|
expect(Tenant.resolve_host("www.saturn.consul.dev")).to eq "saturn"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nested additional subdomains" do
|
||||||
|
expect(Tenant.resolve_host("europa.jupiter.consul.dev")).to eq "europa.jupiter"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix in additional nested subdomains" do
|
||||||
|
expect(Tenant.resolve_host("www.europa.jupiter.consul.dev")).to eq "europa.jupiter"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not ignore www if it isn't the prefix" do
|
||||||
|
expect(Tenant.resolve_host("wwwsaturn.consul.dev")).to eq "wwwsaturn"
|
||||||
|
expect(Tenant.resolve_host("saturn.www.consul.dev")).to eq "saturn.www"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the host as a subdomain" do
|
||||||
|
expect(Tenant.resolve_host("consul.dev.consul.dev")).to eq "consul.dev"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nested subdomains containing the host" do
|
||||||
|
expect(Tenant.resolve_host("saturn.consul.dev.consul.dev")).to eq "saturn.consul.dev"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns full domains when they don't contain the host" do
|
||||||
|
expect(Tenant.resolve_host("unrelated.dev")).to eq "unrelated.dev"
|
||||||
|
expect(Tenant.resolve_host("mercury.anotherconsul.dev")).to eq "mercury.anotherconsul.dev"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix in full domains" do
|
||||||
|
expect(Tenant.resolve_host("www.unrelated.dev")).to eq "unrelated.dev"
|
||||||
|
expect(Tenant.resolve_host("www.mercury.anotherconsul.dev")).to eq "mercury.anotherconsul.dev"
|
||||||
|
end
|
||||||
|
|
||||||
|
context "multitenancy disabled" do
|
||||||
|
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
|
||||||
|
|
||||||
|
it "always returns nil" do
|
||||||
|
expect(Tenant.resolve_host("saturn.consul.dev")).to be nil
|
||||||
|
expect(Tenant.resolve_host("jupiter.lvh.me")).to be nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "default host contains subdomains" do
|
||||||
|
before do
|
||||||
|
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "demo.consul.dev" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores subdomains already present in the default host" do
|
||||||
|
expect(Tenant.resolve_host("demo.consul.dev")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix" do
|
||||||
|
expect(Tenant.resolve_host("www.demo.consul.dev")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns additional subdomains" do
|
||||||
|
expect(Tenant.resolve_host("saturn.demo.consul.dev")).to eq "saturn"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix in additional subdomains" do
|
||||||
|
expect(Tenant.resolve_host("www.saturn.demo.consul.dev")).to eq "saturn"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nested additional subdomains" do
|
||||||
|
expect(Tenant.resolve_host("europa.jupiter.demo.consul.dev")).to eq "europa.jupiter"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores the www prefix in additional nested subdomains" do
|
||||||
|
expect(Tenant.resolve_host("www.europa.jupiter.demo.consul.dev")).to eq "europa.jupiter"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not ignore www if it isn't the prefix" do
|
||||||
|
expect(Tenant.resolve_host("wwwsaturn.demo.consul.dev")).to eq "wwwsaturn"
|
||||||
|
expect(Tenant.resolve_host("saturn.www.demo.consul.dev")).to eq "saturn.www"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "default host is similar to development and test domains" do
|
||||||
|
before do
|
||||||
|
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "mylvh.me" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for the default host" do
|
||||||
|
expect(Tenant.resolve_host("mylvh.me")).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns subdomains when present" do
|
||||||
|
expect(Tenant.resolve_host("neptune.mylvh.me")).to eq "neptune"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "validations" do
|
describe "validations" do
|
||||||
let(:tenant) { build(:tenant) }
|
let(:tenant) { build(:tenant) }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user