Users, Permissions and Multitenant Sites

In my last article, I started to look at multitenant Web applications. These are applications that run a single time, but that can be retrieved via a variety of hostnames. As I explained in that article, even a simple application can be made multitenant by having it check the hostname used to connect to the HTTP server, and then by displaying a different set of content based on that.

For a simple set of sites, that technique can work well. But if you are working on a multitenant system, you more likely will need a more sophisticated set of techniques.

For example, I recently have been working on a set of sites that help people practice their language skills. Each site uses the same software but displays a different interface, as well as (obviously) a different set of words. Similarly, one of my clients has long operated a set of several dozen geographically targeted sites. Each site uses the same software and database, but appears to the outside world to be completely separate. Yet another reason to use a multitenant architecture is if you allow users to create their own sites—and, perhaps, add users to those private sites.

In this article, I describe how to set up all of the above types of sites. I hope you will see that creating such a multitenant system doesn't have to be too complex, and that, on the contrary, it can be a relatively easy way to provide a single software service to a variety of audiences.

Identifying the Site

In my last article, I explained how to modify /etc/passwd such that more than one hostname would be associated with the same IP address. Every multitenant site uses this same idea. A limited set of IP addresses (and sometimes only a single IP address) can be mapped to a larger number of hostnames and/or domain names. When a request comes in, the application first checks to see which site has been requested, and then decides what to do based on it.

The examples in last month's article used Sinatra, a lightweight framework for Web development. It's true that you can do sophisticated things with Sinatra, but when it comes to working with databases and large-scale projects, I prefer to use Ruby on Rails. So here I'm using Rails, along with a back end in PostgreSQL.

In order to do that, you first need to create a simple Rails application:


rails new -d postgresql multiatf

Then create a "multiatf" user in your PostgreSQL installation:


createuser multiatf

Finally, go into the multiatf directory, and create the database:


rake db:create

With this in place, you now have a working (if trivially simple) Rails application. Make sure you still have the following two lines in your /etc/hosts file:


127.0.0.1 atf1
127.0.0.1 atf2

And when you start up the Rails application:


rails s

you can go to http://atf1:3000 or http://atf2:3000, and you should see the same results—namely, the basic "hello" that you get from a Rails application before you have done anything.

The next step then is to create a default controller, which will provide actual content for your users. You can do this by saying:


rails g controller welcome

Now that you have a "welcome" controller, you should uncomment the appropriate route in config/routes.rb:


root 'welcome#index'

If you start your server again and go to http://atf1:3000, you'll now get an error message, because Rails knows to go to the "welcome" controller and invoke the "index" action, but no such action exists. So, you'll have to go into your controller and add an action:


def index
  render text: "Hello!"
end

With that in place, going to your home page gives you the text.

So far, that's not very exciting, and it doesn't add to what I explored in my last article. You can, of course, take advantage of the fact that your "index" method is rendering text, and that you can interpolate values into your text dynamically:


def index
  render text: "Hello, visitor to #{request.host}!"
end

But again, this is not what you're likely to want. You will want to use the hostname in multiple places in your application, which means that you'll repeatedly end up calling "request.host" in your application. A better solution is to assign a @hostname variable in a before_action declaration, which will ensure that it takes place for everyone in the system. You could create this "before" filter in your welcome controller, but given that this is something you'll want for all controllers and all actions, I think it would be wiser to put it in the application controller.

Thus, you should open app/controllers/application_controller.rb, and add the following:


before_action :get_hostname

def get_hostname
  @hostname = request.host
end

Then, in your welcome controller, you can change the "index" action to be:


def index
  render text: "Hello, visitor to #{@hostname}!"
end

Sure enough, your hostname now will be available as @hostname and can be used anywhere on your site.

Moving to the Database

In most cases, you'll want to move beyond this simple scheme. In order to do that, you should create a "hosts" table in the database. The idea is that the "hosts" table will contain a list of hostnames and IDs. It also might contain additional configuration information (I discuss that below). But for now, you can just add a new resource to the system. I even would suggest using the built-in scaffolding mechanism that Rails provides:


rails g scaffold hosts name:string

Why use a scaffold? I know that it's very popular among Rails developers to hate scaffolds, but I actually love them when I start a simple project. True, I'll eventually need to remove and rewrite parts, but I like being able to move ahead quickly and being able to poke and prod at my application from the very first moments.

Creating a scaffold in Rails means creating a resource (that is, a model, a controller that handles the seven basic RESTful actions and views for each of them), as well as the basic tests needed to ensure that the actions work correctly. Now, it's true that on a production system, you probably won't want to allow anyone and everyone with an Internet connection to create and modify existing hosts. And indeed, you'll fix this in a little bit. But for now, this is a good and easy way to set things up.

You will need to run the new migration that was created:


rake db:migrate

And then you will want to add your two sites into the database. One way to do this is to modify db/seeds.rb, which contains the initial data that you'll want in the database. You can use plain-old Active Record method calls in there, such as:


Host.create([{name: 'atf1'}, {name: 'atf2'}])

Before you add the seeded data, make sure the model will enforce some constraints. For example, in app/models/host.rb, I add the following:


validates :name, {:uniqueness => true}

This ensures that each hostname will appear only once in the "hosts" table. Moreover, it ensures that when you run rake db:seed, only new hosts will be added; errors (including attempts to enter the same data twice) will be ignored.

With the above in place, you can add the seeded data:


rake db:seed

Now, you should have two records in your "hosts" table:


[local]/multiatf_development=# select name from hosts;
--------
| name |
--------
| atf1 |
--------
| atf2 |
--------
(2 rows)

With this in place, you now can change your application controller:


before_action :get_host

def get_host
  @requested_host = Host.where(name: request.host).first

  if @requested_host.nil?
    render text: "No such host '#{request.host}'.", status: 500
    return false
  end

end

(By the way, I use @requested_host here, so as not to collide with the @host variable that will be set in hosts_controller.)

@requested_host is no longer a string, but rather an object. It, like @requested_host before, is an instance variable set in a before filter, so it is available in all of your controllers and views. Notice that it is now potentially possible for someone to access your site via a hostname that is not in your "hosts" table. If and when that happens, @requested_host will be nil, and you give an appropriate error message.

This also means that you now have to change your "welcome" controller, ever so slightly:


def index
  render text: "Hello, visitor to #{@requested_host.name}!"
end

This change, from the string @requested_host to the object @requested_host, is about much more than just textual strings. For one, you now can restrict access to your site, such that only those hosts that are active can now be seen. For example, let's add a new boolean column, is_active, to the "hosts" table:


rails g migration add_is_active_to_hosts

On my machine, I then edit the new migration:


class AddIsActiveToHosts < ActiveRecord::Migration
  def change
    add_column :hosts, :is_active, :boolean, default: true, 
     ↪null: false
  end
end

______________________

Reuven M. Lerner, Linux Journal Senior Columnist, a longtime Web developer, consultant and trainer, is completing his PhD in learning sciences at Northwestern University.