Rails: best practice to scope queries based on subdomain?
I'm working on a Rails (currently 2.3.4) app that makes use of subdomains to isolate inde开发者_JS百科pendent account sites. To be clear, what I mean is foo.mysite.com should show the foo account' content and bar.mysite.com should show bar's content.
What's the best way to ensure that all model queries are scoped to the current subdomain?
For example, one of my controllers looks something like:
@page = @global_organization.pages.find_by_id(params[:id])
(Note @global_organization
is set in the application_controller via subdomain-fu.)
When what I would prefer is something like:
@page = Page.find_by_id(params[:id])
where the Page model finds are automatically scoped to the right organization. I've tried using the default_scope directive like this: (in the Page model)
class Page < ActiveRecord::Base
default_scope :conditions => "organization_id = #{Thread.current[:organization]}"
# yadda yadda
end
(Again, just to note, the same application_controller sets Thread.current[:organization] to the organization's id for global access.) The problem with this approach is that the default scope gets set on the first request and never changes on subsequent requests to different subdomains.
Three apparent solutions thus far:
1 Use separate vhosts for each subdomain and just run different instances of the app per subdomain (using mod_rails). This approach isn't scalable for this app.
2 Use the original controller approach above. Unfortunately there are quite a number of models in the app and many of the models are a few joins removed from the organization, so this notation quickly becomes cumbersome. What's worse is that this actively requires developers to remember and apply the limitation or risk a significant security problem.
3 Use a before_filter to reset the default scope of the models on each request. Not sure about the performance hit here or how best to select which models to update per-reqeust.
Thoughts? Any other solutions I'm missing? This would seem to be a common enough problem that there's gotta be a best practice. All input appreciated, thanks!
Be careful going with default scope here, as it will lead you into a false sense of security, particularly when creating records.
I've always used your first example to keep this clear:
@page = @go.pages.find(params[:id])
The biggest reason is because you also want to ensure this association is applied to new records, so your new/create actions will look like the following, ensuring that they are properly scoped to the parent association:
# New
@page = @go.pages.new
# Create
@page = @go.pages.create(params[:page])
Have you tried defining the default_scope
a lambda
? The lambda
bit that defines the options get evaluated every time the scope is used.
class Page < ActiveRecord::Base
default_scope lambda do
{:conditions => "organization_id = #{Thread.current[:organization]}"}
end
# yadda yadda
end
It's essentially doing your third option, by working in tandem with your before filter magic. But it's a little more aggressive than that, kicking in on every single find used on the Page model.
If you want this behaviour for all models you could add the default_scope to ActiveRecord::Base, but you mention a few being a couple of joins away. So if you go this route, you'll have to override the default scopes in those models to address the joins.
We've been using https://github.com/penguincoder/acts_as_restricted_subdomain for the last 2 years, but it only works with Rails 2.3.
We are currently trying to upgrade (and gemmify) the plugin to work with Rails 3. I'm curious on how you worked your problem out.
You might be better of having a database per account and switching the database connection based on the subdomain.
In addition to the above link if you have a model (in your case Account) that you want to use the default database just include establish connection
in the model.
class Account < ActiveRecord::Base
# Always use shared database
establish_connection "shared_#{RAILS_ENV}".to_sym
I've found the acts_as_tenant gem [github] works great for this feature. It does the scoping for you with minimal extra effort, and also makes bypassing the scoping difficult.
Here's the initial blog post about it: http://www.rollcallapp.com/blog/2011/10/03/adding-multi-tenancy-to-your-rails-app-acts-as-tenant
精彩评论