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.
Restful_authentication is required for declarative_authorization to run.
script/plugin source http://svn.techno_weenie.net/projects/plugins
script/plugin install restful_authentication
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
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
endend
#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'
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
gem sources -a http://gems.github.com
gem install stffn-declarative_authorization
...
Rails::Initializer.run do |config|
config.gem "stffn-declarative_authorization", :lib => "declarative_authorization"...
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
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
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
#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 %>
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
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.
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
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
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
endclass Note < ActiveRecord::Base
belongs_to :project
endclass Task < ActiveRecord::Base
belongs_to :project
end
class UserGroup < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
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
endend # 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 #####
protecteddef 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])
endend #end load_project_notes definition
end #end NotesController
#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 %>
...
<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>...
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...
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.
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!
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!
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"}
[4;36;1mUser Load (16.0ms)[0m [0;1mSELECT * FROM "users" WHERE ("users"."id" = 1) LIMIT 1[0m
[4;35;1mTask Load (0.0ms)[0m [0mSELECT * FROM "tasks" WHERE (project_id='1') [0m
[4;36;1mTask Load (0.0ms)[0m [0;1mSELECT * FROM "tasks" WHERE ("tasks"."id" = 1) [0m
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)
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
Yes, you are correct. My copy paste must have been a little off. Sorry about that.
-Nelson