Skip to content

Commit

Permalink
adding plugin to github hotness
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Coleman committed Jun 1, 2009
0 parents commit 61415ce
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 0 deletions.
25 changes: 25 additions & 0 deletions 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 changes: 26 additions & 0 deletions 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 changes: 5 additions & 0 deletions 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 changes: 136 additions & 0 deletions 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 changes: 188 additions & 0 deletions 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.