Skip to content

Commit

Permalink
Make each_subdomain thread-safe
Browse files Browse the repository at this point in the history
  • Loading branch information
jhollinger committed Jul 3, 2013
2 parents be4c1cf + a0afb55 commit 64e6bb9
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 38 deletions.
72 changes: 44 additions & 28 deletions lib/restricted_subdomain_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ def use_restricted_subdomains(opts = {})
:by => :code
}.merge(opts)

append_before_filter :current_subdomain
respond_to?(:prepend_around_action) ? prepend_around_action(:within_request_subdomain) : prepend_around_filter(:within_request_subdomain)

cattr_accessor :subdomain_klass, :subdomain_column
self.subdomain_klass = options[:through].constantize
self.subdomain_column = options[:by]
Expand All @@ -70,20 +71,24 @@ def use_restricted_subdomains(opts = {})

module InstanceMethods
##
# Returns the current subdomain model. Inspects request.host to figure out
# the subdomain by splitting on periods and using the first entry. This
# implies that the subdomain should *never* have a period in the name.
# Sets the current subdomain model to the subdomain specified by #request_subdomain.
#
def current_subdomain
if @_current_subdomain.nil?
subname = request.host.split(/\./).first
@_current_subdomain = self.subdomain_klass.first(
:conditions => { self.subdomain_column => subname }
)
raise ActiveRecord::RecordNotFound if @_current_subdomain.nil?
self.subdomain_klass.current = @_current_subdomain
def within_request_subdomain
self.subdomain_klass.current = request_subdomain
raise ActiveRecord::RecordNotFound if self.subdomain_klass.current.nil?
begin
yield if block_given?
ensure
self.subdomain_klass.current = nil
end
@_current_subdomain
end

##
# Returns the current subdomain model, or nil if none.
# It respects Agency.each_subdomain, Agency.with_subdomain and Agency.without_subdomain.
#
def current_subdomain
self.subdomain_klass.current
end

##
Expand All @@ -100,27 +105,24 @@ def current_subdomain_symbol

##
# Overwrite the default accessor that will force all session access to
# a subhash keyed on the restricted subdomain symbol. Only works if
# the current subdomain is found, gracefully degrades if missing.
# a subhash keyed on the restricted subdomain symbol. If the current
# current subdomain is not set, it gracefully degrades to the normal session.
#
# Optionall, a specific subdomain may be passed. This allows users from
# all subdomains to be signed in from a single "global" subdomain.
#
def session(subdomain_symbol = nil)
if((subdomain_symbol ||= current_subdomain_symbol rescue nil))
request.session[subdomain_symbol] ||= {}
request.session[subdomain_symbol]
def session
if current_subdomain
request.session[current_subdomain_symbol] ||= {}
request.session[current_subdomain_symbol]
else
request.session
end
end

##
# Forces all session assignments to a subhash keyed on the current
# subdomain symbol, if found. Otherwise works just like normal.
#
def session=(*args)
if((current_subdomain rescue nil))
if current_subdomain
request.session[current_subdomain_symbol] ||= {}
request.session[current_subdomain_symbol] = args
else
Expand All @@ -133,10 +135,24 @@ def session=(*args)
# subdomains is kept.
#
def reset_session
copier = lambda { |sess, (key, val)| sess[key] = val unless key == current_subdomain_symbol; sess }
new_session = request.session.inject({}, &copier)
super
new_session.inject(request.session, &copier)
if current_subdomain
copier = lambda { |sess, (key, val)| sess[key] = val unless key == current_subdomain_symbol; sess }
new_session = request.session.inject({}, &copier)
super
new_session.inject(request.session, &copier)
else
super
end
end

# Returns the subdomain from the current request. Inspects request.host to figure out
# the subdomain by splitting on periods and using the first entry. This
# implies that the subdomain should *never* have a period in the name.
#
# It can be useful to override this for testing with Capybara et all.
#
def request_subdomain
request.host.split(/\./).first
end
end
end
Expand Down
76 changes: 66 additions & 10 deletions lib/restricted_subdomain_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,28 @@ def self.current=(other)
def self.each_subdomain(&blk)
old_current = self.current
@_current_subdomains ||= self.find(:all)
@_current_subdomains.each do |subdomain|
self.find(:all).each do |subdomain|
self.current = subdomain
yield blk
end
self.current = old_current
end
def self.with_subdomain(subdomain, &blk)
old_current = self.current
self.current = subdomain
result = blk.call
self.current = old_current
result
end
def self.without_subdomain(&blk)
old_current = self.current
self.current = nil
result = blk.call
self.current = old_current
result
end
RUBY
end

Expand All @@ -65,6 +80,12 @@ def self.each_subdomain(&blk)
# This does not add any has_many associations in your subdomain class.
# That is an exercise left to the user, sorry. Also beware of
# validates_uniqueness_of. It should be scoped to the foreign key.
#
# If you pass an assocation symbol through the :delegate option, the subdomain
# association will be delegated through that assocation instead of being linked
# directly. (It is assumed that the delegate is restricted to the subdomain.)
# The result is that model lookups will always be inner-joined to the delegate,
# ensuring that the model is indirectly restricted.
#
# Example:
#
Expand All @@ -76,6 +97,19 @@ def self.each_subdomain(&blk)
# use_for_restricted_subdomains :by => :name
# end
#
# Delegate Example: A User is "global" and is linked to one or more subdomains through
# UserCredential. Even though the User is technically global, it will only be visible to the
# associated subdomains.
#
# class User < ActiveRecord::Base
# acts_as_restricted_subdomain :through => :subdomain, :delegate => :user_credentials
# has_many :user_credentials
# end
#
# class UserCredential < ActiveRecord::Base
# acts_as_restricted_subdomain :through => :subdomain
# end
#
# Special thanks to the Caboosers who created acts_as_paranoid. This is
# pretty much the same thing, only without the delete_all bits.
#
Expand All @@ -85,23 +119,45 @@ def acts_as_restricted_subdomain(opts = {})
cattr_accessor :subdomain_symbol, :subdomain_klass
self.subdomain_symbol = options[:through]
self.subdomain_klass = options[:through].to_s.camelize.constantize
belongs_to options[:through]
validates_presence_of options[:through]
before_create :set_restricted_subdomain_column

self.class_eval do
default_scope { self.subdomain_klass.current ? where("#{self.subdomain_symbol}_id" => self.subdomain_klass.current.id ) : nil }
end
# This *isn't* the restricted model, but it should always join against a delegate association
if options[:delegate]
cattr_accessor :subdomain_symbol_delegate, :subdomain_klass_delegate
self.subdomain_symbol_delegate = options[:delegate]
self.subdomain_klass_delegate = options[:delegate].to_s.singularize.camelize.constantize

default_scope do
if self.subdomain_klass.current
# Using straight sql so we can JOIN against two columns. Otherwise one must go into "WHERE", and Arel would mistakenly apply it to UPDATEs and DELETEs.
delegate_foreign_key = self.reflections[self.subdomain_symbol_delegate].foreign_key
join_args = {:delegate_table => self.subdomain_klass_delegate.table_name, :delegate_key => delegate_foreign_key, :table_name => self.table_name, :subdomain_key => "#{self.subdomain_symbol}_id", :subdomain_id => self.subdomain_klass.current.id.to_i}
# Using "joins" makes records readonly, which we don't want
with_scope :find => {:readonly => false} do
joins("INNER JOIN %{delegate_table} ON %{delegate_table}.%{delegate_key} = %{table_name}.id AND %{delegate_table}.%{subdomain_key} = %{subdomain_id}" % join_args)
end
end
end

include InstanceMethods
# This *is* the restricted model and should always include the id in queries
else
belongs_to options[:through]
validates_presence_of options[:through]
before_create :set_restricted_subdomain_column

self.class_eval do
default_scope { self.subdomain_klass.current ? where("#{self.subdomain_symbol}_id" => self.subdomain_klass.current.id ) : nil }
end

include InstanceMethods
end
end
end

##
# Checks to see if the class has been restricted to a subdomain.
#
def restricted_to_subdomain?
self.included_modules.include?(InstanceMethods)
self.respond_to?(:subdomain_symbol) && self.respond_to?(:subdomain_klass)
end

module InstanceMethods
Expand Down

0 comments on commit 64e6bb9

Please sign in to comment.