milia 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.project ADDED
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>milia</name>
4
+ <comment></comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>com.aptana.ide.core.unifiedBuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>org.radrails.rails.core.railsnature</nature>
16
+ <nature>com.aptana.ruby.core.rubynature</nature>
17
+ </natures>
18
+ </projectDescription>
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3@projects
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'activerecord', '>= 3.1'
7
+ gem 'devise', ">= 1.4.8"
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "shoulda", ">= 0"
13
+ gem "bundler", "~> 1.0.0"
14
+ gem "jeweler", "~> 1.6.4"
15
+ gem "rcov", ">= 0"
16
+ gem 'rdoc'
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,51 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.1.1)
5
+ activesupport (= 3.1.1)
6
+ builder (~> 3.0.0)
7
+ i18n (~> 0.6)
8
+ activerecord (3.1.1)
9
+ activemodel (= 3.1.1)
10
+ activesupport (= 3.1.1)
11
+ arel (~> 2.2.1)
12
+ tzinfo (~> 0.3.29)
13
+ activesupport (3.1.1)
14
+ multi_json (~> 1.0)
15
+ arel (2.2.1)
16
+ bcrypt-ruby (3.0.1)
17
+ builder (3.0.0)
18
+ devise (1.4.8)
19
+ bcrypt-ruby (~> 3.0)
20
+ orm_adapter (~> 0.0.3)
21
+ warden (~> 1.0.3)
22
+ git (1.2.5)
23
+ i18n (0.6.0)
24
+ jeweler (1.6.4)
25
+ bundler (~> 1.0)
26
+ git (>= 1.2.5)
27
+ rake
28
+ json (1.6.1)
29
+ multi_json (1.0.3)
30
+ orm_adapter (0.0.5)
31
+ rack (1.3.4)
32
+ rake (0.9.2)
33
+ rcov (0.9.11)
34
+ rdoc (3.10)
35
+ json (~> 1.4)
36
+ shoulda (2.11.3)
37
+ tzinfo (0.3.30)
38
+ warden (1.0.6)
39
+ rack (>= 1.0)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ activerecord (>= 3.1)
46
+ bundler (~> 1.0.0)
47
+ devise (>= 1.4.8)
48
+ jeweler (~> 1.6.4)
49
+ rcov
50
+ rdoc
51
+ shoulda
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 David Anderson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,202 @@
1
+ = milia
2
+
3
+ Milia is a multi-tenanting gem for hosted Rails 3.1 applications which use
4
+ devise for user authentication.
5
+
6
+ == Basic concepts
7
+ * should be transparent to the main application code
8
+ * should be symbiotic with user authentication
9
+ * should raise exceptions upon attempted illegal access
10
+ * should force tenanting (not allow sloppy access to all tenant records)
11
+ * should allow application flexibility upon new tenant sign-up, usage of eula information, etc
12
+ * should be as non-invasive (as possible) to Rails code
13
+ * row-based tenanting is used
14
+ * default_scope is used to enforce tenanting
15
+
16
+ The author used schema-based tenanting in the past but found it deficient for
17
+ the following reasons: most DBMS are optimized to handle enormous number of
18
+ rows but not an enormous number of schema (tables). Schema-based tenancy took a
19
+ performance hit, was seriously time-consuming to backup and restore, was invasive
20
+ into the Rails code structure (monkey patching), was complex to implement, and
21
+ couldn't use Rails migration tools as-is.
22
+
23
+ == Structure
24
+ * necessary models: user, tenant
25
+ * necessary migrations: user, tenant, tenants_users (join table)
26
+
27
+ == Dependency requirements
28
+ * Rails 3.1 or higher
29
+ * Devise 1.4.8 or higher
30
+
31
+ == Installation
32
+
33
+ Either install the gem manually:
34
+
35
+ gem install milia
36
+
37
+ Or in the Gemfile:
38
+
39
+ gem 'milia'
40
+
41
+ == Getting started
42
+
43
+ === Devise setup
44
+ * See https://github.com/plataformatec/devise for how to set up devise.
45
+ * The current version of milia requires that devise use a *User* model.
46
+
47
+ === Milia setup
48
+ *ALL* models require a tenanting field, whether they are to be universal or to
49
+ be tenanted.
50
+
51
+ <i>db/migrate</i>
52
+
53
+ t.references :tenant
54
+
55
+ Tenanted models will also require indexes for the tenant field:
56
+
57
+ add_index :TABLE, :tenant_id
58
+
59
+ Also create a tenants_users join table:
60
+
61
+ <i>db/migrate/20111008081639_create_tenants_users.rb</i>
62
+
63
+ class CreateTenantsUsers < ActiveRecord::Migration
64
+ def change
65
+ create_table :tenants_users, :id => false do |t|
66
+ t.references :tenant
67
+ t.references :user
68
+ end
69
+ add_index :tenants_users, :tenant_id
70
+ add_index :tenants_users, :user_id
71
+ end
72
+ end
73
+
74
+ add the following line AFTER the devise-required filter for authentications:
75
+
76
+ <i>app/controllers/application_controller.rb</i>
77
+
78
+ before_filter :authenticate_user! # forces devise to authenticate user
79
+ before_filter :set_current_tenant # forces milia to set up current tenant
80
+
81
+ catch any exceptions with the following (be sure to also add the designated methods!)
82
+
83
+ rescue_from ::Milia::Control::MaxTenantExceeded, :with => :max_tenants
84
+ rescue_from ::Milia::Control::InvalidTenantAccess, :with => :invalid_tenant
85
+
86
+ Add the following line into the devise_for :users block
87
+
88
+ <i>config/routes.rb</i>
89
+
90
+ devise_for :users do
91
+ post "users" => "milia/registrations#create"
92
+ end
93
+
94
+ === Designate which model determines account
95
+ Add the following acts_as_... to designate which model will be used as the key
96
+ into tenants_users to find the tenant for a given user.
97
+ Only designate one model in this manner.
98
+
99
+ <i>app/models/user.rb</i>
100
+
101
+ class User < ActiveRecord::Base
102
+
103
+ acts_as_universal_and_determines_account
104
+
105
+ end # class User
106
+
107
+ === Designate which model determines tenant
108
+ Add the following acts_as_... to designate which model will be used as the
109
+ tenant model. It is this id field which designates the tenant for an entire
110
+ group of users which exist within a single tenanted domain.
111
+ Only designate one model in this manner.
112
+
113
+ <i>app/models/tenant.rb</i>
114
+
115
+ class Tenant < ActiveRecord::Base
116
+
117
+ acts_as_universal_and_determines_tenant
118
+
119
+ end # class Tenant
120
+
121
+ === Designate universal models
122
+ Add the following acts_as_universal to *ALL* models which are to be universal:
123
+
124
+ <i>app/models/eula.rb</i>
125
+
126
+ class Eula < ActiveRecord::Base
127
+
128
+ acts_as_universal
129
+
130
+ end # class Eula
131
+
132
+ Note that the tenant_id of a universal model will always be forced to nil.
133
+
134
+ === Designate tenanted models
135
+ Add the following acts_as_tenant to *ALL* models which are to be tenanted:
136
+
137
+ <i>app/models/post.rb</i>
138
+
139
+ class Post < ActiveRecord::Base
140
+
141
+ acts_as_tenant
142
+
143
+ end # class Post
144
+
145
+ Note that the tenant_id of a tenanted model must always match the current
146
+ valid tenant.
147
+
148
+ === Exceptions raised
149
+
150
+ Milia::Control::InvalidTenantAccess
151
+ Milia::Control::MaxTenantExceeded
152
+
153
+ === Tenant pre-processing hooks
154
+ Milia expects a tenant pre-processing & setup hook:
155
+
156
+ Tenant.create_new_tenant(params) # see sample code below
157
+
158
+ where the sign-up params are passed, the new tenant must be validated, created,
159
+ and then returned. Any other kinds of prepatory processing are permitted here,
160
+ but should be minimal, and should not involve any tenanted models. At this point
161
+ in the new account sign-up chain, no tenant has been set up yet (but will be
162
+ immediately after the new tenant has been created).
163
+
164
+ <i>app/models/tenant.rb</i>
165
+
166
+ def self.create_new_tenant(params)
167
+
168
+ tenant = Tenant.new(:cname => params[:user][:email], :company => params[:tenant][:company])
169
+
170
+ if new_signups_not_permitted?(params)
171
+
172
+ raise MaxTenantExceeded, "Sorry, new accounts not permitted at this time"
173
+
174
+ else
175
+ tenant.save # create the tenant
176
+ end
177
+ return tenant
178
+ end
179
+
180
+
181
+ == Cautions
182
+ * Milia designates a default_scope for all models (both universal and tenanted). From Rails 3.2 onwards, the last designated default scope overrides any prior scopes.
183
+ * Milia uses Thread.current[:tenant_id] to hold the current tenant for the existing Action request in the application.
184
+ * SQL statements executed outside the context of ActiveRecord pose a potential danger; the current milia implementation does not extend to the DB connection level and so cannot enforce tenanting at this point.
185
+
186
+
187
+
188
+ == Contributing to milia
189
+
190
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
191
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
192
+ * Fork the project
193
+ * Start a feature/bugfix branch
194
+ * Commit and push until you are happy with your contribution
195
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
196
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
197
+
198
+ == Copyright
199
+
200
+ Copyright (c) 2011 Daudi Amani. See LICENSE.txt for
201
+ further details.
202
+
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ # require 'rdoc'
6
+
7
+ begin
8
+ Bundler.setup(:default, :development)
9
+ rescue Bundler::BundlerError => e
10
+ $stderr.puts e.message
11
+ $stderr.puts "Run `bundle install` to install missing gems"
12
+ exit e.status_code
13
+ end
14
+ require 'rake'
15
+
16
+ require 'jeweler'
17
+ Jeweler::Tasks.new do |gem|
18
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
19
+ gem.name = "milia"
20
+ gem.homepage = "http://github.com/dsaronin/milia"
21
+ gem.license = "MIT"
22
+ gem.summary = %Q{Multi-tenanting for hosted Rails 3.1+ applications}
23
+ gem.description = %Q{enables row-based multi-tenanting that is transparent to application}
24
+ gem.email = "dsaronin@gmail.com"
25
+ gem.authors = ["David Anderson"]
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rake/testtask'
31
+ Rake::TestTask.new(:test) do |test|
32
+ test.libs << 'lib' << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+
37
+ require 'rcov/rcovtask'
38
+ Rcov::RcovTask.new do |test|
39
+ test.libs << 'test'
40
+ test.pattern = 'test/**/test_*.rb'
41
+ test.verbose = true
42
+ test.rcov_opts << '--exclude "gems/*"'
43
+ end
44
+
45
+ task :default => :test
46
+
47
+ require 'rdoc/task'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "milia #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,34 @@
1
+ module Milia
2
+
3
+ class RegistrationsController < Devise::RegistrationsController
4
+
5
+ # ------------------------------------------------------------------------------
6
+ # ------------------------------------------------------------------------------
7
+
8
+ # ------------------------------------------------------------------------------
9
+ # ------------------------------------------------------------------------------
10
+ def create
11
+
12
+ sign_out_session!
13
+
14
+ @tenant = Tenant.create_new_tenant(params)
15
+ if @tenant.errors.empty? # tenant created
16
+
17
+ initiate_tenant( @tenant ) # first time stuff for new tenant
18
+ super # do the rest of the user account creation
19
+
20
+ else
21
+ @user = User.new(params[:user])
22
+ render :action => 'new'
23
+ end
24
+
25
+ end # def create
26
+ # ------------------------------------------------------------------------------
27
+ # ------------------------------------------------------------------------------
28
+
29
+ # ------------------------------------------------------------------------------
30
+ # ------------------------------------------------------------------------------
31
+
32
+ end # class Registrations
33
+
34
+ end # module Milia
data/doc/ref_notes.txt ADDED
@@ -0,0 +1,155 @@
1
+ DEFAULT_SCOPE RAILS 3.1 (from https://gist.github.com/958338)
2
+ ================================================================================
3
+ default_scope can take a block, lambda, or any other object which responds
4
+ to call for lazy evaluation:
5
+
6
+ default_scope { ... }
7
+ default_scope lambda { ... }
8
+ default_scope method(:foo)
9
+
10
+ This feature was originally implemented by Tim Morgan, but was then removed in
11
+ favour of defining a default_scope class method,
12
+ but has now been added back in by Jon Leighton.
13
+ The relevant lighthouse ticket is #1812.
14
+
15
+ Default scopes are now evaluated at the latest possible moment,
16
+ to avoid problems where scopes would be created which would implicitly contain
17
+ the default scope, which would then be impossible to get rid of via
18
+ Model.unscoped.
19
+ No
20
+ te that this means that if you are inspecting the internal structure of an
21
+ ActiveRecord::Relation, it will not contain the default scope, though the
22
+ resulting query will do. You can get a relation containing the default scope by
23
+ calling ActiveRecord#with_default_scope, though this is not part of the public API.
24
+
25
+ Calling default_scope multiple times in a class
26
+ (including when a superclass calls default_scope) is deprecated.
27
+ The current behavior is that this will merge the default scopes together:
28
+
29
+ class Post < ActiveRecord::Base # Rails 3.1
30
+ default_scope where(:published => true)
31
+ default_scope where(:hidden => false)
32
+ # The default scope is now: where(:published => true, :hidden => false)
33
+ end
34
+
35
+ In Rails 3.2, the behavior will be changed to overwrite previous scopes:
36
+
37
+ class Post < ActiveRecord::Base # Rails 3.2
38
+ default_scope where(:published => true)
39
+ default_scope where(:hidden => false)
40
+ # The default scope is now: where(:hidden => false)
41
+ end
42
+ If you wish to merge default scopes in special ways, it is recommended to define
43
+ your default scope as a class method and use the standard techniques for
44
+ sharing code (inheritance, mixins, etc.):
45
+
46
+ class Post < ActiveRecord::Base
47
+ def self.default_scope
48
+ where(:published => true).where(:hidden => false)
49
+ end
50
+ end
51
+
52
+ http://samuel.kadolph.com/2010/12/simple-rails-multi-tenancy/
53
+ ===================================================================
54
+ class Tenant < ActiveRecord::Base
55
+ class << self
56
+ def current
57
+ Thread.current[:current_tenant]
58
+ end
59
+
60
+ def current=(tenant)
61
+ Thread.current[:current_tenant] = tenant
62
+ end
63
+ end
64
+ end
65
+
66
+ app/models/tenant_scoped_model.rb
67
+ class TenantScopedModel < ActiveRecord::Base
68
+ self.abstract_class = true
69
+
70
+ class << self
71
+ protected
72
+ def current_scoped_methods
73
+ last = scoped_methods.last
74
+ last.respond_to?(:call) ? relation.scoping { last.call } : last
75
+ end
76
+ end
77
+
78
+ belongs_to :tenant
79
+
80
+ default_scope lambda { where('tenant_id = ?', Tenant.current) }
81
+
82
+ before_save { self.tenant = Tenant.current }
83
+ end
84
+
85
+ class ApplicationController < ActionController::Base
86
+ before_filter do
87
+ @tenant = Tenant.current = Tenant.find_by_host!(request.host)
88
+ end
89
+ end
90
+
91
+ Caveats
92
+
93
+ Calling unscoped bypasses the default_scope; this is both good and bad because you can get all partitioned data from a model regardless of the current tenant.
94
+ You will get errors in the console when working with a scoped model unless you set Tenant.current.
95
+ before_save { self.tenant = Tenant.current } is necessary because rails does not automatically set the attributes from our default_scope because it is a lambda.
96
+
97
+
98
+ http://www.justinball.com/2011/09/27/customizing-views-for-a-multi-tenant-application-using-ruby-on-rails-custom-resolvers/
99
+ ===============================================================================
100
+ but setting a global on the current thread felt like a big hack. (Here's a good article on Thread.current ) Lucky for me Jose was willing to spend a little time working with me and the resulting code works without globals. Instead of passing a global around we removed the Singleton code from the resolver and create an instance of the resolver per each account:
101
+
102
+
103
+ class ApplicationController < ActionController::Base
104
+ before_filter :set_resolver
105
+
106
+ def current_account
107
+ @current_account ||= Account.find_by_domain(request.host) || Account.find_by_code(request.subdomains.first) || Account.first
108
+ end
109
+
110
+ @@account_resolver = {}
111
+
112
+ def account_resolver_for(account)
113
+ @@account_resolver[account.id] ||= CustomView::Resolver.new(account)
114
+ end
115
+
116
+ def set_resolver
117
+ return unless current_account
118
+ resolver = account_resolver_for(current_account)
119
+ resolver.update_account(current_account)
120
+ prepend_view_path resolver
121
+ end
122
+
123
+ end
124
+ class Resolver < ActionView::Resolver
125
+
126
+ def initialize(account)
127
+ @account = account
128
+ end
129
+
130
+ # Check if the custom_view_cache_count is still the same, if not clear the cache
131
+ def update_account(updated_account)
132
+ self.clear_cache unless @account.custom_view_cache_count == updated_account.custom_view_cache_count
133
+ @account = updated_account
134
+ end
135
+ end
136
+
137
+ POSTGRES RULES
138
+ ===============================================================================
139
+ 36.2. Views and the Rule System
140
+ Views in PostgreSQL are implemented using the rule system.
141
+ In fact, there is essentially no difference between:
142
+
143
+ CREATE VIEW myview AS SELECT * FROM mytab;
144
+
145
+ compared against the two commands:
146
+
147
+ CREATE TABLE myview (same column list as mytab);
148
+ CREATE RULE "_RETURN" AS ON SELECT TO myview DO INSTEAD
149
+ SELECT * FROM mytab;
150
+
151
+ because this is exactly what the CREATE VIEW command does internally.
152
+ This has some side effects. One of them is that the information about a view
153
+ in the PostgreSQL system catalogs is exactly the same as it is for a table.
154
+ So for the parser, there is absolutely no difference between a table and a view.
155
+ They are the same thing: relations.
data/lib/milia.rb ADDED
@@ -0,0 +1,6 @@
1
+
2
+ require File.dirname(__FILE__) + '/milia/base'
3
+ require File.dirname(__FILE__) + '/milia/control'
4
+
5
+
6
+ require File.dirname(__FILE__) + '/milia/railtie' if defined?(Rails::Railtie)
data/lib/milia/base.rb ADDED
@@ -0,0 +1,116 @@
1
+ module Milia
2
+ module Base
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # #############################################################################
9
+ # #############################################################################
10
+ module ClassMethods
11
+
12
+ # ------------------------------------------------------------------------
13
+ # acts_as_tenant -- makes a tenanted model
14
+ # Forces all references to be limited to current_tenant rows
15
+ # ------------------------------------------------------------------------
16
+ def acts_as_tenant()
17
+ attr_protected :tenant_id
18
+ default_scope lambda { where( "#{table_name}.tenant_id = ?", Thread.current[:tenant_id] ) }
19
+
20
+ # ..........................callback enforcers............................
21
+ before_save do |obj| # force tenant_id to be correct for current_user
22
+ obj.tenant_id = Thread.current[:tenant_id]
23
+ true # ok to proceed
24
+ end
25
+
26
+ # ..........................callback enforcers............................
27
+ before_update do |obj| # force tenant_id to be correct for current_user
28
+ raise ::Control::InvalidTenantAccess unless obj.tenant_id == Thread.current[:tenant_id]
29
+ true # ok to proceed
30
+ end
31
+
32
+ # ..........................callback enforcers............................
33
+ before_destroy do |obj| # force tenant_id to be correct for current_user
34
+ raise ::Control::InvalidTenantAccess unless obj.tenant_id == Thread.current[:tenant_id]
35
+ true # ok to proceed
36
+ end
37
+
38
+ end
39
+
40
+ # ------------------------------------------------------------------------
41
+ # acts_as_universal -- makes a univeral (non-tenanted) model
42
+ # Forces all reference to the universal tenant (nil)
43
+ # ------------------------------------------------------------------------
44
+ def acts_as_universal()
45
+ attr_protected :tenant_id
46
+ default_scope where( "#{table_name}.tenant_id IS NULL" )
47
+
48
+ # ..........................callback enforcers............................
49
+ before_save do |obj| # force tenant_id to be universal
50
+ raise ::Control::InvalidTenantAccess unless obj.tenant_id.nil?
51
+ true # ok to proceed
52
+ end
53
+
54
+ # ..........................callback enforcers............................
55
+ before_update do |obj| # force tenant_id to be universal
56
+ raise ::Control::InvalidTenantAccess unless obj.tenant_id.nil?
57
+ true # ok to proceed
58
+ end
59
+
60
+ # ..........................callback enforcers............................
61
+ before_destroy do |obj| # force tenant_id to be universal
62
+ raise ::Control::InvalidTenantAccess unless obj.tenant_id.nil?
63
+ true # ok to proceed
64
+ end
65
+
66
+ end
67
+
68
+ # ------------------------------------------------------------------------
69
+ # acts_as_universal_and_determines_tenant_reference
70
+ # All the characteristics of acts_as_universal AND also does the magic
71
+ # of binding a user to a tenant
72
+ # ------------------------------------------------------------------------
73
+ def acts_as_universal_and_determines_account()
74
+ has_and_belongs_to_many :tenants
75
+
76
+ acts_as_universal()
77
+
78
+ # before create, tie user with current tenant
79
+ # return true if ok to proceed; false if break callback chain
80
+ after_create do |new_user|
81
+ tenant = Tenant.find( Thread.current[:tenant_id] )
82
+ unless tenant.users.include?(new_user)
83
+ tenant.users << new_user # add user to this tenant if not already there
84
+ end
85
+
86
+ end # before_create do
87
+
88
+ before_destroy do |old_user|
89
+ old_user.tenants.clear # remove all tenants for this user
90
+ true
91
+ end # before_destroy do
92
+
93
+ end # acts_as
94
+
95
+ # ------------------------------------------------------------------------
96
+ # ------------------------------------------------------------------------
97
+ def acts_as_universal_and_determines_tenant()
98
+ has_and_belongs_to_many :users
99
+
100
+ acts_as_universal()
101
+
102
+ before_destroy do |old_tenant|
103
+ old_tenant.users.clear # remove all users from this tenant
104
+ true
105
+ end # before_destroy do
106
+
107
+ end
108
+ # ------------------------------------------------------------------------
109
+ # ------------------------------------------------------------------------
110
+
111
+ end # module ClassMethods
112
+ # #############################################################################
113
+ # #############################################################################
114
+
115
+ end # module Base
116
+ end # module Milia
@@ -0,0 +1,64 @@
1
+ module Milia
2
+ module Control
3
+
4
+ # #############################################################################
5
+ class InvalidTenantAccess < SecurityError; end
6
+ class MaxTenantExceeded < ArgumentError; end
7
+ # #############################################################################
8
+
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ # #############################################################################
14
+ # #############################################################################
15
+ module ClassMethods
16
+
17
+ end # module ClassMethods
18
+ # #############################################################################
19
+ # #############################################################################
20
+
21
+ private
22
+
23
+ # ------------------------------------------------------------------------------
24
+ # set_current_tenant -- sets the tenant id for the current invocation (thread)
25
+ # args
26
+ # tenant_id -- integer id of the tenant; nil if get from current user
27
+ # EXCEPTIONS -- InvalidTenantAccess
28
+ # ------------------------------------------------------------------------------
29
+ def set_current_tenant( tenant_id = nil )
30
+ if user_signed_in?
31
+ @_my_tenants ||= current_user.tenants # gets all possible tenants for user
32
+
33
+ if tenant_id.nil? # no arg; find automatically from user
34
+ tenant_id = @_my_tenants.first.id
35
+ else # passed an arg; validate tenant_id before setup
36
+ raise InvalidTenantAccess unless @_my_tenants.any?{|tu| tu.id == tenant_id}
37
+ end
38
+ else # user not signed in yet...
39
+ tenant_id = 0 if tenant_id.nil? # an impossible tenant_id
40
+ end
41
+
42
+ Thread.current[:tenant_id] = tenant_id
43
+
44
+ true # before filter ok to proceed
45
+ end
46
+
47
+ # ------------------------------------------------------------------------------
48
+ # initiate_tenant -- initiates first-time tenant; establishes thread
49
+ # ONLY for brand-new tenants upon User account sign up
50
+ # arg
51
+ # tenant -- tenant obj of the new tenant
52
+ # ------------------------------------------------------------------------------
53
+ def initiate_tenant( tenant )
54
+ Thread.current[:tenant_id] = tenant.id
55
+ end
56
+
57
+ # ------------------------------------------------------------------------------
58
+ # ------------------------------------------------------------------------------
59
+
60
+ # #############################################################################
61
+ # #############################################################################
62
+
63
+ end # module Control
64
+ end # module Milia
@@ -0,0 +1,18 @@
1
+ require 'milia'
2
+ require 'rails'
3
+
4
+ module Milia
5
+ class Railtie < Rails::Railtie
6
+ initializer :after_initialize do
7
+
8
+ ActiveRecord::Base.send(:include, Milia::Base)
9
+ ActionController::Base.send(:include, Milia::Control)
10
+
11
+ require File.dirname(__FILE__) + '/../../app/controllers/registrations_controller'
12
+ end
13
+
14
+ rake_tasks do
15
+ load 'milia/tasks.rb'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ namespace :mt do
2
+ desc "nop"
3
+ task :nop => :environment do
4
+ end
5
+ end
data/milia.gemspec ADDED
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "milia"
8
+ s.version = "0.2.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["David Anderson"]
12
+ s.date = "2011-10-12"
13
+ s.description = "enables row-based multi-tenanting that is transparent to application"
14
+ s.email = "dsaronin@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".project",
22
+ ".rvmrc",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE.txt",
26
+ "README.rdoc",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "app/controllers/registrations_controller.rb",
30
+ "doc/ref_notes.txt",
31
+ "lib/milia.rb",
32
+ "lib/milia/base.rb",
33
+ "lib/milia/control.rb",
34
+ "lib/milia/railtie.rb",
35
+ "lib/milia/tasks.rb",
36
+ "milia.gemspec",
37
+ "test/helper.rb",
38
+ "test/test_milia.rb"
39
+ ]
40
+ s.homepage = "http://github.com/dsaronin/milia"
41
+ s.licenses = ["MIT"]
42
+ s.require_paths = ["lib"]
43
+ s.rubygems_version = "1.8.10"
44
+ s.summary = "Multi-tenanting for hosted Rails 3.1+ applications"
45
+
46
+ if s.respond_to? :specification_version then
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
+ s.add_runtime_dependency(%q<activerecord>, [">= 3.1"])
51
+ s.add_runtime_dependency(%q<devise>, [">= 1.4.8"])
52
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
53
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
54
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
55
+ s.add_development_dependency(%q<rcov>, [">= 0"])
56
+ s.add_development_dependency(%q<rdoc>, [">= 0"])
57
+ else
58
+ s.add_dependency(%q<activerecord>, [">= 3.1"])
59
+ s.add_dependency(%q<devise>, [">= 1.4.8"])
60
+ s.add_dependency(%q<shoulda>, [">= 0"])
61
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
62
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
63
+ s.add_dependency(%q<rcov>, [">= 0"])
64
+ s.add_dependency(%q<rdoc>, [">= 0"])
65
+ end
66
+ else
67
+ s.add_dependency(%q<activerecord>, [">= 3.1"])
68
+ s.add_dependency(%q<devise>, [">= 1.4.8"])
69
+ s.add_dependency(%q<shoulda>, [">= 0"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
72
+ s.add_dependency(%q<rcov>, [">= 0"])
73
+ s.add_dependency(%q<rdoc>, [">= 0"])
74
+ end
75
+ end
76
+
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'milia'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestMilia < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: milia
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Anderson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-12 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &85548970 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *85548970
25
+ - !ruby/object:Gem::Dependency
26
+ name: devise
27
+ requirement: &85548560 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.4.8
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *85548560
36
+ - !ruby/object:Gem::Dependency
37
+ name: shoulda
38
+ requirement: &85548300 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *85548300
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &85547970 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *85547970
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &85547660 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.6.4
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *85547660
69
+ - !ruby/object:Gem::Dependency
70
+ name: rcov
71
+ requirement: &85547230 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *85547230
80
+ - !ruby/object:Gem::Dependency
81
+ name: rdoc
82
+ requirement: &85546850 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *85546850
91
+ description: enables row-based multi-tenanting that is transparent to application
92
+ email: dsaronin@gmail.com
93
+ executables: []
94
+ extensions: []
95
+ extra_rdoc_files:
96
+ - LICENSE.txt
97
+ - README.rdoc
98
+ files:
99
+ - .document
100
+ - .project
101
+ - .rvmrc
102
+ - Gemfile
103
+ - Gemfile.lock
104
+ - LICENSE.txt
105
+ - README.rdoc
106
+ - Rakefile
107
+ - VERSION
108
+ - app/controllers/registrations_controller.rb
109
+ - doc/ref_notes.txt
110
+ - lib/milia.rb
111
+ - lib/milia/base.rb
112
+ - lib/milia/control.rb
113
+ - lib/milia/railtie.rb
114
+ - lib/milia/tasks.rb
115
+ - milia.gemspec
116
+ - test/helper.rb
117
+ - test/test_milia.rb
118
+ homepage: http://github.com/dsaronin/milia
119
+ licenses:
120
+ - MIT
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ segments:
132
+ - 0
133
+ hash: 62265967
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ! '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 1.8.10
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: Multi-tenanting for hosted Rails 3.1+ applications
146
+ test_files: []