milia 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.project +18 -0
- data/.rvmrc +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +202 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/app/controllers/registrations_controller.rb +34 -0
- data/doc/ref_notes.txt +155 -0
- data/lib/milia.rb +6 -0
- data/lib/milia/base.rb +116 -0
- data/lib/milia/control.rb +64 -0
- data/lib/milia/railtie.rb +18 -0
- data/lib/milia/tasks.rb +5 -0
- data/milia.gemspec +76 -0
- data/test/helper.rb +18 -0
- data/test/test_milia.rb +7 -0
- metadata +146 -0
data/.document
ADDED
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
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
|
data/lib/milia/tasks.rb
ADDED
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
|
data/test/test_milia.rb
ADDED
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: []
|