I am about to begin writing a Rails application that will allow clients to have a separate subdomain for their access to our application. Thinking from a data security standpoint, it would be nice if each client's access was truly limited to their database, that way, if there is a bug in production code, they would only be able to access their own database and not that of any other clients.
I know the code behind how to do what I want, but I was wondering if there was a simpler solution that I might be missing. How would you go about securing client data so that in the event of a bug or hacker threat, their data would be less likely to be exposed?
Here is some code I use for this very problem:
application_controller.rb
before_filter :set_database
helper_method :current_website
# I use the entire domain, just change to find_by_subdomain and pass only the subdomain
def current_website
#website ||= Website.find_by_domain(request.host)
end
def set_database
current_website.use_database
end
# Bonus - add view_path
def set_paths
self.prepend_view_path current_website.view_path unless current_website.view_path.blank?
end
Website.rb
def use_database
ActiveRecord::Base.establish_connection(website_connection)
end
# Revert back to the shared database
def revert_database
ActiveRecord::Base.establish_connection(default_connection)
end
private
# Regular database.yml configuration hash
def default_connection
#default_config ||= ActiveRecord::Base.connection.instance_variable_get("#config").dup
end
# Return regular connection hash but with database name changed
# The database name is a attribute (column in the database)
def website_connection
default_connection.dup.update(:database => database_name)
end
Hope this helps!
I found a different solution that works a little easier, but makes the assumption you have a database for each Subdomain:
application_controller.rb
before_filter :subdomain_change_database
def subdomain_change_database
if request.subdomain.present? && request.subdomain != "www"
# 'SOME_PREFIX_' + is optional, but would make DBs easier to delineate
ActiveRecord::Base.establish_connection(website_connection('SOME_PREFIX_' + request.subdomain ))
end
end
# Return regular connection hash but with database name changed
# The database name is a attribute (column in the database)
def website_connection(subdomain)
default_connection.dup.update(:database => subdomain)
end
# Regular database.yml configuration hash
def default_connection
#default_config ||= ActiveRecord::Base.connection.instance_variable_get("#config").dup
end
This will switch to a database like mydb_subdomain. This is a complete replacement database option, but it makes it super easy to roll out multiple versions.
Turns out I just asked a really similar question but quite a bit further along in development - I've included three ideas for how to go about securely using a single database in there.
Off the top of my head you could run a new server instance for each subdomain using different environment.
But that won't scale very well.
However the first few google hits for multiple rails databases turn up some new suggestions. Putting together the information in those links provides this wholly untested solution for a single server instance.
You'll need to add a database entry for each subdomain in your databases.yml. Then add a before_filter to your application controller
Update! Example reloads the database configurations dynamically. Unfortunately there's no good way to make the update rails wide without messing with your server's internals. So the database configuration will have to be reloaded on every request.
This example assumes database entries in databases.yml are named after subdomains.
config/database.yml
login: &login
adapter: mysql
username: rails
password: IamAStrongPassword!
host: localhost
production:
<<: *login
database: mysite_www
subdomain1:
<<: *login
database: mysite_subdomain1
subdomain2:
<<: *login
database: mysite_subdomain2
...
app/controllers/application_controller.rb
require 'erb'
before_filter :switch_db_connection
def switch_db_connection
subdomain = request.subdomains.first
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(Rails.configuration.database_configuration_file)).result)
ActiveRecord::Base.establish_connection("mysite_#{subdomain}")
end
As I said it's completely untested. But I don't foresee any major problems. If it doesn't work hopefully it puts you on the right track.
Related
I have a rails 4.2 multi-tenant app using the Apartment gem which has been awesome.
Each company has their own subdomain. I'm using a custom "elevator" which looks at the full request host to determine which "Tenant" should be loaded.
When I create a new company I have an after_create hook to create the new tenant with the proper request host.
This always seems to require a restart of the server both in development and production otherwise I get a Tenant Not Found error.
It's using sqlite in development and postgres in production.
Do I really have to restart the server each time I create a new tenant? Is there an automated way to do this? Maybe just reloading the initializer will work, but I'm not sure how to do that/if that's possible?
I have been messing around with this for a month and haven't been able to find a solution that works. Please help!
initializers/apartment.rb
require 'apartment/elevators/host_hash'
config.tenant_names = lambda { Company.pluck :request_host }
Rails.application.config.middleware.use 'Apartment::Elevators::HostHash', Company.full_hosts_hash
initializers/host_hash.rb
require 'apartment/elevators/generic'
module Apartment
module Elevators
class HostHash < Generic
def initialize(app, hash = {}, processor = nil)
super app, processor
#hash = hash
end
def parse_tenant_name(request)
if request.host.split('.').first == "www"
nil
else
raise TenantNotFound,
"Cannot find tenant for host #{request.host}" unless #hash.has_key?(request.host)
#hash[request.host]
end
end
end
end
end
Company Model
after_create :create_tenant
def self.full_hosts_hash
Company.all.inject(Hash.new) do |hash, company|
hash[company.request_host] = company.request_host
hash
end
end
private
def create_tenant
Apartment::Tenant.create(request_host)
end
What ended up working
I changed the elevator configuration to get away from the HostHash one that's in the apartment gem and used a completely custom one. Mostly based off of an issue on the apartment gem github: https://github.com/influitive/apartment/issues/280
initializers/apartment.rb
Rails.application.config.middleware.use 'BaseSite::BaseElevator'
app/middleware/base_site.rb
require 'apartment/elevators/generic'
module BaseSite
class BaseElevator < Apartment::Elevators::Generic
def parse_tenant_name(request)
company = Company.find_by_request_host(request.host)
return company.request_host unless company.nil?
fail StandardError, "No website found at #{request.host} not found"
end
end
end
I think the problem could be that your host_hash.rb lives in the initializers directory. Shouldn't it be in a folder called "middleware"?, as per the Apartment gem ReadME you referenced in your comment. In that example they used app/middleware/my_custom_elevator.rb. Perhaps yours might look like app/middleware/host_hash.rb?
Right now the file is in initializers, so it's loading from there. But your apartment.rb references it by Rails.application.config.middleware.use. Just a hunch but in addition to loading it initially, it may be looking for it in a nonexistent middleware folder. I'd go ahead and create app/middleware, put the file in there instead, and see what happens. Not sure but you might need to alter require paths too.
Let us know if that helps.
I have a multi-domain Rails 4 app where the request.domain of the http request determines which functionality I expose a given visitor to. A visitor can sign up through Devise. During the user creation a user.domain field will be filled in with the domain he signed up for, and he will then only have access to this particular domain and its functionality.
Question:
Each domain in my app should be served by its own MongoDB database. If user.domain == 'domain1.com' the user object, as well as all objects that belongs_to it, should be stored in the database that serves domain1.com. How do I set this up?
Complication 1:
I do not only interact with my database through Mongoid, but also through mongo Shell methods like db.collection.insert(). I need to have the correct database connection in both cases. Example:
class ApplicationController < ActionController::Base
before_filter :connect_to_db
def connect_to_db
domain = request.domain
# Establish connection
end
end
Complication 2:
A lot of the database interaction happens outside the MVC context. I need to have the correct database connection in e.g. a Sidekiq context. Example:
class MyJob
include Sidekiq::Worker
def perform(domain)
connect_to_db(domain)
User.all.each do |user|
...
end
end
def connect_to_db(domain)
# Establish connection
end
end
Potentially relevant SO answers:
This answer and this answer suggests that you can apply a set_database or store_in session method on a model level, but this would not be sufficient for me, because models are shared between my domains. Various stragegies have also been discussed in this Google group.
How to correctly change database configuration for multi-tenant app in Rails 3?
At this point, I'm switching DB configuration in ApplicationController's before filter, like following code
class ApplicationController < ActionController::Base
before_filter :set_database
def set_database
db_name = get_db_name
spec = ActiveRecord::Base.configurations[Rails.env]
new_spec = spec.clone
new_spec["database"] = db_name
ActiveRecord::Base.establish_connection(new_spec)
end
end
Is this a good way? I have a concerns regarding user sessions. How can I correctly change session store settings, e.g. :key? Another problem here is, if user session is stored in DB, because it seems that user session is loaded in rack middleware before ApplicationController code.
To switch between databases, this solution looks cleaner. Update the conn method will the logic of your get_db_name method. Hope this helps.
I'm building an application that will require CouchDB's mobile syncing feature.
So for each 'account' on the service I want to create a separate CouchDB database instance so that only this account's data is synced.
I'm using CouchRest Model and Devise, which handles subdomain authentication via a separate users database.
However what is the correct way to connect to the appropriate database at runtime for each model?
A before_filter that sets up a named connection, then loops through each model and does something like this: ?
[Post, Tag, Comment].each do |model|
model_server = CouchRest::Server.new(couch_config[:connection])
model_server.default_database = "my_project-#{Rails.env}-#{model.to_s.downcase}"
model.database = model_server.default_database
end
(Pseudocode)
Assuming that the web server (Heroku) runs each request in a separate thread, this should mean that on each request, the database connection is changed dynamically.
Seems like there should be an easier way!
As a solution to question you can override the database method:
class OneDbPerAccountDocument < CouchRest::ExtendedDocument
def self.database
Account.current_database
end
...
end
And then just subclass your models (Post, Tag, Comment) from this class.
class Account < OneDbPerAccountDocument
def self.current=(name)
#current_database = #couch_server.database("my-project_#{name}")
end
def self.current_database
#current_database
end
end
With this trick all you need to do in controller is just call something like
Account.current = request.subdomain
But, beware that this approach will become a little messy when you'll have several thousands of accounts (databases).
What is the best way to do per-user database connections in Rails?
I realize this is a poor Rails design practice, but we're gradually replacing an existing web application that uses one database per user. A complete redesign/rewrite is not feasible.
Put something like this in your application controller. I'm using the subdomain plus "_clientdb" to pick the name of the database. I have all the databases using the same username and password, so I can grab that from the db config file.
Hope this helps!
class ApplicationController < ActionController::Base
before_filter :hijack_db
def hijack_db
db_name = request.subdomains.first + "_clientdb"
# lets manually connect to the proper db
ActiveRecord::Base.establish_connection(
:adapter => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['adapter'],
:host => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['host'],
:username => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['username'],
:password => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['password'],
:database => db_name
)
end
end
Take a look at ActiveRecord::Base.establish_connection. That's how you connect to a different database server. I can't be of much more help since I don't know how you recognize the user or map it to it's database, but I suppose a master database will have that info (and the connection info should be on the database.yml file).
Best of luck.