Click to download this project (zip file)

This is a comprehensive tutorial that goes through step-by-step of how to create a project using declarative _authorization.

First, we should define the scope of the application, the different roles users can have, and what level of access will be available based on those roles.

The application is essentially a collection of projects, each associated with a set of tasks and notes.

There will be two roles: admin and client.

Admin will have unrestricted access to all parts of the application.  They will be able to create new users, assign roles, create projects and thier associated tasks and notes, and designate which clients have access to which projects.

Clients will be able to view some projects, as designated by the admin, and associated notes and task where client access has been permited via a "client access flag" on each individual note and task.  Multiple clients may be able to see the same project.

Steps 1-14 cover installing restful_authentication and declarative_authorization

Steps 15-20 cover setting up this specific application

Steps 20-23 cover implementing declarative_authorization on your controllers.

Lets begin.

 

Step 1 - Create a new project called ClientAccess
Step 2 - Install restful_authentication

Restful_authentication is required for declarative_authorization to run.

script/plugin source http://svn.techno_weenie.net/projects/plugins

script/plugin install restful_authentication

 

 

Step 3 - Generate Authenticated User and Session

script/generate authenticated user session

Edit the created migration to add a "roles" column to the users table (note:  "roles" must be plural, the singular name is protected by Rails)

class CreateUsers < ActiveRecord::Migration
def self.up
create_table "users", :force => true do |t|
t.column :login,                     :string
t.column :email,                     :string
t.column :crypted_password,          :string, :limit => 40
t.column :salt,                      :string, :limit => 40
t.column :created_at,                :datetime
t.column :updated_at,                :datetime
t.column :remember_token,            :string
t.column :remember_token_expires_at, :datetime
t.column :roles,                     :text
end
end

def self.down
drop_table "users"
end
end

Perform a migration

rake:db migrate

Step 4 - Edit the application_controller

class ApplicationController < ActionController::Base

#the below include statement is originally in the users_controller.  You should comment it out there and add it here.

include AuthenticatedSystem

before_filter :set_current_user

protected

def set_current_user
Authorization.current_user = current_user
end

end

Step 5 - Add login mapping to the routes file

#replace the  #map.resource :session with :
map.resource :session, :controller => 'session'

#and add these restful routes
map.signup '/signup',:controller => 'users', :action => 'new'
map.login '/login',:controller => 'session', :action => 'new'
map.logout '/logout',:controller => 'session', :action => 'destroy'
map.edit '/edit',:controller => 'users', :action => 'edit'

Step 6 - Edit the session_controller to redirect users to somewhere useful on login and logout.

In this case, we are going to redirect users on login to the projects index (which we haven't created yet, but will shortly).  Users will be redirected to the login screen upon logout.

def create
....
redirect_back_or_default(:projects)
...
end

def destroy
...
redirect_back_or_default(:login)
...
end

 

Step 7 - Install declarative_authorization

gem sources -a http://gems.github.com

gem install stffn-declarative_authorization

Step 8 - Edith the config/environments.rb file to include the declarative_authorization gem

...

Rails::Initializer.run do |config|

config.gem "stffn-declarative_authorization", :lib => "declarative_authorization"

...

Step 9 - Create a ruby file in your config/ directory called authorization_rules.rb

This will be our one-stop shop for creating roles and assigning priviledges.

authorization do
role :admin do

has_permission_on :users, :to => :manage
has_permission_on :projects, :to => :manage
has_permission_on :tasks, :to => :manage
has_permission_on :notes, :to => :manage

end

role :client do
has_permission_on :projects, :to => :read do
#we are going to check that the client access flag for the project is equal to true
#AND that the user belongs to the project.  Since they both checks use if_attribute, it can
#be condensed to one simple statement.
if_attribute :cl_access => true, :users => contains {user}
end

#the if_permitted_to clause below ensures that the logged in client has access to read the project in
#order to read the notes or tasks for that project.  It guards from potential hackers structuring their
#own request in the internet browsers address bar.

has_permission_on :notes, :to => :read, :join_by => :and do
if_permitted_to :read, :project
end
has_permission_on :tasks, :to => :read, :join_by => :and do
if_permitted_to :read, :project
end
end

end # end authorization
# there are two method here that we haven't created yet, but will soon.
privileges do
privilege :manage, :includes => [:create, :read, :update, :delete, :show_project_notes, :show_project_tasks]
privilege :read, :includes => [:index, :show, :show_project_tasks]
privilege :create, :includes => :new
privilege :update, :includes => :edit
privilege :delete, :includes => :destroy
end ## end privileges declaration

Step 10 - Edit the users.rb model

add :roles to attr_accessible, insert the serialize :roles, Array and role_symbols definition:

attr_accessible :login, :email, :password, :password_confirmation, :roles

serialize :roles, Array

def role_symbols
@role_symbols ||= (roles || []).map {|r| r.to_sym}
end

Step 11- Create three new methods in the users_controller

These will allow us to view, and edit new users:

def index
@users = User.find(:all)

respond_to do |format|
format.html # index.html.erb
format.xml  { render :xml => @users }
end
end

def edit
@user = User.find(params[:id])
end

def update
@user = User.find(params[:id])

respond_to do |format|
if @user.update_attributes(params[:user])
flash[:notice] = 'user was successfully updated.'
format.html { redirect_to(User.new) }
format.xml  { head :ok }
else
format.html { render :action => "edit" }
format.xml  { render :xml => @user.errors, :status => :unprocessable_entity }
end
end
end

Step 12 - Create two new user views, index.html.erb and edit.html.erb

#index.html.erb :

<h1>Users</h1>
<table>
<tr>
<th>Login</th>
<th>Roles</th>
</tr>
<% for user in @users %>
<tr>
<td><b><%= h user.login %></b></td>
<td><%= h user.roles.map(&:to_s) * ',' if user.roles %></td>
<td><%= link_to 'Edit', edit_user_path(user)%> <%# if permitted_to? :edit, user %></td>
</tr>
<% end %>
</table>

#edit.html.erb :

<h1>Editing user: <%= h @user.login%></h1>
<% form_for(@user) do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :login %><br />
<%= f.text_field :login %>
</p>
<p>
<%= f.label :roles %><br />
<%= f.select :roles, (controller.authorization_engine.roles).uniq, {}, {:multiple => true} %>
</p>
<p>
<%= f.submit "Update" %>
</p>
<% end %>

<%= link_to 'Back', users_path %>

Step 13- Create our new users!

We are going to create three users.  One admin, and two clients.  The roles will be assigned in the next step by editing the users.

go to http://3000/users/new.  Create users with Login name: SiteAdmin, Client_1, and Client_2.

! Note - For the purposes of this tutorial, it is important that you create them in that order.  We reference the users table id directly later.

After creating all three you can view that they were created successfully by going to http://3000/users

Step 14- Edit users to assign roles

Next to each user, click Edit, select the appropriate role, and click Update.  You will notice there is a multi-select box with the roles that are specified in the authorization_rules.rb file.  Select admin for SiteAdmin, and client for Client_1 and Client_2.  You could theoretically assign multiple roles for each user, but the usefullness of that is beyond this tutorial.

Step 15 - Create the projects, tasks, and notes scaffold

For simplicity sake, we will have a minimal number of columns.  the project will only have a name and a client access flag, the notes will have the related project_id, a comment and client access flag, the tasks will have the related project_id, a description and cl_access flag.

script/generate scaffold project name:string cl_access:boolean

script/generate scaffold note project_id:integer comment:string cl_access:boolean

script/generate scaffold task project_id:integer description:string cl_access:boolean

Step 16 - create a scaffold for our user_groups

We will use an intermediate model to allow for multiple clients to have access to the same project.  It will hdeave only two columns, user_id and project_id

script/generate scaffold user_group user_id:integer project_id:integer

Edit the migration to constrain the to columns:

def self.up
create_table :user_groups do |t|
t.integer :user_id, :null => false, :options => "CONSTRAINT fk_user_group_users REFERENCES users(id)"
t.integer :project_id, :null => false, :options => "CONSTRAINT fk_user_group_projects REFERENCES projects(id)"

t.timestamps
end
end

run your rake to create the four tables from step 8 and 9

rake db:migrate

Step 17- Edit the models.

Add the has_many and belongs_to definitions to the models to create associations between projects<->tasks, projects<->notes, projects<->users

class Project < ActiveRecord::Base
has_many :tasks
has_many :notes
has_many :users, :through => :user_groups
has_many :user_groups
end

class Note < ActiveRecord::Base
belongs_to :project
end

class Task < ActiveRecord::Base
belongs_to :project
end


class UserGroup < ActiveRecord::Base
belongs_to :user
belongs_to :project
end

 

Step 18- Add two methods in the tasks_controller / notes_controller, and a before filter

class TasksController < ApplicationController
before_filter :load_project_tasks, :only => :show_project_tasks

...

def show_project_tasks
#using before filter @tasks= Task.find(:all, :conditions => ["project_id=:project_id",{:project_id => params[:id]}])
end

#### protected below here  #####
protected
def load_project_tasks
#using an if statement to test the users permission level.  There is likely a way to test
#directly agains the users role, but I haven't figured it out.  Only admin has permission
#to create projects, so this condition works for the time being.
if permitted_to?(:create, :projects)
@tasks= Task.find(:all, :conditions => ["project_id=?",params[:id]])
else  #if you are a client, only tasks that have the cl_access flag = true are pulled.
@tasks= Task.find(:all, :conditions => ["project_id=? and cl_access = ?", params[:id], true])
end
end

end # TasksController

class NotesController < ApplicationController
before_filter :load_project_notes, :only => :show_project_notes

...

def show_project_notes
#using before filter @notes= Note.find(:all, :conditions => ["project_id=:project_id",{:project_id => params[:id]}])
end

#### protected below here  #####
protected

def load_project_notes
if permitted_to?(:create, :projects)
@notes= Note.find(:all, :conditions => ["project_id=?",params[:id]])
else  #if you are a client, only notes that have the cl_access flag = true are pulled.
@notes= Note.find(:all, :conditions => ["project_id=? and cl_access = ?", params[:id], true])
end

end #end load_project_notes definition

end #end NotesController

 

Step 19- Create views for show_project_tasks and show_project_notes in their respective views folders

#show_project_notes.html.erb

<h1>Listing Project Notes</h1>
<% @notes.each do |note| %>
<%=h note.comment %><br/>
<% end %>

#show_project_tasks.html.erb

<h1>Listing Project Tasks</h1>
<% @tasks.each do |task| %>
<%=h task.description %><br/>
<% end %>

Step 20- Add links in the project index file to view the associated notes and tasks.

...

<td><%= link_to 'Notes', {:action => 'show_project_notes', :controller => 'notes', :id => project}%></td>
<td><%= link_to 'Tasks', {:action => 'show_project_tasks', :controller => 'tasks', :id => project}%></td>

...

Step 21- Implement declarative_authorization

We are going to add filter_access_to calls to the top of the projects_controller, notes_controller, and tasks_controller.  We are also going to edit the index definition to restrict visibility based on the roles and priviliges.  It is important that the filter_access_to calls are AFTER the before filters.

#tasks_controller.rb

class TasksController < ApplicationController
before_filter :load_project_tasks, :only => :show_project_tasks

filter_access_to :all, :attribute_check => true
filter_access_to :index, :attribute_check => false

def index
@tasks = Task.with_permissions_to(:read)

respond_to do |format|
format.html # index.html.erb
format.xml  { render :xml => @tasks }
end
end

...

#notes_controller.rb

class NotesController < ApplicationController
before_filter :load_project_notes, :only => :show_project_notes

filter_access_to :all, :attribute_check => true
filter_access_to :index, :attribute_check => false

def index
@notes = Note.with_permissions_to(:read)

respond_to do |format|
format.html # index.html.erb
format.xml  { render :xml => @tasks }
end
end

...

Step 22 - Test!

Wait a second - we need some data and client associations first.  We will log in as the administrator (SiteAdmin), create three projects, some tasks and notes for each.  Finally, we will assign users to those projects by creating user_groups.

htp://3000/login - enter credentials.  You should be brought to the project index page.

Create three new projects - maybe call them Project 1, Project 2, and Project 3 for simplicity sake.  Check the box for Client Access in Project 1 and Project 2, leave Project 3 unchecked.

Then navigate to http://3000/tasks

Create some tasks for each project.  For now, you can enter by hand the project_id (1, 2 , and 3, respectively).  I will leave it to you to figure a more elegant way of doing this in the future.

Then navigate to http://3000/notes

Create some notes for each project.  Again, you can enter by hand the project_id (1, 2 , and 3, respectively).

Then navigate to http://3000/groups

Create a new group. You will only need to create a group for Client_1 (enter 2 for the user since 1 is reserved for the SiteAdmin), and allow priviledge to Project 1 (enter 1 for the project).  Again - make this more elegant on your own time.

Create two more groups, this time for Client_2 (user 3), and allow priviledge to projects 2 and 3.

Step 23 - Test for Real!

Logout the admin by going to http://3000/logout

Login again, this time as Client_1.

You should be taken to the projects index page and should only see Project 1.  Click on Show.  You should be able to view the project.  Now edit the address as if you wanted to see project 2, like this:

http://3000/projects/2

You should recieve the message "You are not allowed to access this action."  Great!  Authentication is working for projects!

Now go back and click on Notes.  You should see the notes for that project.  Now try to hack it by editing the address again as such:

http://3000/notes/show_project_notes/2

You should recieve the same denial access message!

Go ahead and test the same for Tasks, and Client_2!

Step 24 - Now lets break it!

Logout the user, and log back in as the administrator.

navigate to the tasks index

http://3000/tasks

Delete the FIRST task for project one.

Go back to projects...

http://3000/projects

and click on Tasks for project 1 - oops!

ActiveRecord::RecordNotFound in TasksController#show_project_tasks

Couldn't find Task with ID=1

If anyone knows how to fix this so that step 24 doesn't break my project, I would be extremely grateful! It appears that the query in yellow below is what is causing the error. I can't figure out why it is searching the tasks table with the project.id (yes, I am certain that it is the project.id from previous errors.)


From the Console:
rocessing TasksController#show_project_tasks (for 127.0.0.1 at 2009-08-01 18:47:20) [GET]
Parameters: {"id"=>"1"}
User Load (16.0ms) SELECT * FROM "users" WHERE ("users"."id" = 1) LIMIT 1
Task Load (0.0ms) SELECT * FROM "tasks" WHERE (project_id='1') 
Task Load (0.0ms) SELECT * FROM "tasks" WHERE ("tasks"."id" = 1) 

ActiveRecord::RecordNotFound (Couldn't find Task with ID=1):
C:/Ruby/lib/ruby/gems/1.8/gems/stffn-declarative_authorization-0.3.0/lib/declarative_authorization/in_controller.rb:336:in `load_object'
C:/Ruby/lib/ruby/gems/1.8/gems/stffn-declarative_authorization-0.3.0/lib/declarative_authorization/in_controller.rb:302:in `permit!'
C:/Ruby/lib/ruby/gems/1.8/gems/stffn-declarative_authorization-0.3.0/lib/declarative_authorization/in_controller.rb:94:in `filter_access_filter'
C:/Ruby/lib/ruby/gems/1.8/gems/stffn-declarative_authorization-0.3.0/lib/declarative_authorization/in_controller.rb:94:in `each'
C:/Ruby/lib/ruby/gems/1.8/gems/stffn-declarative_authorization-0.3.0/lib/declarative_authorization/in_controller.rb:94:in `all?'
C:/Ruby/lib/ruby/gems/1.8/gems/stffn-declarative_authorization-0.3.0/lib/declarative_authorization/in_controller.rb:94:in `filter_access_filter'
-e:2:in `load'
-e:2

Rendered rescues/_trace (78.0ms)
Rendered rescues/_request_and_response (0.0ms)
Rendering rescues/layout (not_found)

 

 

 

 

 
Comments (1)
1 Friday, 29 January 2010 00:15
Scott Longberry
Thanks for the tutorial! A quick question though. Shouldn't the code in step 18, in the notes controller, that reads:

protected
if permitted_to?(:create, :projects)
@notes= Note.find(:all, :conditions => ["project_id=?",params[:id]])
else #if you are a client, only notes that have the cl_access flag = true are pulled.
@notes= Note.find(:all, :conditions => ["project_id=? and cl_access = ?", params[:id], true])
end

end #end NotesController

Actually be:

protected
def load_project_notes
if permitted_to?(:create, :projects)
@notes= Note.find(:all, :conditions => ["project_id=?",params[:id]])
else #if you are a client, only notes that have the cl_access flag = true are pulled.
@notes= Note.find(:all, :conditions => ["project_id=? and cl_access = ?", params[:id], true])
end
end

end #end NotesController

? I'm just trying to make sure I understand what is going on in the code.

Thanks,

Scott
Tuesday, 02 February 2010 20:00
nelson
Hi Scott,
Yes, you are correct. My copy paste must have been a little off. Sorry about that.
-Nelson

Add your comment

Your name:
Your email:
Your website:
Comment:
  The word for verification. Lowercase letters only with no spaces.
Word verification: