Skip to content
Browse files

adding plugin to github hotness

  • Loading branch information...
0 parents commit 61415ce0d796199236882b2b4ad41187915de5f2 Andrew Coleman committed
Showing with 380 additions and 0 deletions.
  1. +25 −0 LICENSE
  2. +26 −0 README
  3. +5 −0 init.rb
  4. +136 −0 lib/restricted_subdomain_controller.rb
  5. +188 −0 lib/restricted_subdomain_model.rb
25 LICENSE
@@ -0,0 +1,25 @@
+
+LICENSE
+
+The MIT License
+
+Copyright (c) 2009 Andrew Coleman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
26 README
@@ -0,0 +1,26 @@
+Subdomain Restrictions
+
+A general purpose plugin for limiting a model to a subdomain by way of an
+ActiveRecord model. This is a three pronged approach.
+
+The controller must find the specified model, and raises a 404 if it is not
+found. It also sets the current subdomain into the model for all later access.
+
+The model side of things will restrict all finds to the scope of the
+subdomain, if you want. It's a salt-shaker approach. You know, you salt
+your beans and potatoes but not your steak. It will validate that the
+association exists for all salted models, too.
+
+For convienence, it also provides a looping iterator to evaluate your entire
+Rails application in the scope of each defined subdomain. Just give it a
+block and it will do whatever you want for each site in turn.
+
+The last bit is the session. The session gets all access automatically
+scoped to a subhash keyed on the subdomain as a symbol. That way you can
+log into a site named 'foo' and not be logged into 'bar'.
+
+For a final bit of hotness, when you run a script/console session, there will
+be no restrictions placed on your actions unless you manually call
+YourSubdomain.current = YourSubdomain.find_by_code 'somesite'
+This way you can manually adminster all of the models without any hassle.
+
5 init.rb
@@ -0,0 +1,5 @@
+require 'restricted_subdomain_controller'
+ActionController::Base.send :include, RestrictedSubdomain::Controller
+require 'restricted_subdomain_model'
+ActiveRecord::Base.send :include, RestrictedSubdomain::Model
+
136 lib/restricted_subdomain_controller.rb
@@ -0,0 +1,136 @@
+module RestrictedSubdomain
+ module Controller
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ protected
+
+ ##
+ # == General
+ #
+ # Enables subdomain restrictions by adding a before_filter and helper to
+ # access the current subdomain through current_subdomain in the
+ # controller.
+ #
+ # == Usage
+ #
+ # Takes two arguments: :through and :by. :through should be a class of the
+ # model used to represent the subdomain (defaults to Agency) and the :by
+ # should be the column name of the field containing the subdomain
+ # (defaults to :code).
+ #
+ # == Working Example
+ #
+ # For example, the usage of Agency and :code will work out thusly:
+ #
+ # In app/controllers/application.rb (or any other!) add:
+ # use_restricted_subdomain :through => Agency, :by => :code
+ #
+ # 1. Request hits http://secksi.example.com/login
+ # 2. Subdomain becomes 'secksi'
+ # 3. The corresponding 'Agency' with a ':code' of 'secksi' becomes the
+ # current subdomain. If it's not found, an ActiveRecord::RecordNotFound
+ # is thrown to automatically raise a 404 not found.
+ #
+ # == account_location
+ #
+ # This plugin is very similar to the functionality of the account_location
+ # plugin written by DHH. There are three basic differences between them,
+ # though. This plugin allows for any model and any column, not just
+ # @account.username like account_plugin. I also wanted epic failure if a
+ # subdomain was not found, not just pretty "uh oh" or a default page.
+ # There should be no choice -- just finished. The plugin also integrates
+ # with the model, you cannot access information outside of your domain
+ # for any model tagged with subdomain restrictions. If your users are
+ # limited to a subdomain, you cannot in any way access the users from
+ # another subdomain simply by typing User.find(params[:random_id]).
+ # It should also provide an epic failure.
+ #
+ # This plugin provides that kind of separation. It was designed to provide
+ # separation of data in a medical application so as to run _n_ different
+ # instances of an application in _1_ instance of the application, with
+ # software restrictions that explicitly and implicitly forbid access
+ # outside of your natural subdomain.
+ #
+ # Funny story: I actually completely finished this part of the plugin...
+ # Then i discovered that account_location existed and did pretty much the
+ # same thing without any meta-programming. Good times :)
+ #
+ def use_restricted_subdomains(opts = {})
+ options = {
+ :through => Agency,
+ :by => :code
+ }.merge(opts)
+
+ append_before_filter :current_subdomain
+ cattr_accessor :subdomain_klass, :subdomain_column
+ self.subdomain_klass = options[:through]
+ self.subdomain_column = options[:by]
+ helper_method :current_subdomain
+
+ include RestrictedSubdomain::Controller::InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ protected
+
+ ##
+ # 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.
+ #
+ def current_subdomain
+ if @_current_subdomain.nil?
+ subname = request.host.split(/\./).first
+ @_current_subdomain = self.subdomain_klass.find :first,
+ :conditions => { self.subdomain_column => subname }
+ raise ActiveRecord::RecordNotFound if @_current_subdomain.nil?
+ self.subdomain_klass.current = @_current_subdomain
+ end
+ @_current_subdomain
+ end
+
+ ##
+ # Returns a symbol of the current subdomain. So, something like
+ # http://secksi.example.com returns :secksi
+ #
+ def current_subdomain_symbol
+ if current_subdomain
+ current_subdomain.send(self.subdomain_column).to_sym
+ else
+ nil
+ end
+ end
+
+ ##
+ # 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.
+ #
+ def session
+ if current_subdomain_symbol
+ 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_symbol
+ request.session[current_subdomain_symbol] ||= {}
+ request.session[current_subdomain_symbol] = args
+ else
+ request.session = args
+ end
+ end
+ end
+ end
+end
188 lib/restricted_subdomain_model.rb
@@ -0,0 +1,188 @@
+module RestrictedSubdomain
+ module Model
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ ##
+ # This method will mark a class as the subdomain model. It expects to
+ # contain the subdomain in a column. You can override the default (:code)
+ # by passing a :by parameter. That column will be validated for presence
+ # and uniqueness, so be sure to add an index on that column.
+ #
+ # This will add a cattr_accessor of current which will always contain
+ # the current subdomain requested from the controller.
+ #
+ # A method for iterating over each subdomain model is also provided,
+ # called each_subdomain. Pass a block and do whatever you need to do
+ # restricted to a particular scope of that subdomain. Useful for console
+ # and automated tasks where each subdomain has particular features that
+ # may differ from each other.
+ #
+ # Example:
+ # class Agency < ActiveRecord::Base
+ # use_for_restricted_subdomains :by => :code
+ # end
+ #
+ def use_for_restricted_subdomains(opts = {})
+ options = {
+ :by => :code
+ }.merge(opts)
+
+ validates_presence_of options[:by]
+ validates_uniqueness_of options[:by]
+ cattr_accessor :current
+
+ self.class_eval <<-RUBY
+ def self.each_subdomain(&blk)
+ @_current_subdomains ||= self.find(:all)
+ @_current_subdomains.each do |subdomain|
+ self.current = subdomain
+ yield blk
+ end
+ end
+ RUBY
+ end
+
+ ##
+ # This method marks a model as restricted to a subdomain. This means that
+ # it will have an association to whatever class models your subdomain,
+ # see use_for_restricted_subdomains. It overrides the default find method
+ # to always include a subdomain column parameter. You need to pass the
+ # subdomain class symbol and column (defaults klass to :agency).
+ #
+ # Adds validation for the column and a belongs_to association.
+ #
+ # 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.
+ #
+ # Example:
+ #
+ # class Widget < ActiveRecord::Base
+ # acts_as_restricted_subdomain :through => :subdomain
+ # end
+ #
+ # class Subdomain < ActiveRecord::Base
+ # use_for_restricted_subdomains :by => :name
+ # end
+ #
+ # Special thanks to the Caboosers who created acts_as_paranoid. This is
+ # pretty much the same thing, only without the delete_all bits.
+ #
+ def acts_as_restricted_subdomain(opts = {})
+ options = { :through => :agency }.merge(opts)
+ unless restricted_to_subdomain?
+ cattr_accessor :subdomain_symbol, :subdomain_klass
+ self.subdomain_symbol = options[:through]
+ self.subdomain_klass = options[:through].to_s.camelize.constantize
+ belongs_to options[:through]
+ before_create :set_restricted_subdomain_column
+ class << self
+ alias_method :find_every_with_subdomain, :find_every
+ alias_method :calculate_with_subdomain, :calculate
+ end
+ include InstanceMethods
+ end
+ end
+
+ ##
+ # Checks to see if the class has been restricted to a subdomain.
+ #
+ def restricted_to_subdomain?
+ self.included_modules.include?(InstanceMethods)
+ end
+ end
+
+ module InstanceMethods # :nodoc:
+ def self.included(base) # :nodoc:
+ base.extend(ClassMethods)
+ end
+
+ private
+
+ def set_restricted_subdomain_column
+ self.send("#{subdomain_symbol}=", subdomain_klass.current)
+ if self.send("#{subdomain_symbol}_id").nil?
+ self.errors.add(subdomain_symbol, 'is missing')
+ false
+ else
+ true
+ end
+ end
+
+ public
+
+ module ClassMethods
+ def find_with_subdomain(*args)
+ options = extract_options_from_args!(args) rescue args.extract_options!
+ validate_find_options(options)
+ set_readonly_option!(options)
+ options[:with_subdomain] = true
+
+ case args.first
+ when :first then find_initial(options)
+ when :all then find_every(options)
+ else find_from_ids(args, options)
+ end
+ end
+
+ def count_with_subdomain(*args)
+ calculate_with_subdomain(:count, *construct_subdomain_options_from_legacy_args(*args))
+ end
+
+ def construct_subdomain_options_from_legacy_args(*args)
+ options = {}
+ column_name = :all
+
+ # We need to handle
+ # count()
+ # count(options={})
+ # count(column_name=:all, options={})
+ # count(conditions=nil, joins=nil) # deprecated
+ if args.size > 2
+ raise ArgumentError, "Unexpected parameters passed to count(options={}): #{args.inspect}"
+ elsif args.size > 0
+ if args[0].is_a?(Hash)
+ options = args[0]
+ elsif args[1].is_a?(Hash)
+ column_name, options = args
+ else
+ options.merge!(:conditions => args[0])
+ options.merge!(:joins => args[1]) if args[1]
+ end
+ end
+
+ [column_name, options]
+ end
+
+ def count(*args)
+ with_subdomain_scope { count_with_subdomain(*args) }
+ end
+
+ def calculate(*args)
+ with_subdomain_scope { calculate_with_subdomain(*args) }
+ end
+
+ protected
+
+ def with_subdomain_scope(&block)
+ if subdomain_klass.current
+ with_scope({ :find => { :conditions => ["#{table_name}.#{subdomain_symbol}_id = ?", subdomain_klass.current.id ] } }, :merge, &block)
+ else
+ with_scope({}, :merge, &block)
+ end
+ end
+
+ private
+
+ def find_every(options)
+ options.delete(:with_subdomain) ?
+ find_every_with_subdomain(options) :
+ with_subdomain_scope { find_every_with_subdomain(options) }
+ end
+ end
+ end
+ end
+end

0 comments on commit 61415ce

Please sign in to comment.
Something went wrong with that request. Please try again.