Setting up a simple user permission management system

For one of our current Ruby on Rails applications, we only had a simple login system, based on the acts_as_authenticated plugin.

However, there was a growing need to have some kind of (basic) permission system.

We wanted to keep it as lightweight as possible and came up with the following solution.

This tutorial starts from an existing project that is already using the acts_as_authenticated plugin for the user accounts.

What do we want to do ? We want to be able to grant or deny access to a “project” for a specific user. Of course, this also means that we need an “admin”-account to manage these permissions.

1. Create a new model / controller / views / migration :

user@your-pc:/project_path$ruby script/generate rspec_scaffold permission user_id:integer project_id:integer

This will create everything at once: the model permission.rb, the controller permissions_controller.rb, a scaffold for the view and the RSpec tests that go along with it. Your routes.rb file also be modified with the new RESTful routing entries for the permissions-section. Ofcourse you can choose to do everything manually or to use different type of generators, the result will be pretty much the same.

Now, modify the generated migration to look something like this:

class CreatePermissions < ActiveRecord::Migration
  def self.up
    add_column :users, :admin, :boolean, :default => 0
end
if admin = User.find_by_login("admin")
    admin.update_attributes(:admin => 1)
  else
    user = User.new( :login => admin,
      :password => yourpass,
      :password_confirmation => yourpass,
      :email => "admin@address.com" 
    )
    user.save!
  end
end
create_table :permissions do |t|
  t.column :user_id, :integer
  t.column :project_id, :integer
end
def self.down
  drop_table :permissions
  remove_column :users, :admin
end

Execute the migration:

user@your-pc:/project_path$ rake db:migrate

2. Set up the ActiveRecord relations

class Permissions < ActiveRecord::Base
  belongs_to :project
  belongs_to :user
  # Prevent duplicate permissions
  validates_uniqueness_of :user_id, :scope => project_id,
                                    :message => "already has the permission for this project." 
end
class Project < ActiveRecord::Base
  has_many :permissions, :dependent => :destroy
  # if you already have a relation "has_many :users", you'll have to rename this one.
  has_many :users, :through => :permissions
end
class User < ActiveRecord::Base
  has_many :permissions, :dependent => :destroy
  # if you already have a relation "has_many :projects", you'll have to rename this one.
  has_many :projects, :through => :permissions
end

3. Modify your controller / views.

Here I’m not going into detail, it’s just a matter of modifying the default scaffold layout according to your wishes. The admin user will need a clean overview of the current permissions and an easy way to add and remove them. Pretty basic stuff, but probably the part that will take the most of your time :-).

Now, let’s implement the admin restrictions.

class PermissionsController < ApplicationController
  before_filter :check_admin
  ...
  ...
end
class ApplicationController < ActionController::Base
  # acts_as_authenticated plugin
  include AuthenticatedSystem
  before_filter :login_required, :except => [:login]
  ...
  def check_admin
    unless admin?
      flash[:error] = "You don't have permission to access this page, please contact an Administrator." 
      redirect_to :controller => 'projects'
      return        
    end
  end
end
def admin?
  @admin ||= (current_user != :false ? current_user.admin : nil)
end
helper_method :admin?
...

That was cool ! With a few lines of code you’re preventing non-admin users from playing around with the permission-system. In your view, you can now make a conditional link to the permissions page thanks to the “helper_method :admin” :

<% if admin? %>
  <%= link_to "Manage Permissions", permissions_path, :class => "menuItem" %>
<% end %>

Great! Next step: use our brand new permission system to prevent users from accessing projects that don’t belong to them.

4. Implementing the permission security on the projects

Your index action in the projects_controller might look something like this :

def index
  @projects = Project.find(:all)
end
respond_to do |format|
  format.html # index.rhtml
  format.xml  { render :xml => @projects.to_xml }
end

At http://...../projects, this gives you an overwiew of all projects in the database.

First, make sure you’ve assigned some projects to yourself in your newly designed permission interface. Now change the first line to the following and watch what happens.

def index
  @projects = current_user.projects
  ...
end

WaW! Now you only get the projects on your screen to which you, the currently logged in user, has received permission to.

As an extra safety, you could add a before_filter to your projects_controller:

class ProjectsController < ApplicationController
  before_filter :check_permission, :except => [:index, :new, :create]
  ...
  private
  def check_permission
    begin
      @project = current_user.projects.find(params[:id])
    rescue  #record not found
      flash[:error] = "This project ain't none of your business !" 
      redirect_to :controller => 'projects'
      return        
    end
  end
  ...
end

I don’t think this last step is really necessary however… If you make sure that you always use current_user.projects.find(...) instead of just Project.find() (also in your other controllers), you should be safe.

Todo

I didn’t implement the interface for the user-management yet (assigning admin rights to users), but that’s really easy from here, I’m sure you can manage that :-).

Alternative(s)

It might have been better to use a Rails plugin for this, a quick search at http://agilewebdevelopment.com/plugins shows several potential solutions. Don’t hesitate to share your experience if you ‘ve used one of them !

Entries per category

  1. docpublisher (6)
  2. events (6)
  3. rails (6)
  4. ruby (15)
  5. xml (3)