This changeset introduces some integrated web service type features that take advantage

of the Rails 1.1 responds_to functionality. It also lays a foundation for future API
enhancements.

Basically, if you request the /projects, contexts/ or notes/ URLs with a client that specifies that it wants XML, Tracks will return XML. See DHH on the Accept header (http://www.loudthinking.com/arc/000572.html).

But there's a wrinkle. The controller actions mapped to these URLs are protected by an authentication filter. In normal use, Tracks redirects an unauthenticated user to the login screen for session-based authentication.

I've added a secondary authentication check that looks for a valid username and password coming from HTTP_BASIC authentication.

To test out the new functionality, try this:

curl -H 'Accept: application/xml' --basic --user YOUR_TRACKS_USERNAME:YOUR_TRACKS_PASSWORD http://localhost:3000/projects/

curl -H 'Accept: application/xml' --basic --user YOUR_TRACKS_USERNAME:YOUR_TRACKS_PASSWORD http://localhost:3000/contexts/

curl -H 'Accept: application/xml' --basic --user YOUR_TRACKS_USERNAME:YOUR_TRACKS_PASSWORD http://localhost:3000/notes/

HTTP_BASIC sends passwords in plain text, so the use of https is encouraged.

I haven't tested this on a shared host yet, but Coda Hale, whose simple_http_auth inspired this solution and provided some copy and paste code for it (thanks, Coda!), has some notes about how to make it work in his plugin readme (http://svn.codahale.com/simple_http_auth/README). To wit, putting the following in .htaccess:

  RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]

My thinking on this architecture is as follows:

1) Follow the spirit of responds_to and DRY to leverage existing controller code for API functionality
2) Get away from using the user token for API interactions. Let's keep it for feeds, so it's basically a "lite" form of security for read-only feeds.
3) Keep Tracks in shape to adopt the simply_restful plugin being developed alongside Rails Edge

There's no real new functionality in this release that the existing API didn't provide (except for seeing your notes as XML, and somehow I don't think people are clamoring for that), but this work is an important step to being able to implement the types of API features people have been asking for.

While I was at it, I did some refactoring to the login_controller for readability and style.

Finally, I replaced the activity indicator graphic to work with the new navigation background color.



git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@251 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
lukemelia 2006-06-04 07:07:42 +00:00
parent 3da6fe2525
commit 463a61f514
7 changed files with 93 additions and 43 deletions

View file

@ -2,7 +2,7 @@ class ContextController < ApplicationController
helper :todo
before_filter :login_required
prepend_before_filter :login_required
layout "standard"
def index
@ -16,6 +16,10 @@ class ContextController < ApplicationController
def list
self.init
@page_title = "TRACKS::List Contexts"
respond_to do |wants|
wants.html
wants.xml { render :xml => @contexts.to_xml }
end
end
# Filter the projects to show just the one passed in the URL

View file

@ -11,12 +11,8 @@ class LoginController < ApplicationController
session['user_id'] = @user.id
# If checkbox on login page checked, we don't expire the session after 1 hour
# of inactivity
session['noexpiry']= params['user_noexpiry']
if session['noexpiry'] == "on"
msg = "will not expire."
else
msg = "will expire after 1 hour of inactivity."
end
session['noexpiry'] = params['user_noexpiry']
msg = (should_expire_sessions?) ? "will not expire." : "will expire after 1 hour of inactivity."
flash['notice'] = "Login successful: session #{msg}"
redirect_back_or_default :controller => "todo", :action => "list"
else
@ -27,31 +23,19 @@ class LoginController < ApplicationController
end
def signup
if User.find_all.empty? # signup the first user as admin
if User.find_all.empty? # the first user of the system
@page_title = "Sign up as the admin user"
elsif session['user_id'] # we have someone logged in
get_admin_user
if session['user_id'] == @admin.id # logged in user is admin, so allow signup
@user = get_new_user
else
admin = User.find_admin
if current_user_is admin
@page_title = "Sign up a new user"
else
@user = get_new_user
else # all other situations (i.e. a non-admin is logged in, or no one is logged in, but we have some users)
@page_title = "No signups"
@admin_email = @admin.preferences["admin_email"]
@admin_email = admin.preferences["admin_email"]
render :action => "nosignup"
return
end
else # no-one logged in, but we have some Users
get_admin_user
@page_title = "No signups"
@admin_email = @admin.preferences["admin_email"]
render :action => "nosignup"
return
end
if session['new_user']
@user = session['new_user']
session['new_user'] = nil
else
@user = User.new
end
end
@ -74,7 +58,7 @@ class LoginController < ApplicationController
end
def delete
if params['id'] and ( params['id'] = @user.id or @user.is_admin )
if params['id'] and ( params['id'] == @user.id or @user.is_admin )
@user = User.find(params['id'])
# TODO: Maybe it would be better to mark deleted. That way user deletes can be reversed.
@user.destroy
@ -93,12 +77,8 @@ class LoginController < ApplicationController
# Gets called by periodically_call_remote to check whether
# the session has timed out yet
unless session == nil
return if @controller_name == 'feed' or session['noexpiry'] == "on"
# If the method is called by the feed controller
# (which we don't have under session control)
# or if we checked the box to keep logged in on login
# then the session is not going to get called
if session
return unless should_expire_sessions?
# Get expiry time (allow ten seconds window for the case where we have none)
expiry_time = session['expiry_time'] || Time.now + 10
@time_left = expiry_time - Time.now
@ -111,4 +91,24 @@ class LoginController < ApplicationController
end
end
private
def get_new_user
if session['new_user']
user = session['new_user']
session['new_user'] = nil
else
user = User.new
end
user
end
def current_user_is(user)
session['user_id'] && session['user_id'] == user.id
end
def should_expire_sessions?
session['noexpiry'] != "on"
end
end

View file

@ -1,13 +1,17 @@
class NoteController < ApplicationController
model :user
before_filter :login_required
prepend_before_filter :login_required
layout "standard"
def index
@all_notes = @user.notes
@page_title = "TRACKS::All notes"
respond_to do |wants|
wants.html
wants.xml { render :xml => @all_notes.to_xml }
end
end
def show

View file

@ -3,7 +3,7 @@ class ProjectController < ApplicationController
model :todo
helper :todo
before_filter :login_required
prepend_before_filter :login_required
layout "standard"
@ -18,6 +18,10 @@ class ProjectController < ApplicationController
def list
init
@page_title = "TRACKS::List Projects"
respond_to do |wants|
wants.html
wants.xml { render :xml => @projects.to_xml }
end
end
# Filter the projects to show just the one passed in the URL

View file

@ -15,6 +15,10 @@ class User < ActiveRecord::Base
find_first(["login = ? AND password = ?", login, sha1(pass)])
end
def self.find_admin
find_first([ "is_admin = ?", true ])
end
def change_password(pass,pass_confirm)
self.password = pass
self.password_confirmation = pass_confirm

View file

@ -50,6 +50,12 @@ module LoginSystem
return true
end
http_user, http_pass = get_basic_auth_data
if user = User.authenticate(http_user, http_pass)
session['user_id'] = user.id
return true
end
# store current location so that we can
# come back after the user logged in
store_location
@ -65,10 +71,10 @@ module LoginSystem
# example use :
# a popup window might just close itself for instance
def access_denied
if request.xhr?
render :partial => 'login/redirect_to_login'
else
redirect_to :controller=>"login", :action =>"login"
respond_to do |wants|
wants.html { redirect_to :controller=>"login", :action =>"login" }
wants.js { render :partial => 'login/redirect_to_login' }
wants.xml { basic_auth_denied }
end
end
@ -88,4 +94,32 @@ module LoginSystem
end
end
# HTTP Basic auth code adapted from Coda Hale's simple_http_auth plugin. Thanks, Coda!
def get_basic_auth_data
auth_locations = ['REDIRECT_REDIRECT_X_HTTP_AUTHORIZATION',
'REDIRECT_X_HTTP_AUTHORIZATION',
'X-HTTP_AUTHORIZATION', 'HTTP_AUTHORIZATION']
authdata = nil
for location in auth_locations
if request.env.has_key?(location)
authdata = request.env[location].to_s.split
end
end
if authdata and authdata[0] == 'Basic'
user, pass = Base64.decode64(authdata[1]).split(':')[0..1]
else
user, pass = ['', '']
end
return user, pass
end
def basic_auth_denied
response.headers["Status"] = "Unauthorized"
response.headers["WWW-Authenticate"] = "Basic realm=\"'Tracks Login Required'\""
render :text => "401 Unauthorized: You are not authorized to interact with Tracks.", :status => 401
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After