My apologies for this large, multi-pronged commit. What's here:

* Introduce Tracks::Config class to wrap environment.rb config settings
* Remove unused admin and index actions from user_controller
* Introduce flash partial and standardize on symbol keys for the flash hash
* Replace usages of render_partial with render :partial

Two new authentication options! These probably need documentation...

* Introduce LDAP authentication option (see configuration in environment.rb.tmpl). Thanks to Jeremy Evans for creating the SimpleLdapAuthenticator plugin. Note: the ldap auth integration test is likely to be fragile. Works for me on OS X with openldap, but your mileage may vary.
* Introduce Open ID authentication option (see configuration in environment.rb.tmpl and http://openid.net for more info). Thanks to East Media for the Open ID Consumer Plugin.
 
In environment.rb, you can enable any combination of the three auth options. If you have more than one selected, users can opt between them via their preferences pages. To play with the Open ID auth, you can get an identity at pip.verisignlabs.com.

Note that there are some new migrations to support the new authentication options, so don't forget to rake migrate!


git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@334 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
lukemelia 2006-11-05 10:41:59 +00:00
parent 4e0b459524
commit 99b734a52c
69 changed files with 1649 additions and 218 deletions

View file

@ -86,10 +86,10 @@ class ContextController < ApplicationController
# fallback for standard requests
if @saved
flash["notice"] = 'Added new next action.'
flash[:notice] = 'Added new next action.'
redirect_to :controller => 'todo', :action => 'list'
else
flash["warning"] = 'The next action was not added. Please try again.'
flash[:warning] = 'The next action was not added. Please try again.'
redirect_to :controller => 'todo', :action => 'list'
end
@ -97,7 +97,7 @@ class ContextController < ApplicationController
if request.xhr? # be sure to include an error.rjs
render :action => 'error'
else
flash["warning"] = 'An error occurred on the server.'
flash[:warning] = 'An error occurred on the server.'
redirect_to :controller => 'todo', :action => 'list'
end
end
@ -118,9 +118,9 @@ class ContextController < ApplicationController
return if request.xhr?
if @saved
flash['notice'] = "The action <strong>'#{@item.description}'</strong> was marked as <strong>#{@item.done? ? 'complete' : 'incomplete' }</strong>"
flash[:notice] = "The action <strong>'#{@item.description}'</strong> was marked as <strong>#{@item.done? ? 'complete' : 'incomplete' }</strong>"
else
flash['notice'] = "The action <strong>'#{@item.description}'</strong> was NOT marked as <strong>#{@item.done? ? 'complete' : 'incomplete' } due to an error on the server.</strong>"
flash[:notice] = "The action <strong>'#{@item.description}'</strong> was NOT marked as <strong>#{@item.done? ? 'complete' : 'incomplete' } due to an error on the server.</strong>"
end
redirect_to :action => "list"
end
@ -133,9 +133,9 @@ class ContextController < ApplicationController
@context.attributes = params["context"]
@context.name = deurlize(@context.name)
if @context.save
render_partial 'context_listing', @context
render :partial => 'context_listing', :object => @context
else
flash["warning"] = "Couldn't update new context"
flash[:warning] = "Couldn't update new context"
render :text => ""
end
end
@ -148,7 +148,7 @@ class ContextController < ApplicationController
if @context.destroy
render_text ""
else
flash["warning"] = "Couldn't delete context \"#{@context.name}\""
flash[:warning] = "Couldn't delete context \"#{@context.name}\""
redirect_to( :controller => "context", :action => "list" )
end
end
@ -178,7 +178,7 @@ class ContextController < ApplicationController
return @context
else
@context = nil # Should be nil anyway.
flash["warning"] = "Item and session user mis-match: #{@context.user_id} and #{@user.id}!"
flash[:warning] = "Item and session user mis-match: #{@context.user_id} and #{@user.id}!"
render_text ""
end
end
@ -189,7 +189,7 @@ class ContextController < ApplicationController
return @context
else
@context = nil
flash["warning"] = "Project and session user mis-match: #{@context.user_id} and #{@user.id}!"
flash[:warning] = "Project and session user mis-match: #{@context.user_id} and #{@user.id}!"
render_text ""
end
end
@ -199,7 +199,7 @@ class ContextController < ApplicationController
if @user == item.user
return item
else
flash["warning"] = "Item and session user mis-match: #{item.user.name} and #{@user.name}!"
flash[:warning] = "Item and session user mis-match: #{item.user.name} and #{@user.name}!"
render_text ""
end
end

View file

@ -2,7 +2,8 @@ class LoginController < ApplicationController
model :user, :preference
layout 'login'
skip_before_filter :set_session_expiration
open_id_consumer if Tracks::Config.auth_schemes.include?('open_id')
def login
@page_title = "TRACKS::Login"
case request.method
@ -13,15 +14,68 @@ class LoginController < ApplicationController
# of inactivity
session['noexpiry'] = params['user_noexpiry']
msg = (should_expire_sessions?) ? "will expire after 1 hour of inactivity." : "will not expire."
flash['notice'] = "Login successful: session #{msg}"
flash[:notice] = "Login successful: session #{msg}"
cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year }
redirect_back_or_default :controller => "todo", :action => "index"
else
@login = params['user_login']
flash['warning'] = "Login unsuccessful"
flash[:warning] = "Login unsuccessful"
end
end
end
def begin
# If the URL was unusable (either because of network conditions,
# a server error, or that the response returned was not an OpenID
# identity page), the library will return HTTP_FAILURE or PARSE_ERROR.
# Let the user know that the URL is unusable.
case open_id_response.status
when OpenID::SUCCESS
# The URL was a valid identity URL. Now we just need to send a redirect
# to the server using the redirect_url the library created for us.
# redirect to the server
redirect_to open_id_response.redirect_url((request.protocol + request.host_with_port + "/"), url_for(:action => 'complete'))
else
flash[:warning] = "Unable to find openid server for <q>#{params[:openid_url]}</q>"
redirect_to :action => 'login'
end
end
def complete
case open_id_response.status
when OpenID::FAILURE
# In the case of failure, if info is non-nil, it is the
# URL that we were verifying. We include it in the error
# message to help the user figure out what happened.
if open_id_response.identity_url
flash[:message] = "Verification of #{open_id_response.identity_url} failed. "
else
flash[:message] = "Verification failed. "
end
flash[:message] += open_id_response.msg.to_s
when OpenID::SUCCESS
# Success means that the transaction completed without
# error. If info is nil, it means that the user cancelled
# the verification.
@user = User.find_by_open_id_url(open_id_response.identity_url)
unless (@user.nil?)
flash[:message] = "You have successfully verified #{open_id_response.identity_url} as your identity."
session['user_id'] = @user.id
redirect_back_or_default :controller => 'todo', :action => 'index'
else
flash[:warning] = "You have successfully verified #{open_id_response.identity_url} as your identity, but you do not have a Tracks account. Please ask your administrator to sign you up."
end
when OpenID::CANCEL
flash[:message] = "Verification cancelled."
else
flash[:warning] = "Unknown response status: #{open_id_response.status}"
end
redirect_to :action => 'login' unless performed?
end
def signup
if User.find_all.empty? # the first user of the system
@ -53,7 +107,7 @@ class LoginController < ApplicationController
@user = User.authenticate(user.login, params['user']['password'])
@user.create_preference
@user.save
flash['notice'] = "Signup successful for user #{@user.login}."
flash[:notice] = "Signup successful for user #{@user.login}."
redirect_back_or_default :controller => "todo", :action => "index"
end
end
@ -70,8 +124,8 @@ class LoginController < ApplicationController
def logout
session['user_id'] = nil
reset_session
flash['notice'] = "You have been logged out of Tracks."
redirect_to :controller => "login", :action => "login"
flash[:notice] = "You have been logged out of Tracks."
redirect_to :action => "login"
end
def check_expiry

View file

@ -26,19 +26,19 @@ class NoteController < ApplicationController
note.attributes = params["new_note"]
if note.save
render_partial 'notes_summary', note
render :partial => 'notes_summary', :object => note
else
render_text ""
render :text => ''
end
end
def delete
note = check_user_return_note
if note.destroy
render_text ""
render :text => ''
else
flash["warning"] = "Couldn't delete note \"#{note.id.to_s}\""
render_text ""
flash[:warning] = "Couldn't delete note \"#{note.id.to_s}\""
render :text => ''
end
end
@ -46,10 +46,10 @@ class NoteController < ApplicationController
note = check_user_return_note
note.attributes = params["note"]
if note.save
render_partial 'notes', note
render :partial => 'notes', :object => note
else
flash["warning"] = "Couldn't update note \"#{note.id.to_s}\""
render_text ""
render :text => ''
end
end
@ -60,7 +60,7 @@ class NoteController < ApplicationController
if @user == note.user
return note
else
render_text ""
render :text => ''
end
end
end

View file

@ -34,7 +34,7 @@ class ProjectController < ApplicationController
@page_title = "TRACKS::Project: #{@project.name}"
if @contexts.empty?
flash['warning'] = 'You must add at least one context before adding next actions.'
flash[:warning] = 'You must add at least one context before adding next actions.'
end
if @not_done.empty?
@ -108,10 +108,10 @@ class ProjectController < ApplicationController
# fallback for standard requests
if @saved
flash["notice"] = 'Added new next action.'
flash[:notice] = 'Added new next action.'
redirect_to :controller => 'todo', :action => 'index'
else
flash["warning"] = 'The next action was not added. Please try again.'
flash[:warning] = 'The next action was not added. Please try again.'
redirect_to :controller => 'todo', :action => 'index'
end
@ -119,7 +119,7 @@ class ProjectController < ApplicationController
if request.xhr? # be sure to include an error.rjs
render :action => 'error'
else
flash["warning"] = 'An error occurred on the server.'
flash[:warning] = 'An error occurred on the server.'
redirect_to :controller => 'todo', :action => 'index'
end
end
@ -140,9 +140,9 @@ class ProjectController < ApplicationController
return if request.xhr?
if @saved
flash['notice'] = "The action <strong>'#{@item.description}'</strong> was marked as <strong>#{@item.done? ? 'complete' : 'incomplete' }</strong>"
flash[:notice] = "The action <strong>'#{@item.description}'</strong> was marked as <strong>#{@item.done? ? 'complete' : 'incomplete' }</strong>"
else
flash['notice'] = "The action <strong>'#{@item.description}'</strong> was NOT marked as <strong>#{@item.done? ? 'complete' : 'incomplete' } due to an error on the server.</strong>"
flash[:notice] = "The action <strong>'#{@item.description}'</strong> was NOT marked as <strong>#{@item.done? ? 'complete' : 'incomplete' } due to an error on the server.</strong>"
end
redirect_to :action => "list"
end
@ -152,13 +152,13 @@ class ProjectController < ApplicationController
def update
self.init
check_user_set_project
@project.attributes = params["project"]
@project.attributes = params['project']
@project.name = deurlize(@project.name)
if @project.save
render_partial 'project_listing', @project
render :partial => 'project_listing', :object => @project
else
flash["warning"] = "Couldn't update project"
render_text ""
flash[:warning] = "Couldn't update project"
render :text => ''
end
end
@ -178,9 +178,9 @@ class ProjectController < ApplicationController
def destroy
check_user_set_project
if @project.destroy
render_text ""
render :text => ''
else
flash["warning"] = "Couldn't delete project \"#{@project.name}\""
flash[:warning] = "Couldn't delete project \"#{@project.name}\""
redirect_to( :controller => "project", :action => "list" )
end
end
@ -210,8 +210,8 @@ class ProjectController < ApplicationController
return @project
else
@project = nil # Should be nil anyway
flash["warning"] = "Project and session user mis-match: #{@project.user_id} and #{@user.id}!"
render_text ""
flash[:warning] = "Project and session user mis-match: #{@project.user_id} and #{@user.id}!"
render :text => ''
end
end
@ -221,8 +221,8 @@ class ProjectController < ApplicationController
return @project
else
@project = nil
flash["warning"] = "Project and session user mis-match: #{@project.user_id} and #{@user.id}!"
render_text ""
flash[:warning] = "Project and session user mis-match: #{@project.user_id} and #{@user.id}!"
render :text => ''
end
end
@ -231,8 +231,8 @@ class ProjectController < ApplicationController
if @user == item.user
return item
else
flash["warning"] = "Item and session user mis-match: #{item.user.name} and #{@user.name}!"
render_text ""
flash[:warning] = "Item and session user mis-match: #{item.user.name} and #{@user.name}!"
render :text => ''
end
end

View file

@ -1,13 +1,9 @@
class UserController < ApplicationController
layout 'standard'
prepend_before_filter :login_required
def index
render_text "This will be our jumping-off point for managing user functions!"
end
def admin
render_text "You'll only be allowed to go here if you're an administrator."
if Tracks::Config.auth_schemes.include?('open_id')
open_id_consumer
before_filter :begin_open_id_auth, :only => :update_auth_type
end
verify :method => :post,
@ -22,7 +18,7 @@ class UserController < ApplicationController
#
def create
admin = User.find_admin
#render_text "user is " + session["user_id"].to_s + " and admin is " + a.id.to_s
#logger.debug "user is " + session["user_id"].to_s + " and admin is " + a.id.to_s
unless session["user_id"].to_i == admin.id.to_i
access_denied
return
@ -81,6 +77,71 @@ class UserController < ApplicationController
redirect_to :controller => 'user', :action => 'change_password'
end
end
def change_auth_type
@page_title = "TRACKS::Change authentication type"
end
def update_auth_type
if (params[:user][:auth_type] == 'open_id')
case open_id_response.status
when OpenID::SUCCESS
# The URL was a valid identity URL. Now we just need to send a redirect
# to the server using the redirect_url the library created for us.
# redirect to the server
redirect_to open_id_response.redirect_url((request.protocol + request.host_with_port + "/"), url_for(:action => 'complete'))
else
flash[:warning] = "Unable to find openid server for <q>#{params[:openid_url]}</q>"
redirect_to :action => 'change_auth_type'
end
return
end
@user.auth_type = params[:user][:auth_type]
if @user.save
flash[:notice] = "Authentication type updated."
redirect_to :controller => 'user', :action => 'preferences'
else
flash[:warning] = "There was a problem updating your authentication type: #{ @user.errors.full_messages.join(', ')}"
redirect_to :controller => 'user', :action => 'change_auth_type'
end
end
def complete
case open_id_response.status
when OpenID::FAILURE
# In the case of failure, if info is non-nil, it is the
# URL that we were verifying. We include it in the error
# message to help the user figure out what happened.
if open_id_response.identity_url
flash[:message] = "Verification of #{open_id_response.identity_url} failed. "
else
flash[:message] = "Verification failed. "
end
flash[:message] += open_id_response.msg.to_s
when OpenID::SUCCESS
# Success means that the transaction completed without
# error. If info is nil, it means that the user cancelled
# the verification.
@user.auth_type = 'open_id'
@user.open_id_url = open_id_response.identity_url
if @user.save
flash[:message] = "You have successfully verified #{open_id_response.identity_url} as your identity and set your authentication type to Open ID."
else
flash[:warning] = "You have successfully verified #{open_id_response.identity_url} as your identity but there was a problem saving your authentication preferences."
end
redirect_to :action => 'preferences'
when OpenID::CANCEL
flash[:message] = "Verification cancelled."
else
flash[:warning] = "Unknown response status: #{open_id_response.status}"
end
redirect_to :action => 'change_auth_type' unless performed?
end
def refresh_token
@user.crypt_word
@ -93,10 +154,10 @@ class UserController < ApplicationController
def do_change_password_for(user)
user.change_password(params[:updateuser][:password], params[:updateuser][:password_confirmation])
if user.save
flash["notice"] = "Password updated."
flash[:notice] = "Password updated."
return true
else
flash["warning"] = 'There was a problem saving the password. Please retry.'
flash[:warning] = 'There was a problem saving the password. Please retry.'
return false
end
end

View file

@ -140,4 +140,8 @@ module ApplicationHelper
link_to( descriptor, { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" )
end
def render_flash
render :partial => 'shared/flash'
end
end

View file

@ -10,17 +10,20 @@ class User < ActiveRecord::Base
attr_protected :is_admin
def self.authenticate(login, pass)
find_first(["login = ? AND password = ?", login, sha1(pass)])
candidate = find(:first, :conditions => ["login = ?", login])
return nil if candidate.nil?
if candidate.auth_type == 'database'
return candidate if candidate.password == sha1(pass)
elsif candidate.auth_type == 'ldap' && Tracks::Config.auth_schemes.include?('ldap')
return candidate if SimpleLdapAuthenticator.valid?(login, pass)
end
nil
end
def self.find_admin
find_first([ "is_admin = ?", true ])
end
def self.get_salt
SALT
end
def display_name
if first_name.blank? && last_name.blank?
return login
@ -44,7 +47,7 @@ class User < ActiveRecord::Base
protected
def self.sha1(pass)
Digest::SHA1.hexdigest("#{get_salt}--#{pass}--")
Digest::SHA1.hexdigest("#{Tracks::Config.salt}--#{pass}--")
end
before_create :crypt_password, :crypt_word
@ -54,10 +57,12 @@ protected
write_attribute("password", self.class.sha1(password)) if password == @password_confirmation
end
validates_presence_of :password, :login
validates_presence_of :login
validates_presence_of :password, :if => Proc.new{|user| user.auth_type == 'database'}
validates_length_of :password, :within => 5..40
validates_confirmation_of :password
validates_length_of :login, :within => 3..80
validates_uniqueness_of :login, :on => :create
validates_inclusion_of :auth_type, :in => Tracks::Config.auth_schemes, :message=>"not a valid authentication type"
validates_presence_of :open_id_url, :if => Proc.new{|user| user.auth_type == 'open_id'}
end

View file

@ -32,7 +32,7 @@
:update => "container_#{context.id}",
:complete => visual_effect(:appear, 'container_#{context.id}') %>
<table style="table-layout: fixed;" width="450">
<%= render_partial 'context_form', context %>
<%= render :partial => 'context_form', :object => context %>
<tr>
<td width="150">&nbsp;</td>
<td width="300"><input type="submit" value="Update" />&nbsp;<a href="javascript:void(0);" onclick="Element.toggle('context-<%= context.id %>','context-<%= context.id %>-edit-form');Form.reset('form-context-<%= context.id %>');">Cancel</a></td>

View file

@ -1,12 +1,8 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<div id="<%= name %>"<%= flash[name] ? "" : " style=\"display:none\""%>><%= flash[name] %></div>
<% end %>
<%= render_flash %>
<div id="list-contexts">
<% for context in @contexts %>
<%= render_partial( 'context_listing', context ) %>
<% end %>
<%= render :partial => 'context_listing', :collection => @contexts %>
</div>
<%= sortable_element 'list-contexts', get_listing_sortable_options %>

View file

@ -1,10 +1,6 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<%= render :partial => "context/context", :locals => { :context => @context, :collapsible => false } %>
<% unless @done.empty? -%>

View file

@ -1,10 +1,6 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<%= render :partial => "items" %>

View file

@ -1,10 +1,6 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<div id="feeds">
<div id="feedlegend">

View file

@ -1,29 +1,22 @@
<%= start_form_tag :action=> "login" %>
<% auth_schemes = Tracks::Config.auth_schemes -%>
<div title="Account login" id="loginform" class="form">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<h3>Please log in to use Tracks:</h3>
<% if @message %>
<div id="message"><%= @message %></div>
<% end %>
<h3>Please log in to use Tracks:</h3>
<% if auth_schemes.include?('database') || auth_schemes.include?('open_id') %>
<%= start_form_tag :action=> 'login' %>
<table>
<tr>
<td width="100px"><label for="user_login">Login:</label></td>
<td width="100px"><input type="text" name="user_login" id="user_login" size="20" value=""/></td>
<td width="100px"><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_password">Password:</label></td>
<td width="100px"><input type="password" name="user_password" id="user_password" size="20"/></td>
<td width="100px"><input type="password" name="user_password" id="user_password" class="login_text" /></td>
</tr>
<tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
@ -32,8 +25,28 @@
<td><input type="submit" name="login" value="Login &#187;" class="primary" /></td>
</tr>
</table>
<%= end_form_tag %>
<% end %>
<% if auth_schemes.include?('open_id') %>
<%= start_form_tag :action=> 'login', :action => 'begin' %>
<table>
<tr>
<td width="100px"><label for="openid_url">Identity URL:</label></td>
<td width="100px"><input type="text" name="openid_url" id="openid_url" value="" class="login_text open_id" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td><input type="submit" name="login" value="Verify &#187;" class="primary" /></td>
</tr>
</table>
<%= end_form_tag %>
<% end %>
</div>
<%= end_form_tag %>

View file

@ -3,11 +3,7 @@
<%= error_messages_for 'user' %><br/>
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<h3><%= @page_title -%></h3>

View file

@ -26,7 +26,7 @@
:html => { :id => "form-note-#{note.id}", :class => "inline-form" },
:update => "note-#{note.id}-container",
:complete => visual_effect(:appear, "note-#{note.id}-container") %>
<%= render_partial "note_edit_form", note %>
<%= render :partial => "note_edit_form", :object => note %>
<%= end_form_tag %>
</div><!-- [end:action-item.id-edit-form] -->

View file

@ -4,7 +4,7 @@
<% else -%>
<% for notes in @all_notes -%>
<div class="container" id="note-<%= notes.id %>-wrapper">
<%= render_partial "notes", notes %>
<%= render :partial => 'notes', :object => notes %>
</div>
<% end -%>
<% end -%>

View file

@ -1,5 +1,5 @@
<div id="display_box_projects">
<div class="container" id="note-<%= @note.id %>-wrapper">
<%= render_partial "notes", @note %>
<%= render :partial => 'notes', :object => @note %>
</div>
</div>

View file

@ -32,7 +32,7 @@
:update => "container_#{project.id}",
:complete => visual_effect(:appear, 'container_#{project.id}') %>
<table style="table-layout: fixed;" width="450">
<%= render_partial 'project_form', project %>
<%= render :partial => 'project_form', :object => project %>
<tr>
<td width="150">&nbsp;</td>
<td width="300"><input type="submit" value="Update" />&nbsp;<a href="javascript:void(0);" onclick="Element.toggle('project-<%= project.id %>','project-<%= project.id %>-edit-form');Form.reset('form-project-<%= project.id %>');">Cancel</a></td>

View file

@ -51,7 +51,7 @@
:html => { :id => "form-action-#{item.id}", :class => "inline-form" },
:update => "item-#{item.id}-container",
:complete => visual_effect(:appear, "item-#{item.id}-container") %>
<%= render_partial 'todo/action_edit_form', item %>
<%= render :partial => 'todo/action_edit_form', :object => item %>
<%= end_form_tag %>
</div><!-- [end:action-item.id-edit-form] -->

View file

@ -1,12 +1,8 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<div id="<%= name %>"<%= flash[name] ? "" : " style=\"display:none\""%>><%= flash[name] %></div>
<% end %>
<%= render_flash %>
<div id="list-projects">
<% for project in @projects %>
<%= render_partial( 'project_listing', project ) %>
<% end %>
<%= render :partial => 'project_listing', :collection => @projects %>
</div>
<%= sortable_element 'list-projects', get_listing_sortable_options %>
</div>

View file

@ -1,10 +1,6 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<%= render :partial => "project/project", :locals => { :project => @project, :collapsible => false } %>
<% unless @done.empty? -%>

View file

@ -0,0 +1,7 @@
<% for flash_key in [:notice, :warning, :message] %>
<% if flash.has_key?(flash_key) %>
<div id="<%= flash_key %>"><%= flash[flash_key] %></div>
<% else%>
<div id="<%= flash_key %>" style="display:none"><%= flash[flash_key] %></div>
<% end %>
<% end %>

View file

@ -1,5 +1,4 @@
page["form-action-#{@item.id}"].replace_html :partial => 'todo/edit_form'
page["item-#{@item.id}"].hide
page["action-#{@item.id}-edit-form"].show
page.visual_effect :appear, "action-#{@item.id}-edit-form", :duration => 0.2
page.call "Form.focusFirstElement", "form-action-#{@item.id}"

View file

@ -1,10 +1,6 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<%= render :partial => "context/context", :collection => @contexts_to_show,
:locals => { :collapsible => true } %>

View file

@ -1,10 +1,6 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<%= render :partial => "tickler_items" %>

View file

@ -0,0 +1,20 @@
<div id="single_box" class="container context authtype_container">
<h2>Change authentication type</h2>
<%= render_flash %>
<%= error_messages_for 'user' %>
<p>Select your new authentication type and click 'Change Authentication Type' to replace your current settings.</p>
<%= start_form_tag :action => 'update_auth_type' %>
<div><label for="user_auth_type">Authentication type:</label> <%= select('user', 'auth_type', Tracks::Config.auth_schemes.collect {|p| [ p, p ] }) %></div>
<div id="open_id" style="display:<%= @user.auth_type == 'open_id' ? 'block' : 'none' %>"><label for="user_open_id_url">Identity URL:</label> <input type="text" name="openid_url" value="<%= @user.open_id_url %>" class="open_id" /></div>
<div class="actions"><%= submit_tag 'Change Authentication Type' %> <%= link_to 'Cancel', :action => 'preferences' %></div>
<%= observe_field( :user_auth_type, :function => "$('open_id').style.display = value == 'open_id' ? 'block' : 'none'") %>
<%= end_form_tag %>
</div>

View file

@ -2,15 +2,11 @@
<h2><%= @page_title %></h2>
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render_flash %>
<%= error_messages_for 'user' %>
<%= error_messages_for 'user' %>
<p>Enter your new password in the fields below and click 'Change Password' to replace your current password with your new one (note that this will also change the URL you use to subscribe to your RSS or text field).</p>
<p>Enter your new password in the fields below and click 'Change Password' to replace your current password with your new one.</p>
<%= start_form_tag :action => 'update_password' %>
<table width="440px">

View file

@ -1,13 +1,9 @@
<div id="single_box" class="container context">
<div id="single_box" class="container context prefscontainer">
<%= render_flash %>
<h2>Your preferences</h2>
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<ul id="prefs">
<li>First name: <span class="highlight"><%= @user.first_name %></span></li>
<li>Last name: <span class="highlight"><%= @user.last_name %></span></li>
@ -30,16 +26,37 @@
<li>Refresh interval (in minutes): <span class="highlight"><%= @prefs.refresh %></span></li>
<li>Verbose action descriptors: <span class="highlight"><%= @prefs.verbose_action_descriptors %></span></li>
</ul>
<%= link_to "Edit preferences", :controller => 'user', :action => 'edit_preferences' %> |
<%= link_to 'Change password', :controller => 'user', :action => 'change_password' %>
<div class="actions">
<%= link_to "Edit preferences &raquo;", { :controller => 'user', :action => 'edit_preferences'}, :class => 'edit_link' %>
</div>
<h2>Your token</h2>
<div id="token_area">
<div class="description">Token (for feeds and API use):</div>
<div id="token><span class="highlight"><%= @user.word %></span></div>
<div class="token_regenerate">
<%= button_to "Generate a new token", { :controller => 'user', :action => 'refresh_token'},
:confirm => "Are you sure? Generating a new token will replace the existing one and break any external usages of this token." %>
</div>
</li>
</div>
<div class="description">Token (for feeds and API use):</div>
<div id="token><span class="highlight"><%= @user.word %></span></div>
<div class="token_regenerate">
<%= button_to "Generate a new token", { :controller => 'user', :action => 'refresh_token'},
:confirm => "Are you sure? Generating a new token will replace the existing one and break any external usages of this token." %>
</div>
</div>
<h2>Your authentication</h2>
<div id="authentication_area">
<% if Tracks::Config.auth_schemes.length > 1 %>
<p>Your authentication type is <span class="highlight"><%= @user.auth_type %></span>.
<div class="actions">
<%= link_to "Change your authentication type &raquo;", { :controller => 'user', :action => 'change_auth_type'}, :class => 'edit_link' %>
</div>
<% end %>
<% if @user.auth_type == 'database' %>
<div class="actions">
<%= link_to 'Change your password &raquo;', :controller => 'user', :action => 'change_password' %>
</div>
<% end %>
<% if @user.auth_type == 'open_id' %>
<p>Your Open ID URL is <span class="highlight"><%= @user.open_id_url %></span>.
<div class="actions">
<%= link_to 'Change Your Identity URL &raquo;', :controller => 'user', :action => 'change_auth_type' %></p>
</div>
<% end %>
</div>
</div>

View file

@ -60,9 +60,23 @@ SALT = "change-me"
require 'acts_as_namepart_finder'
require 'acts_as_todo_container'
require 'config'
ActiveRecord::Base.class_eval do
include Tracks::Acts::NamepartFinder
include Tracks::Acts::TodoContainer
end
AUTHENTICATION_SCHEMES = ['database'] #one or more of ['database', 'ldap', 'open_id']
if (AUTHENTICATION_SCHEMES.include? 'ldap')
require 'net/ldap' #requires ruby-net-ldap gem be installed
require 'simple_ldap_authenticator'
SimpleLdapAuthenticator.ldap_library = 'net/ldap'
SimpleLdapAuthenticator.servers = %w'localhost'
SimpleLdapAuthenticator.use_ssl = false
SimpleLdapAuthenticator.login_format = 'cn=%s,dc=example,dc=com'
end
if (AUTHENTICATION_SCHEMES.include? 'open_id')
#requires ruby-openid gem to be installed
end

View file

@ -0,0 +1,9 @@
class AddUserAuthType < ActiveRecord::Migration
def self.up
add_column :users, :auth_type, :string, :default => 'database', :null => false
end
def self.down
remove_column :users, :auth_type
end
end

View file

@ -0,0 +1,45 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class AddOpenIdTables < ActiveRecord::Migration
def self.up
create_table "open_id_associations", :force => true do |t|
t.column "server_url", :binary
t.column "handle", :string
t.column "secret", :binary
t.column "issued", :integer
t.column "lifetime", :integer
t.column "assoc_type", :string
end
create_table "open_id_nonces", :force => true do |t|
t.column "nonce", :string
t.column "created", :integer
end
create_table "open_id_settings", :force => true do |t|
t.column "setting", :string
t.column "value", :binary
end
end
def self.down
drop_table "open_id_associations"
drop_table "open_id_nonces"
drop_table "open_id_settings"
end
end

View file

@ -0,0 +1,9 @@
class AddUserOpenIdUrl < ActiveRecord::Migration
def self.up
add_column :users, :open_id_url, :string
end
def self.down
remove_column :users, :open_id_url
end
end

View file

@ -2,7 +2,7 @@
# migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
ActiveRecord::Schema.define(:version => 15) do
ActiveRecord::Schema.define(:version => 18) do
create_table "contexts", :force => true do |t|
t.column "name", :string, :default => "", :null => false
@ -19,6 +19,25 @@ ActiveRecord::Schema.define(:version => 15) do
t.column "updated_at", :datetime
end
create_table "open_id_associations", :force => true do |t|
t.column "server_url", :binary
t.column "handle", :string
t.column "secret", :binary
t.column "issued", :integer
t.column "lifetime", :integer
t.column "assoc_type", :string
end
create_table "open_id_nonces", :force => true do |t|
t.column "nonce", :string
t.column "created", :integer
end
create_table "open_id_settings", :force => true do |t|
t.column "setting", :string
t.column "value", :binary
end
create_table "preferences", :force => true do |t|
t.column "user_id", :integer, :default => 0, :null => false
t.column "date_format", :string, :limit => 40, :default => "%d/%m/%Y", :null => false
@ -70,6 +89,8 @@ ActiveRecord::Schema.define(:version => 15) do
t.column "is_admin", :integer, :limit => 4, :default => 0, :null => false
t.column "first_name", :string
t.column "last_name", :string
t.column "auth_type", :string, :default => "database", :null => false
t.column "open_id_url", :string
end
end

15
tracks/lib/config.rb Normal file
View file

@ -0,0 +1,15 @@
module Tracks
class Config
def self.salt
SALT
end
def self.auth_schemes
AUTHENTICATION_SCHEMES
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

View file

@ -132,4 +132,15 @@ div.memo {
#errorExplanation ul li {
font-size: 1em;
list-style: disc;
}
input.login_text {
width:200px;
}
input.open_id {
background: url(../images/open-id-login-bg.gif) no-repeat;
background-color: #fff;
background-position: 0 50%;
color: #000;
padding-left: 18px;
width:182px;
}

View file

@ -643,9 +643,10 @@ div.message {
}
ul#prefs {list-style-type: disc; margin-left: 5px;}
#token_area {
#token_area, #authentication_area {
text-align:center;
margin-top:20px;
margin-bottom:10px;
}
#token_area .description{
font-weight:bold;
@ -654,7 +655,14 @@ ul#prefs {list-style-type: disc; margin-left: 5px;}
width:100%;
text-align:center;
}
.prefscontainer .actions {
text-align:center;
margin-bottom:20px;
}
.authtype_container .actions {
margin-top:20px;
margin-bottom:20px;
}
#feedlegend {
padding: 2px;
border: 1px solid #CCC;
@ -688,3 +696,11 @@ ul#prefs {list-style-type: disc; margin-left: 5px;}
font-weight: normal;
font-style:oblique;
}
input.open_id {
background: url(../images/open-id-login-bg.gif) no-repeat;
background-color: #fff;
background-position: 0 50%;
color: #000;
padding-left: 18px;
width:182px;
}

View file

@ -2,17 +2,29 @@
admin_user:
id: 1
login: admin
password: <%= Digest::SHA1.hexdigest("#{User.get_salt()}--abracadabra--") %>
password: <%= Digest::SHA1.hexdigest("#{Tracks::Config.salt}--abracadabra--") %>
word: <%= Digest::SHA1.hexdigest("adminSat Feb 25 17:14:00 GMT 20060.236961325863376") %>
is_admin: true
first_name: Admin
last_name: Schmadmin
auth_type: database
other_user:
id: 2
login: jane
password: <%= Digest::SHA1.hexdigest("#{User.get_salt()}--sesame--") %>
password: <%= Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--") %>
word: <%= Digest::SHA1.hexdigest("janeSun Feb 19 14:42:45 GMT 20060.408173979260027") %>
is_admin: false
first_name: Jane
last_name: Doe
auth_type: database
ldap_user:
id: 3
login: john
password:
word: <%= Digest::SHA1.hexdigest("johnSun Feb 19 14:42:45 GMT 20060.408173979260027") %>
is_admin: false
first_name: John
last_name: Deere
auth_type: ldap

View file

@ -10,7 +10,7 @@ class BackendControllerTest < Test::Unit::TestCase
def setup
@controller = BackendController.new
request, response = ActionController::TestRequest.new, ActionController::TestResponse.new
assert_equal "change-me", User.get_salt()
assert_equal "change-me", Tracks::Config.salt
end
def test_new_todo_fails_with_incorrect_token

View file

@ -10,10 +10,11 @@ class LoginControllerTest < Test::Unit::TestCase
def setup
assert_equal "test", ENV['RAILS_ENV']
assert_equal "change-me", User.get_salt()
assert_equal "change-me", Tracks::Config.salt
@controller = LoginController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@num_users_in_fixture = User.count
end
#============================================
@ -33,7 +34,7 @@ class LoginControllerTest < Test::Unit::TestCase
assert_equal user.id, @response.session['user_id']
assert_equal user.login, "admin"
assert user.is_admin
assert_equal "Login successful: session will not expire.", flash['notice']
assert_equal "Login successful: session will not expire.", flash[:notice]
assert_redirect_url "http://#{@request.host}/bogus/location"
end
@ -43,7 +44,7 @@ class LoginControllerTest < Test::Unit::TestCase
assert_equal user.id, @response.session['user_id']
assert_equal user.login, "jane"
assert user.is_admin == false || user.is_admin == 0
assert_equal "Login successful: session will expire after 1 hour of inactivity.", flash['notice']
assert_equal "Login successful: session will expire after 1 hour of inactivity.", flash[:notice]
assert_redirected_to :controller => 'todo', :action => 'index'
end
@ -59,14 +60,14 @@ class LoginControllerTest < Test::Unit::TestCase
def test_login_bad_password
post :login, {:user_login => 'jane', :user_password => 'wrong', :user_noexpiry => 'on'}
assert_session_has_no :user
assert_equal "Login unsuccessful", flash['warning']
assert_equal "Login unsuccessful", flash[:warning]
assert_response :success
end
def test_login_bad_login
post :login, {:user_login => 'blah', :user_password => 'sesame', :user_noexpiry => 'on'}
assert_session_has_no :user
assert_equal "Login unsuccessful", flash['warning']
assert_equal "Login unsuccessful", flash[:warning]
assert_response :success
end
@ -81,7 +82,7 @@ class LoginControllerTest < Test::Unit::TestCase
admin = login('admin', 'abracadabra', 'on')
assert admin.is_admin
newbie = create('newbie', 'newbiepass')
assert_equal "Signup successful for user newbie.", flash['notice']
assert_equal "Signup successful for user newbie.", flash[:notice]
assert_redirected_to :controller => 'todo', :action => 'index'
assert_valid newbie
get :logout # logout the admin user
@ -92,8 +93,7 @@ class LoginControllerTest < Test::Unit::TestCase
assert_redirected_to :controller => 'todo', :action => 'index'
assert_equal 'newbie', user.login
assert user.is_admin == false || user.is_admin == 0
num_users = User.find(:all)
assert_equal num_users.length, 3
assert_equal User.count, @num_users_in_fixture + 1
end
# Test whether signup of new users is denied to a non-admin user
@ -103,9 +103,7 @@ class LoginControllerTest < Test::Unit::TestCase
assert non_admin.is_admin == false || non_admin.is_admin == 0
post :signup, :user => {:login => 'newbie2', :password => 'newbiepass2', :password_confirmation => 'newbiepass2'}
assert_template 'login/nosignup'
num_users = User.find(:all)
assert_equal num_users.length, 2
assert_number_of_users_is_unchanged
end
# ============================================
@ -117,8 +115,7 @@ class LoginControllerTest < Test::Unit::TestCase
assert admin.is_admin
assert_equal admin.id, @response.session['user_id']
post :create, :user => {:login => 'newbie', :password => '', :password_confirmation => ''}
num_users = User.find(:all)
assert_equal num_users.length, 2
assert_number_of_users_is_unchanged
assert_redirected_to :controller => 'login', :action => 'signup'
end
@ -127,8 +124,7 @@ class LoginControllerTest < Test::Unit::TestCase
assert admin.is_admin
assert_equal admin.id, @response.session['user_id']
post :create, :user => {:login => 'n', :password => 'newbiepass', :password_confirmation => 'newbiepass'}
num_users = User.find(:all)
assert_equal num_users.length, 2
assert_number_of_users_is_unchanged
assert_redirected_to :controller => 'login', :action => 'signup'
end
@ -140,8 +136,13 @@ class LoginControllerTest < Test::Unit::TestCase
assert_equal admin.id, @response.session['user_id']
post :create, :user => {:login => 'jane', :password => 'newbiepass', :password_confirmation => 'newbiepass'}
num_users = User.find(:all)
assert_equal num_users.length, 2
assert_number_of_users_is_unchanged
assert_redirected_to :controller => 'login', :action => 'signup'
end
private
def assert_number_of_users_is_unchanged
assert_equal User.count, @num_users_in_fixture
end
end

View file

@ -10,32 +10,12 @@ class UserControllerTest < Test::Unit::TestCase
def setup
assert_equal "test", ENV['RAILS_ENV']
assert_equal "change-me", User.get_salt()
assert_equal "change-me", Tracks::Config.salt
@controller = UserController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
# Test index with and without login
#
def test_index
get :index # should fail because no login
assert_redirected_to :controller => 'login', :action => 'login'
@request.session['user_id'] = users(:admin_user).id # log in the admin user
get :index
assert_response :success
end
# Test admin with and without login
#
def test_admin
get :admin # should fail because no login
assert_redirected_to :controller => 'login', :action => 'login'
@request.session['user_id'] = users(:admin_user).id # log in the admin user
get :admin
assert_response :success
end
def test_preferences
get :preferences # should fail because no login
assert_redirected_to :controller => 'login', :action => 'login'
@ -81,8 +61,8 @@ class UserControllerTest < Test::Unit::TestCase
post :update_password, :updateuser => {:password => 'newpassword', :password_confirmation => 'newpassword'}
assert_redirected_to :controller => 'user', :action => 'preferences'
@updated_user = User.find(users(:admin_user).id)
assert_equal @updated_user.password, Digest::SHA1.hexdigest("#{User.get_salt()}--newpassword--")
assert_equal flash['notice'], "Password updated."
assert_equal @updated_user.password, Digest::SHA1.hexdigest("#{Tracks::Config.salt}--newpassword--")
assert_equal flash[:notice], "Password updated."
end
def test_update_password_no_confirmation
@ -92,7 +72,7 @@ class UserControllerTest < Test::Unit::TestCase
post :update_password, :updateuser => {:password => 'newpassword', :password_confirmation => 'wrong'}
assert_redirected_to :controller => 'user', :action => 'change_password'
assert users(:admin_user).save, false
assert_equal flash['warning'], 'There was a problem saving the password. Please retry.'
assert_equal flash[:warning], 'There was a problem saving the password. Please retry.'
end
def test_update_password_validation_errors
@ -105,7 +85,7 @@ class UserControllerTest < Test::Unit::TestCase
# For some reason, no errors are being raised now.
#assert_equal 1, users(:admin_user).errors.count
#assert_equal users(:admin_user).errors.on(:password), "is too short (min is 5 characters)"
assert_equal flash['warning'], 'There was a problem saving the password. Please retry.'
assert_equal flash[:warning], 'There was a problem saving the password. Please retry.'
end
end

View file

@ -10,7 +10,7 @@ class CreateUserControllerTest < ActionController::IntegrationTest
fixtures :users
@@foobar_postdata = "<request><login>foo</login><password>bar</password></request>"
@@john_postdata = "<request><login>john</login><password>barracuda</password></request>"
@@johnny_postdata = "<request><login>johnny</login><password>barracuda</password></request>"
def setup
assert_test_environment_ok
@ -63,13 +63,13 @@ class CreateUserControllerTest < ActionController::IntegrationTest
def test_creates_new_user
initial_count = User.count
authenticated_post_xml_to_user_create @@john_postdata
authenticated_post_xml_to_user_create @@johnny_postdata
assert_response_and_body 200, "User created."
assert_equal initial_count + 1, User.count
john1 = User.find_by_login('john')
assert_not_nil john1, "expected user john to be created"
john2 = User.authenticate('john','barracuda')
assert_not_nil john2, "expected user john to be created"
johnny1 = User.find_by_login('johnny')
assert_not_nil johnny1, "expected user johnny to be created"
johnny2 = User.authenticate('johnny','barracuda')
assert_not_nil johnny2, "expected user johnny to be created"
end
def test_fails_with_get_verb

View file

@ -0,0 +1,117 @@
require "#{File.dirname(__FILE__)}/../test_helper"
require 'tempfile'
require 'user'
class LdapAuthTest < Test::Unit::TestCase
fixtures :users
SLAPD_BIN = "/usr/libexec/slapd" #You may need to adjust this
SLAPD_SCHEMA_DIR = "/etc/openldap/schema/" #You may need to adjust this
SLAPD_TEST_PORT = 10389
OUTPUT_DEBUG_INFO = false
def setup
assert_equal "test", ENV['RAILS_ENV']
assert_equal "change-me", Tracks::Config.salt
setup_ldap_server_conf
start_ldap_server
end
def teardown
stop_ldap_server
end
def test_authenticate_against_ldap
add_ldap_user_to_ldap_repository
user = User.authenticate('john', 'deere')
assert_not_nil(user)
assert_equal user.login, 'john'
end
def setup_ldap_server_conf
@slapd_conf = create_slapd_conf()
open(@slapd_conf.path) { |f| f.read }
unless File.exist?(SLAPD_BIN)
assert false, "slapd could not be found at #{SLAPD_BIN}. Adjust the path in #{__FILE__}"
end
end
def start_ldap_server
t = Thread.new(@slapd_conf.path) { |slapd_conf_path|
puts "starting slapd..." if OUTPUT_DEBUG_INFO
run_cmd %Q{/usr/libexec/slapd -f #{slapd_conf_path} -h "ldap://127.0.0.1:10389/"}
}
sleep(2)
run_cmd %Q{ldapsearch -H "ldap://127.0.0.1:10389/" -x -b '' -s base '(objectclass=*)' namingContexts}
end
def add_ldap_user_to_ldap_repository
ldif_file = create_ldif()
run_cmd %Q{ldapadd -H "ldap://127.0.0.1:10389/" -f #{ldif_file.path} -cxv -D "cn=Manager,dc=lukemelia,dc=com" -w secret}
puts `cat #{ldif_file.path}` if OUTPUT_DEBUG_INFO
end
def stop_ldap_server
pid = open(get_pid_file_path(@slapd_conf)) { |f| f.read }
run_cmd "kill -TERM #{pid}"
end
def create_slapd_conf
slapd_conf = Tempfile.new("slapd.conf")
slapd_conf.path
data_dir = slapd_conf.path + '-data'
pid_file = get_pid_file_path(slapd_conf)
Dir.mkdir(data_dir)
encrypted_password = `slappasswd -s secret`
open(slapd_conf.path, 'w') do |f|
f.puts %Q{include #{SLAPD_SCHEMA_DIR}core.schema
pidfile #{pid_file}
database ldbm
suffix "dc=lukemelia,dc=com"
rootdn "cn=Manager,dc=lukemelia,dc=com"
rootpw #{encrypted_password}
directory #{data_dir}
access to *
by self write
by users read
by anonymous auth
}
end
puts `cat #{slapd_conf.path}` if OUTPUT_DEBUG_INFO
slapd_conf
end
def create_ldif
ldif_file = Tempfile.new("ldap_user.ldif")
encrypted_password = `slappasswd -s deere`
open(ldif_file.path, 'w') do |f|
f.puts %Q{dn: dc=lukemelia,dc=com
objectclass: dcObject
objectclass: organization
o: Luke Melia DotCom
dc: lukemelia
dn: cn=john,dc=lukemelia,dc=com
cn: john
sn: john
objectclass: person
userPassword: #{encrypted_password}
}
end
ldif_file
end
def run_cmd(cmd)
puts cmd if OUTPUT_DEBUG_INFO
cmd_out = `#{cmd}`
puts cmd_out if OUTPUT_DEBUG_INFO
end
def get_pid_file_path(tempfile)
tempfile.path + '.pid'
end
end

View file

@ -2,9 +2,11 @@ ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'
class User < ActiveRecord::Base
def self.get_salt
"change-me"
module Tracks
class Config
def self.salt
"change-me"
end
end
end
@ -50,7 +52,7 @@ class ActionController::IntegrationTest
def assert_test_environment_ok
assert_equal "test", ENV['RAILS_ENV']
assert_equal "change-me", User.get_salt()
assert_equal "change-me", Tracks::Config.salt
end
def authenticated_post_xml(url, username, password, parameters, headers = {})

View file

@ -5,7 +5,7 @@ class UserTest < Test::Unit::TestCase
def setup
assert_equal "test", ENV['RAILS_ENV']
assert_equal "change-me", User.get_salt()
assert_equal "change-me", Tracks::Config.salt
@admin_user = User.find(1)
@other_user = User.find(2)
end
@ -16,7 +16,7 @@ class UserTest < Test::Unit::TestCase
assert_kind_of User, @admin_user
assert_equal 1, @admin_user.id
assert_equal "admin", @admin_user.login
assert_equal "#{Digest::SHA1.hexdigest("#{User.get_salt()}--abracadabra--")}", @admin_user.password
assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--abracadabra--")}", @admin_user.password
assert_not_nil @admin_user.word
assert @admin_user.is_admin
end
@ -26,7 +26,7 @@ class UserTest < Test::Unit::TestCase
assert_kind_of User, @other_user
assert_equal 2, @other_user.id
assert_equal "jane", @other_user.login
assert_equal "#{Digest::SHA1.hexdigest("#{User.get_salt()}--sesame--")}", @other_user.password
assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password
assert_not_nil @other_user.word
assert @other_user.is_admin == false || @other_user.is_admin == 0
end
@ -38,7 +38,7 @@ class UserTest < Test::Unit::TestCase
# Test a password shorter than 5 characters
#
def test_validate_short_password
assert_equal "#{Digest::SHA1.hexdigest("#{User.get_salt()}--sesame--")}", @other_user.password
assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password
@other_user.password = "four"
assert !@other_user.save
assert_equal 1, @other_user.errors.count
@ -48,7 +48,7 @@ class UserTest < Test::Unit::TestCase
# Test a password longer than 40 characters
#
def test_validate_long_password
assert_equal "#{Digest::SHA1.hexdigest("#{User.get_salt()}--sesame--")}", @other_user.password
assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password
@other_user.password = generate_random_string(41)
assert !@other_user.save
assert_equal 1, @other_user.errors.count
@ -58,7 +58,7 @@ class UserTest < Test::Unit::TestCase
# Test that correct length password is valid
#
def test_validate_correct_length_password
assert_equal "#{Digest::SHA1.hexdigest("#{User.get_salt()}--sesame--")}", @other_user.password
assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password
@other_user.password = generate_random_string(6)
assert @other_user.save
end

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,22 @@
OpenID Consumer
===============
Enable OpenID authentication and profile exchange from your application.
PRE-REQUISITES
--------------
* JanRain's Yadis and OpenID 1.2 libraries in Ruby.
* These can be obtained using 'gem install ruby-openid'
INSTALLATION
------------
To install you need to create a migration and add a controller.
./script/generate open_id_migration add_open_id_tables
./script/generate open_id_consumer_controller open_id
This can be used well in conjunction with a login system such as ActsAsAuthenticated

View file

@ -0,0 +1,39 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the open_id_consumer plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the open_id_consumer plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'OpenIdConsumer'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,86 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class OpenIdConsumerControllerGenerator < Rails::Generator::NamedBase
attr_reader :controller_name,
:controller_class_path,
:controller_file_path,
:controller_class_nesting,
:controller_class_nesting_depth,
:controller_class_name,
:controller_singular_name,
:controller_plural_name
alias_method :controller_file_name, :controller_singular_name
alias_method :controller_table_name, :controller_plural_name
def initialize(runtime_args, runtime_options = {})
runtime_args << 'open_id' if runtime_args.empty?
super
# Take controller name from the next argument. Default to the pluralized model name.
@controller_name = args.shift
@controller_name ||= ActiveRecord::Base.pluralize_table_names ? @name.pluralize : @name
base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name)
@controller_class_name_without_nesting, @controller_singular_name, @controller_plural_name = inflect_names(base_name)
if @controller_class_nesting.empty?
@controller_class_name = @controller_class_name_without_nesting
else
@controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
end
end
def manifest
record do |m|
# Check for class naming collisions.
m.class_collisions controller_class_path, "#{controller_class_name}Controller",
"#{controller_class_name}Helper"
# Controller, helper, views, and test directories.
m.directory File.join('app/controllers', controller_class_path)
m.directory File.join('app/helpers', controller_class_path)
m.directory File.join('app/views', controller_class_path, controller_file_name)
m.directory File.join('test/functional', controller_class_path)
m.template 'controller.rb',
File.join('app/controllers',
controller_class_path,
"#{controller_file_name}_controller.rb")
m.template 'functional_test.rb',
File.join('test/functional',
controller_class_path,
"#{controller_file_name}_controller_test.rb")
m.template 'helper.rb',
File.join('app/helpers',
controller_class_path,
"#{controller_file_name}_helper.rb")
# Controller templates
m.template "index.rhtml",
File.join('app/views', controller_class_path, controller_file_name, "index.rhtml")
end
end
protected
# Override with your own usage banner.
def banner
"Usage: #{$0} open_id_consumer_controller [open_id]"
end
end

View file

@ -0,0 +1,74 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class <%= controller_class_name %>Controller < ApplicationController
open_id_consumer :required => [:email, :nickname], :optional => [:fullname, :dob, :gender, :country]
def index
@title = 'Welcome'
end
def begin
# If the URL was unusable (either because of network conditions,
# a server error, or that the response returned was not an OpenID
# identity page), the library will return HTTP_FAILURE or PARSE_ERROR.
# Let the user know that the URL is unusable.
case open_id_response.status
when OpenID::SUCCESS
# The URL was a valid identity URL. Now we just need to send a redirect
# to the server using the redirect_url the library created for us.
# redirect to the server
redirect_to open_id_response.redirect_url((request.protocol + request.host_with_port + '/'), url_for(:action => 'complete'))
else
flash[:error] = "Unable to find openid server for <q>#{params[:openid_url]}</q>"
render :action => :index
end
end
def complete
case open_id_response.status
when OpenID::FAILURE
# In the case of failure, if info is non-nil, it is the
# URL that we were verifying. We include it in the error
# message to help the user figure out what happened.
if open_id_response.identity_url
flash[:message] = "Verification of #{open_id_response.identity_url} failed. "
else
flash[:message] = "Verification failed. "
end
flash[:message] += open_id_response.msg.to_s
when OpenID::SUCCESS
# Success means that the transaction completed without
# error. If info is nil, it means that the user cancelled
# the verification.
flash[:message] = "You have successfully verified #{open_id_response.identity_url} as your identity."
if open_id_fields.any?
flash[:message] << "<hr /> With simple registration fields:<br/>"
open_id_fields.each {|k,v| flash[:message] << "<br /><b>#{k}</b>: #{v}"}
end
when OpenID::CANCEL
flash[:message] = "Verification cancelled."
else
flash[:message] = "Unknown response status: #{open_id_response.status}"
end
redirect_to :action => 'index'
end
end

View file

@ -0,0 +1,34 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
require File.dirname(__FILE__) + '/../test_helper'
require '<%= controller_file_name %>_controller'
# Re-raise errors caught by the controller.
class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end
class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase
def setup
@controller = <%= controller_class_name %>Controller.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_truth
assert true
end
end

View file

@ -0,0 +1,19 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
module <%= controller_class_name %>Helper
end

View file

@ -0,0 +1,32 @@
<% # Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
%>
<h1><%%= @title %></h1>
<div><strong><%%= flash[:message] %></strong></div>
<div><strong><%%= flash[:error] %></strong></div>
<p>Please login with your OpenID Identity URL</p>
<div id="verify-form">
<%%= start_form_tag :action => 'begin' %>
Identity URL:
<input type="text" name="openid_url" style="width: 200px" />
<input type="submit" value="Verify" />
</form>
</div>

View file

@ -0,0 +1,29 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class OpenIdMigrationGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
runtime_args << 'add_open_id_tables' if runtime_args.empty?
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
end

View file

@ -0,0 +1,45 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class <%= class_name %> < ActiveRecord::Migration
def self.up
create_table "open_id_associations", :force => true do |t|
t.column "server_url", :binary
t.column "handle", :string
t.column "secret", :binary
t.column "issued", :integer
t.column "lifetime", :integer
t.column "assoc_type", :string
end
create_table "open_id_nonces", :force => true do |t|
t.column "nonce", :string
t.column "created", :integer
end
create_table "open_id_settings", :force => true do |t|
t.column "setting", :string
t.column "value", :binary
end
end
def self.down
drop_table "open_id_associations"
drop_table "open_id_nonces"
drop_table "open_id_settings"
end
end

View file

@ -0,0 +1,23 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class << ActionController::Base
def open_id_consumer(options = {})
include OpenIdConsumer::ControllerMethods
self.open_id_consumer_options = options
end
end

View file

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
puts IO.read(File.join(File.dirname(__FILE__), 'README'))

View file

@ -0,0 +1,103 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
begin
require_gem "ruby-openid", ">= 1.0"
rescue LoadError
require "openid"
end
module OpenIdConsumer
class ActiveRecordOpenIdStore < OpenID::Store
def get_auth_key
setting = Setting.find_by_setting 'auth_key'
if setting.nil?
auth_key = OpenID::Util.random_string(20)
setting = Setting.create :setting => 'auth_key', :value => auth_key
end
setting.value
end
def store_association(server_url, assoc)
remove_association(server_url, assoc.handle)
Association.create(:server_url => server_url,
:handle => assoc.handle,
:secret => assoc.secret,
:issued => assoc.issued,
:lifetime => assoc.lifetime,
:assoc_type => assoc.assoc_type)
end
def get_association(server_url, handle=nil)
assocs = handle.blank? ?
Association.find_all_by_server_url(server_url) :
Association.find_all_by_server_url_and_handle(server_url, handle)
assocs.reverse.each do |assoc|
a = assoc.from_record
if a.expired?
assoc.destroy
else
return a
end
end if assocs.any?
return nil
end
def remove_association(server_url, handle)
assoc = Association.find_by_server_url_and_handle(server_url, handle)
unless assoc.nil?
assoc.destroy
return true
end
false
end
def store_nonce(nonce)
use_nonce(nonce)
Nonce.create :nonce => nonce, :created => Time.now.to_i
end
def use_nonce(nonce)
nonce = Nonce.find_by_nonce(nonce)
return false if nonce.nil?
age = Time.now.to_i - nonce.created
nonce.destroy
age < 6.hours # max nonce age of 6 hours
end
def dumb?
false
end
# not part of the api, but useful
def gc
now = Time.now.to_i
# remove old nonces
nonces = Nonce.find(:all)
nonces.each {|n| n.destroy if now - n.created > 6.hours} unless nonces.nil?
# remove expired assocs
assocs = Association.find(:all)
assocs.each { |a| a.destroy if a.from_record.expired? } unless assocs.nil?
end
end
end

View file

@ -0,0 +1,31 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
begin
require_gem "ruby-openid", ">= 1.0"
rescue LoadError
require "openid"
end
module OpenIdConsumer
class Association < ActiveRecord::Base
set_table_name 'open_id_associations'
def from_record
OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
end
end
end

View file

@ -0,0 +1,69 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
begin
require_gem "ruby-openid", ">= 1.0"
rescue LoadError
require "openid"
end
module OpenIdConsumer
module ControllerMethods
def self.included(controller)
controller.class_eval do
verify :method => :post, :only => :begin, :params => :openid_url, :redirect_to => { :action => 'index' },
:add_flash => { :error => "Enter an Identity URL to verify." }
verify :method => :get, :only => :complete, :redirect_to => { :action => 'index' }
before_filter :begin_open_id_auth, :only => :begin
before_filter :complete_open_id_auth, :only => :complete
attr_reader :open_id_response
attr_reader :open_id_fields
cattr_accessor :open_id_consumer_options
end
end
protected
def open_id_consumer
@open_id_consumer ||= OpenID::Consumer.new(
session[:openid_session] ||= {},
ActiveRecordOpenIdStore.new)
end
def begin_open_id_auth
@open_id_response = open_id_consumer.begin(params[:openid_url])
add_sreg_params!(@open_id_response) if @open_id_response.status == OpenID::SUCCESS
end
def complete_open_id_auth
@open_id_response = open_id_consumer.complete(params)
return unless open_id_response.status == OpenID::SUCCESS
@open_id_fields = open_id_response.extension_response('sreg')
logger.debug "***************** sreg params ***************"
logger.debug @open_id_fields.inspect
logger.debug "***************** sreg params ***************"
end
def add_sreg_params!(openid_response)
open_id_consumer_options.keys.inject({}) do |params, key|
value = open_id_consumer_options[key]
value = value.collect { |v| v.to_s.strip } * ',' if value.respond_to?(:collect)
openid_response.add_extension_arg('sreg', key.to_s, value.to_s)
end
end
end
end

View file

@ -0,0 +1,22 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
module OpenIdConsumer
class Nonce < ActiveRecord::Base
set_table_name 'open_id_nonces'
end
end

View file

@ -0,0 +1,22 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
module OpenIdConsumer
class Setting < ActiveRecord::Base
set_table_name 'open_id_settings'
end
end

View file

@ -0,0 +1,5 @@
SimpleLdapAuthenticator
=======================
Allows for simple authentication to an LDAP server with a minimum of
configuration. See the RDoc for details.

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the simple_ldap_authenticator plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the simple_ldap_authenticator plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'SimpleLdapAuthenticator'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,2 @@
# Include hook code here
#require 'simple_ldap_authenticator'

View file

@ -0,0 +1 @@
# Install hook code here

View file

@ -0,0 +1,127 @@
# SimpleLdapAuthenticator
#
# This plugin supports both Ruby/LDAP and Net::LDAP, defaulting to Ruby/LDAP
# if it is available. If both are installed and you want to force the use of
# Net::LDAP, set SimpleLdapAuthenticator.ldap_library = 'net/ldap'.
# Allows for easily authenticating users via LDAP (or LDAPS). If authenticating
# via LDAP to a server running on localhost, you should only have to configure
# the login_format.
#
# Can be configured using the following accessors (with examples):
# * login_format = '%s@domain.com' # Active Directory, OR
# * login_format = 'cn=%s,cn=users,o=organization,c=us' # Other LDAP servers
# * servers = ['dc1.domain.com', 'dc2.domain.com'] # names/addresses of LDAP servers to use
# * use_ssl = true # for logging in via LDAPS
# * port = 3289 # instead of 389 for LDAP or 636 for LDAPS
# * logger = RAILS_DEFAULT_LOGGER # for logging authentication successes/failures
#
# The class is used as a global variable, you are not supposed to create an
# instance of it. For example:
#
# require 'simple_ldap_authenticator'
# SimpleLdapAuthenticator.servers = %w'dc1.domain.com dc2.domain.com'
# SimpleLdapAuthenticator.use_ssl = true
# SimpleLdapAuthenticator.login_format = '%s@domain.com'
# SimpleLdapAuthenticator.logger = RAILS_DEFAULT_LOGGER
# class LoginController < ApplicationController
# def login
# return redirect_to(:action=>'try_again') unless SimpleLdapAuthenticator.valid?(params[:username], params[:password])
# session[:username] = params[:username]
# end
# end
class SimpleLdapAuthenticator
class << self
@servers = ['127.0.0.1']
@use_ssl = false
@login_format = '%s'
attr_accessor :servers, :use_ssl, :port, :login_format, :logger, :connection, :ldap_library
# Load the required LDAP library, either 'ldap' or 'net/ldap'
def load_ldap_library
return if @ldap_library_loaded
if ldap_library
if ldap_library == 'net/ldap'
require 'net/ldap'
else
require 'ldap'
require 'ldap/control'
end
else
begin
require 'ldap'
require 'ldap/control'
ldap_library = 'ldap'
rescue LoadError
require 'net/ldap'
ldap_library = 'net/ldap'
end
end
@ldap_library_loaded = true
end
# The next LDAP server to which to connect
def server
servers[0]
end
# The connection to the LDAP server. A single connection is made and the
# connection is only changed if a server returns an error other than
# invalid password.
def connection
return @connection if @connection
load_ldap_library
@connection = if ldap_library == 'net/ldap'
Net::LDAP.new(:host=>server, :port=>(port), :encryption=>(:simple_tls if use_ssl))
else
(use_ssl ? LDAP::SSLConn : LDAP::Conn).new(server, port)
end
end
# The port to use. Defaults to 389 for LDAP and 636 for LDAPS.
def port
@port ||= use_ssl ? 636 : 389
end
# Disconnect from current LDAP server and use a different LDAP server on the
# next authentication attempt
def switch_server
self.connection = nil
servers << servers.shift
end
# Check the validity of a login/password combination
def valid?(login, password)
if ldap_library == 'net/ldap'
connection.authenticate(login_format % login.to_s, password.to_s)
begin
if connection.bind
logger.info("Authenticated #{login.to_s} by #{server}") if logger
true
else
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{connection.get_operation_result.code} #{connection.get_operation_result.message}") if logger
switch_server unless connection.get_operation_result.code == 49
false
end
rescue Net::LDAP::LdapError => error
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{error.message}") if logger
switch_server
false
end
else
connection.unbind if connection.bound?
begin
connection.bind(login_format % login.to_s, password.to_s)
connection.unbind
logger.info("Authenticated #{login.to_s} by #{server}") if logger
true
rescue LDAP::ResultError => error
connection.unbind if connection.bound?
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{error.message}") if logger
switch_server unless error.message == 'Invalid credentials'
false
end
end
end
end
end

View file

@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :simple_ldap_authenticator do
# # Task goes here
# end

View file

@ -0,0 +1,8 @@
require 'test/unit'
class SimpleLdapAuthenticatorTest < Test::Unit::TestCase
# Replace this with your real tests.
def test_this_plugin
flunk
end
end