Introduction
This is the third tutorial in my series of building applications using the Streamlined Framework. These are simple step by step procedures that are designed to show you (and remind me!) how to make this all work day in and day out. This tutorial adds authentication and authorization into the mix, showing you how to include this vital pieces into your application. We will be using the following two frameworks:
http://technoweenie.stikipad.com/plugins/show/Acts+as+Authenticated
and
Both of which seem to be stable and well written, but admittedly both have poor tutorials explaining how to use them effectively, especially for those of us who are NOT programmers by day, but just hacking this stuff for the fun of it.
NOTE\'''
As of October 1st, 2007, this is a bare bones tutorial which still needs work, especially since I haven't gone through and verified all my steps yet. Please feel free to make changes, but please also let me know you have made them so I can learn from them and incorporate them into other tutorials.
Thanks, John john@…
Requirements
I'm going to assume you did the first two tutorials and I'll just start by copying over the SimpleLetterAuth tutorial in it's entirety to use as the base to build upon.
> cp -rp SimpleLetterMenu SimpleLetterAuth > cd SimpleLetterAuth
Now we're going to start installing our plugins that we need, starting with the Acts_As_Authenticated.
> script/plugin install http://svn.techno-weenie.net/projects/plugins/acts_as_authenticated > script/generate authenticated user account > rake db:migrate
Add to the Letter controller (app/controllers/letter_controller.rb)
before_filter :login_required
Add to the Person controller (app/controllers/person_controller.rb)
before_filter :login_required
Edit app/controllers/account_controler.rb and remove the top two lines:
# Be sure to include AuthenticationSystem in Application Controller instead include AuthenticatedSystem
Then edit app/controllers/application.rb to have:
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
include AuthenticatedSystem
# Pick a unique cookie name to distinguish our session data from others'
session :session_key => '_SimpleLetter_session_id'
end
> script/server
- login and create an account.
- friggin ugly login screen.
- now, how do you logout?
- roles?
- ok, kill the server and let's starting hacking
Now we want to change the default silly installed index.rhtml form to actually have some useful info, such as who we are logged in as, and the ability to logout:
> edit app/views/account/index.rhtml
<% if not logged_in? %>
<%= form_tag :controller => 'account', :action => 'login' %>
<p><label for="login">Login</label><br/>
<%= text_field_tag 'login' %></p>
<p><label for="password">Password</label><br/>
<%= password_field_tag 'password' %></p>
<p><%= submit_tag 'Log in' %></p>
<%= end_form_tag %>
<%= link_to("Register for an Account", :controller => 'account', :action => 'signup') %>
<%= link_to("Forgot your password?", :controller => 'account', :action => 'forgot_password') %>
<% else %>
<%= form_tag :controller => 'account', :action => 'logout' %>
Logged in as: <br/>
<%= current_user.login %> <br/>
<%= submit_tag 'Log out' %></p>
<%= end_form_tag %>
<% end %>
Now we can login/logout properly. But how do we wrap this into the streamlined framework and CSS stuff? That's the tricky thing...
So once we're authenticated, we need authorization for various parts of our application. So now we get and install the authorization plugin to handle this aspect.
But first let's setup a default route to our 'letter' controller, which is allowed access by all users. This will serve to show us how to tie in authorization and authentication into an app nicely, and will also redirect our web site from it's base URL of http://localhost:3000/ to our letter controller.
> edit config/routes.rb
map.connect , :controller => "letter"
> rm public/index.html
And of course we want to have different roles for different users.
The 'admin' role can do anything, such as adding users, etc. The 'editor' role can edit Letters The 'reader' role can have Letters emailed to themselves. The 'guest' can only search for Letters from the main page.
We need to get the authorization plugin from http://rubyforge.net
> script/plugin install http://svn.writertopia.com/svn/plugins/authorization > edit config/environment.rb
- add to the end of the file
AUTHORIZATION_MIXIN = 'hardwired'
edit app/models/letter.rb, person.rb and add the following at the
top of the class definition. Note, you can't just add this to your app/models/application.rb file, it breaks for some reason.
acts_as_authorizable
edit app/models/user.rb
- add these two lines at the top, just under class definition:
acts_as_authorized_user
acts_as_authorizable
Now where the hell do we pre-define the various user roles? I guess
we could go back and setup a roles table and pre-populate it with a migration? Maybe that's easiest...
> script/generate role_model role
> script/generate migration setup_base_roles
> script/generate migration setup_base_user
> edit config/environment.rb and comment out the AUTHORIZATION_MIXIN line:
> edit db/migrate/005_setup_base_roles.rb
class SetupBaseRoles < ActiveRecord::Migration
def self.up
# Purge any/all roles...
down
# setup our roles
role = Role.create(:name => "admin")
role.save!
role = Role.create(:name => "editor")
role.save!
role = Role.create(:name => "reader")
role.save!
role = Role.create(:name => "guest")
role.save!
end
def self.down
end
end
> rake db:migrate
> script/console
- generate a user account by hand using the console for the initial testing.
>> u=User.create(:login => "admin", :password => "admin",
:password_confirmation => "admin", :email => "admin")
- note the values for salt & crypted_password
- exit the console
> edit db/migrate/006_setup_base_user.rb
class SetupBaseUser < ActiveRecord::Migration
def self.up
# Purge any and all users by default
down
# Setup the 'admin' user with a default password of 'admin', and
# a role of 'admin'. This is the user who can CRUD other users.
user = User.create(:login => "admin",
:email => "admin",
:password => "admin",
:password_confirmation => "admin")
user.save!
end
def self.down
end
end
Now let's do some playing to better understand how the Authorizable plugin actually works. The docs are terse and assume you're a Ruby god already, which I most certainly am not.
> script/console
>> u=User.create(:login => "foo", :email => "foo", :password =>
"foofoo", :password_confirmation => "foofoo")
...
>> u.is_admin?
false
>> u.is_admin
>> u.is_admin?
true
>> u.is_not_admin
>> i.is_admin?
false
>> u.is_gargoyle
...
>> u.is_gargoyle?
true
This is pretty neat how you can assign roles to a user and then check them.
> edit app/views/account/index.rhtml
<% if not logged_in? %>
<%= form_tag :controller => 'account', :action => 'login' %>
<p><label for="login">Login</label><br/>
<%= text_field_tag 'login' %></p>
<p><label for="password">Password</label><br/>
<%= password_field_tag 'password' %></p>
<p><%= submit_tag 'Log in' %></p>
<%= end_form_tag %>
<%= link_to("Register for an Account", :controller => 'account',
:action => 'signup') %>
<%= link_to("Forgot your password?", :controller => 'account',
:action => 'forgot_password') %>
<% else %>
<%= form_tag :controller => 'account', :action => 'logout' %>
Logged in as: <br/>
<%= current_user.login %> <br/>
<% who = User.find_by_login(current_user.login) %>
<% if who.is_admin? %>
You have admin privs!<br>
<% end %>
<%= submit_tag 'Log out' %></p>
<%= end_form_tag %>
<% end %>
script/server
- http://localhost:3000/account
- login as admin as a test...
- logout, then register and login again, note how you don't have admin privs now.
- logout
- login as admin as a test...
- http://localhost:3000/letter
- note how you must login
- note how you get sent back to the letter controller properly.
- Now we want to add in a menu header at the top to either
login/logout for the default streamlined stuff.
edit app/helpers/application_helper.rb
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
def streamlined_top_menus
[
[ "New Letter", {:action=>"new"}],
if logged_in?
[ "Logout", {:controller => 'account', :action => 'logout' } ]
else
["Login", {:controller => 'account', :action => 'login' } ]
end
]
end
end
But now we want to change how we filter accounts, since we really just want to assign people by default to the guest account, which only has privs to see records. You need to login to get more privs.
edit the Letter controller (app/controllers/letter_controller.rb) to modify the before_filter, since we now want to be able to allow access for guests who don't need to login at all. But to get more privs, you need to login.
edit app/controllers/letter_controller.rb
before_filter :login_required, :except => [:index]
Now we want to put in some more dynamic menus, depending on whether the user has certain privs or not. So we got back and edit:
edit app/helpers/application_helper.rb
And we'll change the previous streamlined_top_menus to look more like this:
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
def streamlined_top_menus
menu = [ "New Letter", {:action=>"new"} ]
if logged_in?
if current_user.has_role? 'admin'
menu << [ "Manage Users", {:controller => 'users', :action => 'list'} ]
end
menu << [ "Logout", {:controller => 'account', :action => 'logout' } ]
else
menu << [ "Login", {:controller => 'account', :action => 'login' } ]
end
return menu
end
end
I find this easier and neater to parse, and means it's also easier to keep track of trailing commas, since I tend to still use them like I do in perl scripts, since trailing commands in array definitions don't count, while in Ruby they're an error.
Now we'd also like to be able to edit users and their roles, so we'll now generate a controller and put it under streamlined controller.
> script/generate controller users > edit app/controllers/users_controller.rb
class UsersController < ApplicationController layout "streamlined" acts_as_streamlined before_filter :login_required end
script/server
We also want to make it so that only those users who have admin privs can access this page at all.
There's another couple of problems we need to address. When you logout, you're taken back by default to the login screen. And if you are at that screen and login, you also are left at the default Acts As Authenticated screen. We don't want that, we'd instead like to go back to the index of the Letters, so that users can view letters no matter what permissions they have.
edit app/controllers/account_controller.rb
We need to re-define the index from looking like this:
def index
redirect_to(:action => 'signup') unless logged_in? || User.count > 0
end
To instead look like this:
def index
redirect_to(:controller => '/letter', :action => 'index') unless
logged_in? || User.count > 0
end
And we modify the login method from:
redirect_back_or_default(:controller => '/account', :action => 'index')
to:
redirect_back_or_default(:controller => '/letter', :action => 'index')
We also need to lock down the streamlined_side_menus as defined in app/helpers/letter_helper.rb, and since we want to make this global, let's move this function to the app/helpers/application_helper.rb instead.
edit app/helpers/letter_helper.rb
- delete the def streamlined_side_menus
edit app/helpers/application_helper.rb
- add in the deleted streamlined_side_menus from letter_helper.rb and change it to look like this:
The next thing is that when editing users, we don't want to see or even be able to touch the encrypted password, or the Salt in the views. So we need to modify the following:
> edit app/streamlined/users_ui.rb
Streamlined.ui_for(User) do
user_columns :login, :email, :roles, :created_at, :updated_at
end
Note how this is different from the person_ui.rb file, which uses the older (supported for how long?) Streamlined Framework UI customization via inheritance.
The issue we see here is that the :roles are just returning the number of the role the user has, which isn't very helpful. We'd like to instead link through to the roles table, like this:
Streamlined.ui_for(User) do
user_columns :login,
:email,
:roles, {
:show_view =>
[ :list, { :fields => [:name]} ]
},
:created_at,
:updated_at
end
This makes it much nicer, and it's now even possible to edit a user's roles quickly and easily. It's not amazingly pretty, but it's decent. Note, see the Wiki page on #Relationships for more details.
TODO
- Cleanup the login page, or use a JavaScript? login popup.
- Keep writing and testing this tutorial
- Put in more on how to keep the Admin user from being deleted by accident
- Add in editing of user passwords (crib from another tutorial I found).
- Action Mailer setup should be covered, or maybe punted for now?
