Setting up a simple user permission management system
Posted by Stijn Pint on Jan 29, 2008
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 !