s, :dependency => destroy
```
The reason for this is that if you wish to have a master destroy tenant action,
it will also remove all related tenanted tables and records.
Generate the tenant migration
```
$ rails g model tenant tenant:references name:string:index
```
Generate the tenants_users join table migration
```
$ rails g migration CreateTenantsUsersJoinTable tenants users
```
EDIT: db/migrate/20131119092046_create_tenants_users_join_table.rb
then uncomment the first index line as follows:
```
t.index [:tenant_id, :user_id]
```
#### application controller
app/controllers/application_controller.rb
add the following line IMMEDIATELY AFTER line 4 protect_from_forgery
```
before_action :authenticate_tenant! # authenticate user and sets up tenant
rescue_from ::Milia::Control::MaxTenantExceeded, :with => :max_tenants
rescue_from ::Milia::Control::InvalidTenantAccess, :with => :invalid_tenant
# milia defines a default max_tenants, invalid_tenant exception handling
# but you can override if you wish to handle directly
```
### Designate which model determines account
Add the following acts_as_... to designate which model will be used as the key
into tenants_users to find the tenant for a given user.
Only designate one model in this manner.
app/models/user.rb
```ruby
class User < ActiveRecord::Base
acts_as_universal_and_determines_account
end # class User
```
### Designate which model determines tenant
Add the following acts_as_... to designate which model will be used as the
tenant model. It is this id field which designates the tenant for an entire
group of users which exist within a single tenanted domain.
Only designate one model in this manner.
app/models/tenant.rb
```ruby
class Tenant < ActiveRecord::Base
acts_as_universal_and_determines_tenant
end # class Tenant
```
### Clean up any generated belongs_to tenant references in all models
which the generator might have generated
( both acts_as_tenant and acts_as_universal will specify these ).
### Designate universal models
Add the following acts_as_universal to *ALL* models which are to be universal.
```ruby
acts_as_universal
```
### Designate tenanted models
Add the following acts_as_tenant to *ALL* models which are to be tenanted.
Example for a ficticous Post model:
app/models/post.rb
```ruby
class Post < ActiveRecord::Base
acts_as_tenant
end # class Post
```
### Exceptions raised
```ruby
Milia::Control::InvalidTenantAccess
Milia::Control::MaxTenantExceeded
```
### post authenticate_tenant! callback [optional]
In some applications, you will want to set up commonly used
variables used throughout your application, after a user and a
tenant have been established. This is optional and if the
callback is missing, nothing will happen.
app/controllers/application_controller.rb
```ruby
def callback_authenticate_tenant
# set_environment or whatever else you need for each valid session
end
```
### Tenant pre-processing hooks
#### Milia expects a tenant pre-processing & setup hook:
```ruby
Tenant.create_new_tenant(tenant_params, coupon_params) # see sample code below
```
where the sign-up params are passed, the new tenant must be validated, created,
and then returned. Any other kinds of prepatory processing are permitted here,
but should be minimal, and should not involve any tenanted models. At this point
in the new account sign-up chain, no tenant has been set up yet (but will be
immediately after the new tenant has been created).
app/models/tenant.rb
```ruby
def self.create_new_tenant(tenant_params, user_params, coupon_params)
tenant = Tenant.new(:name => tenant_params[:name])
if new_signups_not_permitted?(coupon_params)
raise ::Milia::Control::MaxTenantExceeded, "Sorry, new accounts not permitted at this time"
else
tenant.save # create the tenant
end
return tenant
end
# ------------------------------------------------------------------------
# new_signups_not_permitted? -- returns true if no further signups allowed
# args: params from user input; might contain a special 'coupon' code
# used to determine whether or not to allow another signup
# ------------------------------------------------------------------------
def self.new_signups_not_permitted?(params)
return false
end
```
#### Milia expects a tenant post-processing hook:
```ruby
Tenant.tenant_signup(user,tenant,other) # see sample code below
```
The purpose here is to do any tenant initialization AFTER devise
has validated and created a user. Objects for the user and tenant
are passed. It is recommended that only minimal processing be done
here ... for example, queueing a background task to do the actual
work in setting things up for a new tenant.
app/models/tenant.rb
```ruby
# ------------------------------------------------------------------------
# tenant_signup -- setup a new tenant in the system
# CALLBACK from devise RegistrationsController (milia override)
# AFTER user creation and current_tenant established
# args:
# user -- new user obj
# tenant -- new tenant obj
# other -- any other parameter string from initial request
# ------------------------------------------------------------------------
def self.tenant_signup(user, tenant, other = nil)
# StartupJob.queue_startup( tenant, user, other )
# any special seeding required for a new organizational tenant
Member.create_org_admin(user) # sample if using Member as tenanted member information model
end
```
### View for Organizer sign ups
This example shows how to display a signup form together with recaptcha.
It also shows usage of an optional coupon field
for whatever reason you might need. If you're not familiar with haml, leading spaces are significant
and are used to indicate logical blocks. Otherwise, it's kinda like erb without all the syntactical cruff.
Leading "." indicate div class; "#" indicates a div ID. The example here is
taken from sample-milia-app.
app/views/devise/registrations/new.html.haml
```ruby
%h1 Simple Milia App
.block#block-signup
%h2 New Organizational Sign up
.content
%span.description
%i
If you're a member of an existing group in our system,
click the activate link in the invitation email from your organization's admin.
You should not sign up for a new organizational account.
%br
.flash
- flash.each do |type, message|
%div{ :class => "message #{type}" }
%p= message
- flash.clear # clear contents so we won't see it again
= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :class => "form" }) do |f|
.group
= f.label :email, :class => "label"
= f.text_field :email, :class => "text_field"
%span.description Ex. test@example.com
.group
= f.label :password, :class => "label"
= f.password_field :password, :class => "text_field"
%span.description must be at least 6 characters
.group
= f.label :password_confirmation, "Re-enter Password", :class => "label"
= f.password_field :password_confirmation, :class => "text_field"
%span.description to confirm your password
.group
= fields_for( :tenant ) do |w|
= w.label( :name, 'Organization', :class => "label" )
= w.text_field( :name, :class => "text_field")
%span.description unique name for your group or organization for the new account
- if ::Milia.use_coupon
.group
= label_tag( 'coupon', 'Coupon code', :class => "label" )
= text_field_tag( "coupon[coupon]", @coupon.to_s, :size => 8, :class => "text_field" )
%span.description optional promotional code, if any
- if ::Milia.use_recaptcha
= recaptcha_tags( :display => { :theme => 'clean', :tabindex => 0 } )
.group.navform.wat-cf
%button.button{ :type => "submit" }
= image_tag "web-app-theme/icons/tick.png"
Sign up
= render :partial => "devise/shared/links"
```
### Alternate use case: user belongs to multiple tenants
Your application might allow a user to belong to multiple tenants. You will need
to provide some type of mechanism to allow the user to choose which account
(thus tenant) they wish to access. Once chosen, in your controller, you will need
to put:
app/controllers/any_controller.rb
```ruby
set_current_tenant( new_tenant_id )
```
## joins might require additional tenanting restrictions
Subordinate join tables will not get the Rails default scope.
Theoretically, the default scope on the master table alone should be sufficient
in restricting answers to the current_tenant alone .. HOWEVER, it doesn't feel
right.
If the master table for the join is a universal table, however, you really *MUST*
use the following workaround, otherwise the database will access data in other
tenanted areas even if no records are returned. This is a potential security
breach. Further details can be found in various discussions about the
behavior of databases such as POSTGRES.
The milia workaround is to add an additional .where( where_restrict_tenants(klass1, klass2,...))
for each of the subordinate models in the join.
### usage of where_restrict_tenants
```ruby
Comment.joins(stuff).where( where_restrict_tenants(Post, Author) ).all
```
## no tenant authorization required controller actions: root_path
Any controller actions, such as the root_path page, will need to skip the tenant & user authorizations.
For example in app/controllers/home_controller.rb place the following near the top of the controller:
```ruby
skip_before_action :authenticate_tenant!, :only => [ :index ]
```
## using tokens for authentication
My app has certain actions which require a token for authentication, instead of a user
sign-in. These use cases include an icalendar feed for a particular user's assignments
or a generic icalendar feed for all of an organization's events. The tokens are NOT
a general replacement for user sign-in for all actions, but merely to enable a simple
restful API for certain specific actions. This section will explain how to incorporate
token authentication together with milia/devise. Please note that the application
assigns to each user an authentication token for this use, as well as creates a
generic "guest" for the organization itself for accessing the organization-wide action.
The general scheme is to have a prepend_before_action authenticate_by_token! specified
only for those actions allowed. This action determines the "user" required to proceed
with the action, signs in that user via devise, then falls through to the normal
before_action authenticate_tenant! action which establishes the current_tenant.
Below are some examples of this (typically the token is passed as the id parameter):
app/controllers/application_controller
```ruby
# ------------------------------------------------------------------------------
# NOTE: be sure to use prepend_before_action authenticate_by_token!
# so that this will occur BEFORE authenticate_tenant!
# ------------------------------------------------------------------------------
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed for every request.
# ------------------------------------------------------------------------------
def authenticate_by_token!
# special case for designated actions only
if ( controller_name == "feeder" &&
( user = User.find_user_by_user_feed( params ) )
) ||
( controller_name == "questions" && ['signup_form', 'finish_signup'].include?(action_name) &&
( user = User.find_user_by_user_feed( params ) )
)
# create a special session after authorizing a user
reset_session
sign_in(user, store: false) # devise's way to signin the user
# now continue with tenant authorization & set up
true # ok to continue processing
else
act_path = controller_name.to_s + '/' + action_name.to_s
logger.info("SECURITY - access denied #{Time.now.to_s(:db)} - auth: #{params[:userfeed] }\tuid:#{(user.nil? ? 'n/f' : user.id.to_s)}\tRequest: " + act_path)
render( :nothing => true, :status => :forbidden) # redirect_back # go back to where you were
nil # abort further processing
end
end
```
app/controllers/feeder_controller
```ruby
prepend_before_action :authenticate_by_token! # special authtentication by html token
```
app/models/user.rb
```ruby
# ------------------------------------------------------------------------
# find_user_by_user_feed -- returns a user based on auth code from params
# ------------------------------------------------------------------------
def self.find_user_by_user_feed( params )
# can get auth by either :userfeed or :id
key = ( params[:userfeed].blank? ? params[:id] : params[:userfeed] )
return nil if key.blank? # neither key present; invalid
return User.where( :authentication_token => key ).first # find by the key; nil if invalid
end
def make_authentication_token
self.authentication_token = generate_unique_authentication_token
end
def generate_unique_authentication_token
loop do
token = AuthKey.make_token # this can be anything to generate a random large token
break token unless User.where(authentication_token: token).first
end
end
```
## console
Note that even when running the console, ($ rails console) it will be run in
multi-tenanting mode. You will need to establish a current_user and
setup the current_tenant, otherwise most Model DB accesses will fail.
For the author's own application, I have set up a small ruby file which I
load when I start the console. This does the following:
```ruby
def change_tenant(my_id,my_tenant_id)
@me = User.find( my_id )
@w = Tenant.find( my_tenant_id )
Tenant.set_current_tenant @w
end
change_tenant(1,1) # or whatever is an appropriate starting user, tenant
```
## Whitelisting additional parameters for tenant/user/coupon
During the Tenant.create_new_tenant part of the sign-up process, three
sets of whitelisted parameters are passed to the method: The parameters
for tenant, user, and coupon. But some applications might require more or
other parameters than the ones expected by milia. Sometimes the application
might need to add some parameters of its own, such a EULA version number,
additions to an activation message, or a unique name for the tenant itself.
Milia has a mechanism to add additional parameters to be whitelisted.
In config/initializers/milia.rb you can add a list of symbols for
the additional parameters to each of a config setting for any of the
three (tenant, user, or coupon). The example below shows how.
```ruby
# whitelist user params list
# allows an app to expand the permitted attribute list
# specify each attribute as a symbol
# example: [:name]
config.whitelist_user_params = [:eula_id, :message]
# whitelist tenant params list
# allows an app to expand the permitted attribute list
# specify each attribute as a symbol
# example: [:name]
config.whitelist_tenant_params = [:company, :cname]
# whitelist coupon params list
# allows an app to expand the permitted attribute list
# specify each attribute as a symbol
# example: [:coupon]
config.whitelist_coupon_params = [:vendor]
```
## inviting additional user/members
To keep this discussion simple, we'll give the example of using class Member < Activerecord::Base
which will be a tenanted table for keeping information regarding all the members in a given
organization. The name "Member" is not a requirement of milia. But this is how you would set up an
invite_member capability. It is in this event, that you will require the line in the Tenant
post-processing hook tenant_signup Member.create_org_admin(user)
which also
creates the Member record for the initial admin on the account.
```
$ rails g resource member tenant:references user:references first_name:string last_name:string favorite_color:string
```
ADD to app/models/tenant.rb
```ruby
has_many :members, dependent: :destroy
```
ADD to app/models/user.rb
```ruby
has_one :member, :dependent => :destroy
```
EDIT app/models/member.rb
REMOVE belongs_to :tenant
ADD
```ruby
acts_as_tenant
DEFAULT_ADMIN = {
first_name: "Admin",
last_name: "Please edit me"
}
def self.create_new_member(user, params)
# add any other initialization for a new member
return user.create_member( params )
end
def self.create_org_admin(user)
new_member = create_new_member(user, DEFAULT_ADMIN)
unless new_member.errors.empty?
raise ArgumentError, new_member.errors.full_messages.uniq.join(", ")
end
return new_member
end
```
CREATE a form for inputting new member information for an invite
(below is a sample only)
app/views/members/new.html.haml
```ruby
%h1 Simple Milia App
.block#block-signup
%h2 Invite a new member into #{@org_name}
.content.login
.flash
- flash.each do |type, message|
%div{ :class => "message #{type}" }
%p= message
- flash.clear # clear contents so we won't see it again
= form_for(@member, :html => { :class => "form login" }) do |f|
- unless @member.errors.empty? && @user.errors.empty?
#errorExplanation.group
%ul
= @member.errors.full_messages.uniq.inject(''){|str, msg| (str << " #{msg}") }.html_safe
= @user.errors.full_messages.uniq.inject(''){|str, msg| (str << " #{msg}") }.html_safe
= fields_for( :user ) do |w|
.group
= w.label :email, :class => "label "
= w.text_field :email, :class => "text_field"
%span.description Ex. test@example.com; must be unique
.group
= f.label :first_name, :class => "label "
= f.text_field :first_name, :class => "text_field"
.group
= f.label :last_name, :class => "label "
= f.text_field :last_name, :class => "text_field"
.group
= f.label :favorite_color, :class => "label "
= f.text_field :favorite_color, :class => "text_field"
%span.description What is your favorite color?
.group.navform.wat-cf
%button.button{ :type => "submit" }
= image_tag "web-app-theme/icons/key.png"
Create user and invite
```
## authorized tenanted user landing page:
You will need a members-only landing page for after someone successfully signs into your app.
Here is what I typically do:
```ruby
# REPLACE the empty def index ... end with following ADD:
# this will give you improved handling for letting user know
# what is expected. If you want to have a welcome page for
# signed in users, uncomment the redirect_to line, etc.
def index
if user_signed_in?
# was there a previous error msg carry over? make sure it shows in flasher
flash[:notice] = flash[:error] unless flash[:error].blank?
redirect_to( welcome_path() )
else
if flash[:notice].blank?
flash[:notice] = "sign in if your organization has an account"
end
end # if logged in .. else first time
end
def welcome
end
```
## Milia API Reference Manual
### From controller-levels:
```ruby
set_current_tenant( tenant_id )
# raise InvalidTenantAccess unless tenant_id is one of the current_user valid tenants
```
set_current_tenant can be used to change the current_tenanted (for example, if a member
can belong to multiple tenants and wants to switch between them). See example else in this
README. NOTE: you will normally NEVER use this. Milia does this automatically during
authorize_tenant! so you never should at the beginning of a session.
### From model-levels:
```ruby
Tenant.current_tenant -- returns tenant object for the current tenant; nil if none
Tenant.current_tenant_id -- returns tenant_id for the current tenant; nil if none
```
If you need to gain access to tenant object itself (say to get the name of the tenant),
then use these accessor methods.
### From background, rake, or console-level (CAUTION):
From background jobs (only at the start of the task);
tenant can either be a tenant object or an integer tenant_id; anything else will raise
exception. set_current_tenant -- is model-level ability to set the current tenant
NOTE: *USE WITH CAUTION* normally this should *NEVER* be done from
the models ... it is only useful and safe WHEN performed at the start
of a background job (DelayedJob#perform) or at start of rails console, or a rake task.
```ruby
Tenant.set_current_tenant( tenant )
raise ArgumentError, "invalid tenant object or id"
```
## running tests
You must cd into the milia/test directory.
Then run test:units, test:functionals seperately.
For some reason, rake test won't work and yields errors.
```ruby
$ cd test
$ rake db:create
$ rake db:migrate
$ rake db:test:prepare
$ rake test:units
$ rake test:functionals
```
### test coverage
* All models, including milia-added methods, are tested.
* Functional testing currently covers all milia-added controller methods.
* TBD: milia overrides of devise registration, confirmation controllers
## Cautions
* 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 and will invalidate multi-tenanting; so *DO NOT USE default_scope*
* Milia uses Thread.current[:tenant_id] to hold the current tenant for the existing Action request in the application.
* 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.
* The tenant_id of a universal model will always be forced to nil.
* The tenant_id of a tenanted model will be set to the current_tenant of the current_user upon creation.
* HABTM (has_and_belongs_to_many) associations don't have models; they shouldn't have id fields
(setup as below) nor any field other than the joined references; they don't have a tenant_id field;
rails will invoke the default_scope of the appropriate joined table which does have a tenant_id field.
## Further documentation
* Check out the three-part blog discussion of _Multi-tenanting Ruby on Rails Applications on Heroku_
at: http://myrailscraft.blogspot.com/2013/05/multi-tenanting-ruby-on-rails.html
* See the Milia tutorial at: http://myrailscraft.blogspot.com/2013/05/multi-tenanting-ruby-on-rails_3982.html
* see code & setup sample in test/railsapp, which is also used to run the tests.
* see milia wiki on github for a CHANGE HISTORY page.
## Contributing to milia
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
* Fork the project
* Start a feature/bugfix branch
* Commit and push until you are happy with your contribution
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
* 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.
## Copyright
Copyright (c) 2014 Daudi Amani. See LICENSE.txt for further details.