Acunote is online project management and Scrum software. Acunote is fast and easy to use. It shows actual progress, not just wishful thinking. Click here to learn more.
« Back to posts

Supporting multiple organizations in a Rails application. Part I

Here goes a recipe for doing multi-org in Rails that we've cooked up in Pluron while working on Acunote. We went through multiple iterations of this in our own codebase, and in this series of articles, I'll present recipes showing how we did it. We'll finish with another iteration which uses improvements introduced in Rails 1.2.

Problem

The hosted web application you are building with Rails needs to handle multiple organizations. For example, there are several organizations (Org model), each has users in it (User model). Users can log in and see current projects (Project model) in the organization and a list of tasks in each of these projects (Task model). The schema (in PostgreSQL syntax) for such a database could look like:

create table Orgs (
  id serial not null,
  name varchar(20) not null,

  primary key(id));

create table Users (
  id serial not null,
  name varchar(50) not null,
  org_id integer not null,

  primary key(id),
  foreign key(org_id) references Orgs(id));

create table Projects (
  id serial not null,
  name varchar(50) not null,
  org_id integer not null,

  primary key(id),
  foreign key(org_id) references Orgs(id));

create table Tasks (
  id serial not null,
  description text,
  project_id integer not null,

  primary key(id),
  foreign key(project_id) references Projects(id));

Let's imagine now we have two organizations - Microsoft and Apple as our clients -- and our database contains following:


-- Orgs --
 id | name
----+-----------
  1 | Microsoft
  2 | Apple

-- Users --
 id | username | org_id
----+----------+--------
  1 | Bill     |      1
  2 | Steve    |      2

-- Projects --
 id | description      | org_id
----+------------------+--------
  1 | Launch Vista     |      1
  2 | Launch Leopard   |      2

-- Tasks --
 id | description                      | project_id
----+----------------------------------+------------
  1 | World Domination                 |          1
  2 | World Domination in a turtleneck |          2

We need our application so that users from one organization can only see data from that organization and not others. In other words, neither Bill nor Steve should find out that both of them are working towards world domination ;)

The Imaginary Case: No organizations at all

Before we dig into the problem let's take a look at how our application would work if we had just one organization.

Here and further in the article we'll consider TaskController with the usual CRUD operations (implementation of new/create is omitted as it is similar to edit/update) and ProjectController with one list action.

So, without any explicity organizations at all TaskController would look like:

class ProjectController < ApplicationController

  #lists projects
  def list
    @projects = Project.find(:all)
  end

end

class TaskController < ApplicationController

  #lists tasks in the project
  #example: /task/list?project=1
  def list
    @project = Project.find(params[:project])
    @tasks = @project.tasks.find(:all)
  end

  #shows the task edit form
  #example: /task/edit/1
  def edit
    @task = Task.find(params[:id])
  end

  #updates the task from the form
  #example: /task/update/1
  def update
    @task = Task.find(params[:id])
    if @task.update_attributes(params[:task])
      redirect_to :action => 'list', :params=> { :project => {@task.project.id} }
    else
      render :action => 'edit'
    end
  end

  #destroys the task
  #example: /task/destroy/1
  def destroy
    task = Task.find(params[:id])
    project = task.project
    task.destroy
    redirect_to :action => 'list', :params => { :project => project.id }
  end

end

This looks pretty simple, doesn't it? Now let's see how adding multiple organizations complicates this code.

Solution: Recipe #1

To implement multi-org system we need to

  • provide login mechanism for users;
  • list only projects in the current user's organization;
  • list and update only tasks for projects in the current user's organization.

Details of login mechanism are beyond the scope of this article. What's important is that after login our system stores the current user's id somewhere in the session, say session[:user_id]. While we can easily find user's organization given user_id by doing Org.find(session[:user_id]), we'll also store current user's org_id in the session[:org_id] to avoid unnecessary database lookups.

In ProjectController we now need to restrict the list of project by passing an extra condition.

class ProjectController < ApplicationController
  def list
    @projects = Project.find(:all, :conditions => ["org_id = ?", session[:org_id]])
  end
end

Things get slightly more complicated in TaskController. TaskController.list action should check that we use allowed project and don't list tasks from projects in other organizations. If the project is not allowed then we should forbid the operation somehow, for example by raising exception. CRUD actions should not only check that a project of this task is valid but that new value of the project_id passed by the form also refers to an allowed project, i.e. one in the same organization.

class TaskController < ApplicationController

  def list
    @project = allowed_projects.find(:first, :conditions => ["project_id = ?", params[:project_id]])
    forbid unless @project
    @tasks = @project.tasks.find(:all)
  end

  def edit
    @task = Task.find(params[:id])

    #check whether this task belongs to a project in this organization
    forbid unless allowed_projects.include? @task.project
  end

  def update
    @task = Task.find(params[:id])

    #first check whether this task belongs to a project in this organization
    forbid unless allowed_projects.include? @task.project

    #next check whether the project_id passed in params[:task][:project_id] is also allowed
    forbid unless allowed_projects.find(params[:task][:project_id])

    if @task.update_attributes(params[:task])
      redirect_to :action => 'list', :params=> { :project => {@task.project.id} }
    else
      render :action => 'edit'
    end
  end

  def destroy
    task = Task.find(params[:id])
    project = task.project
    #check whether this task belongs to a project in this organization
    forbid unless allowed_projects.include? project

    task.destroy
    redirect_to :action => 'list', :params => { :project => project.id }
  end

private
  def allowed_projects
    Project.find(:all, :conditions => ["org_id = ?", session[:org_id]])
  end

  def forbid
    raise RuntimeError , "Security violation - the task or project outside the organization is accessed"
  end
end

So far so good. This recipe is usable but doesn't this all look too complicated? Yes it does. Even more, while developing our application we must always think about what is allowed and what is not, add allowed_... before each find call. Things only get worse when it comes to other CRUD operations. But there's a better solution and we'll look at it in the next article.