authpds 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +5 -0
- data/Rakefile +38 -0
- data/lib/authpds.rb +17 -0
- data/lib/authpds/acts_as_authentic.rb +69 -0
- data/lib/authpds/controllers/authpds_controller.rb +68 -0
- data/lib/authpds/controllers/authpds_user_sessions_controller.rb +36 -0
- data/lib/authpds/exlibris/pds.rb +59 -0
- data/lib/authpds/institution.rb +41 -0
- data/lib/authpds/institution_list.rb +63 -0
- data/lib/authpds/session.rb +335 -0
- data/lib/authpds/version.rb +3 -0
- data/lib/tasks/pds-auth_tasks.rake +6 -0
- data/test/authpds_test.rb +7 -0
- data/test/fixtures/users.yml +49 -0
- data/test/support/config/institutions.yml +63 -0
- data/test/support/user.rb +9 -0
- data/test/support/user_session.rb +23 -0
- data/test/test_helper.rb +78 -0
- data/test/unit/pds_test.rb +72 -0
- data/test/unit/user_session_test.rb +123 -0
- data/test/unit/user_test.rb +77 -0
- metadata +111 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
= Authpds
|
2
|
+
|
3
|
+
This project provides a mechanism for authenticating via Ex Libris' Patron Directory Services (PDS) and provides hooks for making authorization decisions based on the user information provided by PDS. It leverages the authlogic gem and depends on a User-like model.
|
4
|
+
|
5
|
+
For con
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Authpds'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :test
|
data/lib/authpds.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support/dependencies'
|
2
|
+
require 'authlogic'
|
3
|
+
AUTHPDS_PATH = File.dirname(__FILE__) + "/authpds/"
|
4
|
+
[
|
5
|
+
'acts_as_authentic',
|
6
|
+
'session',
|
7
|
+
'institution',
|
8
|
+
'institution_list',
|
9
|
+
'exlibris/pds',
|
10
|
+
'controllers/authpds_controller'
|
11
|
+
].each do |library|
|
12
|
+
require AUTHPDS_PATH + library
|
13
|
+
end
|
14
|
+
if ActiveRecord::Base.respond_to?(:add_acts_as_authentic_module)
|
15
|
+
ActiveRecord::Base.send(:include, Authpds::ActsAsAuthentic)
|
16
|
+
end
|
17
|
+
Authlogic::Session::Base.send(:include, Authpds::Session)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Authpds
|
2
|
+
module ActsAsAuthentic
|
3
|
+
def self.included(klass)
|
4
|
+
klass.class_eval do
|
5
|
+
add_acts_as_authentic_module(InstanceMethods, :prepend)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def self.included(klass)
|
11
|
+
klass.class_eval do
|
12
|
+
serialize :user_attributes
|
13
|
+
attr_accessor :expiration_date
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
public
|
18
|
+
# Setting the username field also resets the persistence_token if the value changes.
|
19
|
+
def username=(value)
|
20
|
+
write_attribute(:username, value)
|
21
|
+
reset_persistence_token if username_changed?
|
22
|
+
end
|
23
|
+
|
24
|
+
def primary_institution
|
25
|
+
return nil unless InstitutionList.institutions_defined?
|
26
|
+
InstitutionList.instance.get(user_attributes[:primary_institution]) unless user_attributes.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def primary_institution=(primary_institution)
|
30
|
+
primary_institution = primary_institution.name if primary_institution.is_a?(Institution)
|
31
|
+
raise ArgumentError.new(
|
32
|
+
"Institution #{primary_institution} does not exist.\n" +
|
33
|
+
"Please maker sure the institutions yaml file is configured correctly.") if InstitutionList.instance.get(primary_institution).nil?
|
34
|
+
self.user_attributes=({:primary_institution => primary_institution})
|
35
|
+
end
|
36
|
+
|
37
|
+
def institutions
|
38
|
+
return nil unless InstitutionList.institutions_defined?
|
39
|
+
user_attributes[:institutions].collect { |institution|
|
40
|
+
InstitutionList.instance.get(institution) } unless user_attributes.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def institutions=(institutions)
|
44
|
+
raise ArgumentError.new(
|
45
|
+
"Institutions input should be an array.") unless institutions.is_a?(Array)
|
46
|
+
filtered_institutions = institutions.collect { |institution|
|
47
|
+
institution = institution.name if institution.is_a?(Institution)
|
48
|
+
institution unless InstitutionList.instance.get(institution).nil?
|
49
|
+
}
|
50
|
+
self.user_attributes=({:institutions => filtered_institutions})
|
51
|
+
end
|
52
|
+
|
53
|
+
# "Smart" updating of user_attributes. Maintains user_attributes that are not explicity overwritten.
|
54
|
+
def user_attributes=(new_attributes)
|
55
|
+
write_attribute(:user_attributes, new_attributes) and return unless new_attributes.kind_of?(Hash)
|
56
|
+
# Set new/updated attributes
|
57
|
+
write_attribute(:user_attributes, (user_attributes || {}).merge(new_attributes))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a boolean based on whether the User has been refreshed recently.
|
61
|
+
# If User#refreshed_at is older than User#expiration_date, the User is expired and the data
|
62
|
+
# may need to be refreshed.
|
63
|
+
def expired?
|
64
|
+
# If the record is older than the expiration date, it is expired.
|
65
|
+
(refreshed_at.nil?) ? true : refreshed_at < expiration_date
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Authpds
|
2
|
+
module Controllers
|
3
|
+
module AuthpdsController
|
4
|
+
|
5
|
+
def self.included(klass)
|
6
|
+
klass.class_eval do
|
7
|
+
include InstanceMethods
|
8
|
+
helper_method :current_user_session, :current_user, :current_primary_institution
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
|
14
|
+
# Get the current UserSession if it exists
|
15
|
+
def current_user_session
|
16
|
+
@current_user_session ||= UserSession.find
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get the current User if there is a UserSession
|
20
|
+
def current_user
|
21
|
+
@current_user ||= current_user_session.record unless current_user_session.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Determine current primary institution based on:
|
25
|
+
# 0. institutions are not being used (returns nil)
|
26
|
+
# 1. institution query string parameter in URL
|
27
|
+
# 2. institution associated with the client IP
|
28
|
+
# 3. primary institution for the current user
|
29
|
+
# 4. first default institution
|
30
|
+
def current_primary_institution
|
31
|
+
@current_primary_institution ||=
|
32
|
+
(InstitutionList.institutions_defined?) ?
|
33
|
+
(params["institution"].nil? or InstitutionList.instance.get(params["institution"]).nil?) ?
|
34
|
+
(primary_institution_from_ip.nil?) ?
|
35
|
+
(current_user.nil? or current_user.primary_institution.nil?) ?
|
36
|
+
InstitutionList.instance.default_institutions.first :
|
37
|
+
current_user.primary_institution :
|
38
|
+
primary_institution_from_ip :
|
39
|
+
InstitutionList.instance.get(params["institution"]) :
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
# Grab the first institution that matches the client IP
|
44
|
+
def primary_institution_from_ip
|
45
|
+
InstitutionList.instance.institutions_with_ip(request.remote_ip).first unless request.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Determine institution layout based on:
|
49
|
+
# 1. primary institution's resolve_layout
|
50
|
+
# 2. default - views/layouts/application
|
51
|
+
def institution_layout
|
52
|
+
(current_primary_institution.nil? or current_primary_institution.application_layout.nil?) ?
|
53
|
+
:application : current_primary_institution.application_layout
|
54
|
+
end
|
55
|
+
|
56
|
+
# Override to add institution.
|
57
|
+
def url_for(options={})
|
58
|
+
options["institution"] = params["institution"] unless params["institution"].nil? or options["institution"]
|
59
|
+
super(options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def user_session_redirect_url(url)
|
63
|
+
(url.nil?) ? (request.referer.nil?) ? root_url : request.referer : url
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Authpds
|
2
|
+
module Controllers
|
3
|
+
module AuthpdsUserSessionsController
|
4
|
+
|
5
|
+
# GET /user_sessions/new
|
6
|
+
# GET /login
|
7
|
+
def new
|
8
|
+
@user_session = UserSession.new(params)
|
9
|
+
@user_session.before_login(params) and return if performed?
|
10
|
+
redirect_to @user_session.login_url(params) unless @user_session.login_url.nil?
|
11
|
+
raise RuntimeError.new( "Error in #{self.class}.\nNo login url defined") if @user_session.login_url.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
# GET /validate
|
15
|
+
def validate
|
16
|
+
@user_session = UserSession.new(params[:user_session])
|
17
|
+
@user_session.save do |result|
|
18
|
+
@user_session.errors.each_full {|error|
|
19
|
+
flash[:error] = "There was an error logging in. #{error}"
|
20
|
+
logger.error("Error in #{self.class} while saving user session. #{error}")
|
21
|
+
} unless result
|
22
|
+
redirect_to (params[:return_url].nil?) ? root_url : params[:return_url]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# DELETE /user_sessions/1
|
27
|
+
# GET /logout
|
28
|
+
def destroy
|
29
|
+
user_session = UserSession.find
|
30
|
+
logout_url = user_session.logout_url(params)
|
31
|
+
user_session.destroy unless user_session.nil?
|
32
|
+
redirect_to user_session_redirect_url(logout_url)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Authpds
|
2
|
+
module Exlibris
|
3
|
+
module Pds
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'uri'
|
6
|
+
require 'net/http'
|
7
|
+
|
8
|
+
# Makes a call to the PDS get-attribute API.
|
9
|
+
# Defaults attribute equal to "bor_info".
|
10
|
+
# Raises an exception on if it encounters errors.
|
11
|
+
class GetAttribute
|
12
|
+
attr_reader :response, :error
|
13
|
+
|
14
|
+
protected
|
15
|
+
# Call to the PDS API.
|
16
|
+
def initialize(pds_url, calling_system, pds_handle, attribute)
|
17
|
+
raise ArgumentError.new("Argument Error in #{self.class}. :pds_url not specified in config.") if pds_url.nil?;
|
18
|
+
raise ArgumentError.new("Argument Error in #{self.class}. :calling_system not specified in config.") if calling_system.nil?;
|
19
|
+
raise ArgumentError.new("Argument Error in #{self.class}. :pds_handle is null.") if pds_handle.nil?;
|
20
|
+
raise ArgumentError.new("Argument Error in #{self.class}. :attribute is null.") if pds_handle.nil?;
|
21
|
+
pds_uri = URI.parse("#{pds_url}/pds?func=get-attribute&attribute=#{attribute}&calling_system=#{calling_system}&pds_handle=#{pds_handle}")
|
22
|
+
http = Net::HTTP.new(pds_uri.host, pds_uri.port)
|
23
|
+
# Set read timeout to 15 seconds.
|
24
|
+
http.read_timeout = 15
|
25
|
+
http.use_ssl = true if pds_uri.is_a?(URI::HTTPS)
|
26
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
|
27
|
+
response = http.post(pds_uri.path, pds_uri.query)
|
28
|
+
begin
|
29
|
+
response.value
|
30
|
+
rescue Exception => e
|
31
|
+
raise "Error in #{self.class}. Invalid HTTP response status.\n#{e.message}"
|
32
|
+
end
|
33
|
+
# PDS returns as HTML content type, unfortunately.
|
34
|
+
@response = Nokogiri.XML(response.body)
|
35
|
+
@error = @response.at("//error").inner_text unless @response.at("//error").nil?
|
36
|
+
# Don't raise an error, because user not found is reported as an error.
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Makes a call get-attribute with attribute "bor_info".
|
41
|
+
# Raises an exception if there is an unexpected response.
|
42
|
+
class BorInfo < GetAttribute
|
43
|
+
|
44
|
+
protected
|
45
|
+
def initialize(pds_url, calling_system, pds_handle, bor_info_attributes)
|
46
|
+
super(pds_url, calling_system, pds_handle, "bor_info")
|
47
|
+
raise RuntimeError.new(
|
48
|
+
"Error in #{self.class}."+
|
49
|
+
"Unrecognized response: #{@response}.") unless @response.root.name.eql?("bor-info") or @response.root.name.eql?("pds")
|
50
|
+
bor_info_attributes.each { |local_attribute, xml_attribute|
|
51
|
+
self.class.send(:attr_reader, local_attribute)
|
52
|
+
instance_variable_set("@#{local_attribute}".to_sym,
|
53
|
+
@response.at("#{xml_attribute}").inner_text) unless @response.at("//bor-info/#{xml_attribute}").nil?
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Institution < Struct.new(:display_name, :name, :default_institution,
|
2
|
+
:application_layout, :ip_addresses, :parent_institution, :view_attributes, :login_attributes)
|
3
|
+
|
4
|
+
# Better initializer than Struct gives us, take a hash instead
|
5
|
+
# of an ordered array. :services=>[] is an array of service ids,
|
6
|
+
# not actual Services!
|
7
|
+
def initialize(h={}, controller)
|
8
|
+
members.each {|m| self.send( ("#{m}=").to_sym , (h.delete(m.to_sym) || h.delete(m))) }
|
9
|
+
default_institution = false unless default_institution
|
10
|
+
# Log the fact that there are left overs in the hash
|
11
|
+
# Rails.logger.warn("The following institution settings were ignored: #{h.inspect}.") unless h.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Instantiates a new copy of all services included in this institution,
|
15
|
+
# returns an array.
|
16
|
+
def instantiate_services!
|
17
|
+
services.collect {|s| }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check the list of IP addresses for the given IP
|
21
|
+
def includes_ip?(prospective_ip_address)
|
22
|
+
return false if ip_addresses.nil?
|
23
|
+
require 'ipaddr'
|
24
|
+
ip_prospect = IPAddr.new(prospective_ip_address)
|
25
|
+
ip_addresses.each do |ip_address|
|
26
|
+
ip_range = (ip_address.match(/[\-\*]/)) ?
|
27
|
+
(ip_address.match(/\-/)) ?
|
28
|
+
(IPAddr.new(ip_address.split("-")[0])..IPAddr.new(ip_address.split("-")[1])) :
|
29
|
+
(ip_address.gsub(/\*/, "0")..ip_address.gsub(/\*/, "255")) :
|
30
|
+
IPAddr.new(ip_address).to_range
|
31
|
+
return true if ip_range === ip_prospect unless ip_range.nil?
|
32
|
+
end
|
33
|
+
return false;
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
h = {}
|
38
|
+
members.each {|m| h[m.to_sym] = self.send(m)}
|
39
|
+
h
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class InstitutionList
|
2
|
+
include Singleton # get the instance with InstitutionList.instance
|
3
|
+
@@institutions_yaml_path = nil
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@institutions = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
# Used for initialization and testing
|
10
|
+
def self.yaml_path=(path)
|
11
|
+
@@institutions_yaml_path = path
|
12
|
+
self.instance.reload
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.institutions_defined?
|
16
|
+
return !@@institutions_yaml_path.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an Institution
|
20
|
+
def get(name)
|
21
|
+
return institutions[name]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns an array of Institutions
|
25
|
+
def default_institutions
|
26
|
+
return institutions.values.find_all {|institution| institution.default_institution == true}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an array of Institutions
|
30
|
+
def institutions_with_ip(ip)
|
31
|
+
return institutions.values.find_all { |institution| institution.includes_ip?(ip) }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reload institutions from the YAML file.
|
35
|
+
def reload
|
36
|
+
@institutions = nil
|
37
|
+
institutions
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Load institutions from the YAML file and return as a hash.
|
42
|
+
def institutions
|
43
|
+
unless @institutions
|
44
|
+
raise ArgumentError.new("institutions_yaml_path was not specified.") if @@institutions_yaml_path.nil?
|
45
|
+
raise NameError.new(
|
46
|
+
"The file #{@@institutions_yaml_path} does not exist. "+
|
47
|
+
"In order to use the institution feature you must create the file."
|
48
|
+
) unless File.exists?(@@institutions_yaml_path)
|
49
|
+
institution_list = YAML.load_file( @@institutions_yaml_path )
|
50
|
+
@institutions = {}
|
51
|
+
# Turn the institution hashes to Institutions
|
52
|
+
institution_list.each_pair do |institution_name, institution_hash|
|
53
|
+
institution_hash["name"] = institution_name
|
54
|
+
# Merge with parent institution
|
55
|
+
institution_hash =
|
56
|
+
institution_list[institution_hash["parent_institution"]].
|
57
|
+
merge(institution_hash) unless institution_hash["parent_institution"].nil?
|
58
|
+
@institutions[institution_name] = Institution.new(institution_hash)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
return @institutions
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
module Authpds
|
2
|
+
# == Overview
|
3
|
+
# The Auth module mixes in callbacks to Authlogic::Session::Base for persisting,
|
4
|
+
# validating and managing the destruction of sessions. The module also provides
|
5
|
+
# instance methods used by the SessionController for managing UserSessions before
|
6
|
+
# login and redirecting to login and logout urls.
|
7
|
+
# The methods in this module are intended to be overridden for custom authentication/authorization
|
8
|
+
# needs. The documentation below describes the methods available for overriding, convenience methods
|
9
|
+
# available for use by custom implementations, instructions for mixing in custom implementations and
|
10
|
+
# further details about the module.
|
11
|
+
#
|
12
|
+
# == Methods Available for Overriding
|
13
|
+
# :on_every_request:: Used for creating a UserSession without the User having to explicitly login, thereby supporting single sign-on.
|
14
|
+
# When overridden, implementations should update the UserSession User, via UserSession#get_user based
|
15
|
+
# on custom authentication/authorization criteria. Authlogic will take care of the rest by saving the User
|
16
|
+
# and creating the UserSession.
|
17
|
+
# :before_login:: Allows for custom logic immediately before a login is initiated. If a controller :redirect_to or :render
|
18
|
+
# is performed, the directive will supercede :login_url. Precedes :login_url.
|
19
|
+
# :login_url:: Should return a custom login URL for redirection to when logging in via a remote system.
|
20
|
+
# If undefined, /login will go to the UserSession login view,
|
21
|
+
# default user_session/new). Preceded by :before_login.
|
22
|
+
# :after_login:: Used for creating a UserSession after login credentials are provided. When overridden,
|
23
|
+
# custom implementations should update the UserSession User, via UserSession#get_user based
|
24
|
+
# on authentication/authorization criteria. Authlogic will take care of the rest
|
25
|
+
# by saving the User and creating the UserSession.
|
26
|
+
# :before_logout:: Allows for custom logic immediately before logout is performed
|
27
|
+
# :after_logout:: Allows for custom logic immediately after logout is performed
|
28
|
+
# :redirect_logout_url:: Should return a custom logout URL for redirection to after logout has been performed.
|
29
|
+
# Allows for single sign-out via a remote system.
|
30
|
+
#
|
31
|
+
# == Convenience Methods for Use by Custom Implementations
|
32
|
+
# UserSession#controller:: Returns the current controller. Used for accessing cookies and session information,
|
33
|
+
# performing redirects, etc.
|
34
|
+
# UserSession#get_user:: Returns the User for updating by :on_every_request and :after_login. Returns an existing User
|
35
|
+
# if she exists, otherwise creates a new User.
|
36
|
+
# UserSession#validate_url:: Returns the URL for validating a UserSession on return from a remote login system.
|
37
|
+
# User#expiration_period=:: Sets the expiration date for the User. Default is one week ago.
|
38
|
+
# User#refreshed_at=:: Sets the last time the User was refreshed and saves the value to the database.
|
39
|
+
# User#expired?:: Returns a boolean based on whether the User has been refreshed recently.
|
40
|
+
# If User#refreshed_at is older than User#expiration_date, the User is expired and the data
|
41
|
+
# may need to be refreshed.
|
42
|
+
# User#user_attributes=:: "Smart" updating of user_attributes. Maintains user_attributes that are not explicity overwritten.
|
43
|
+
#
|
44
|
+
# == Mixing in Custom Implementations
|
45
|
+
# Once you've built your class, you can mix it in to Authlogic with the following config setting in config/environment.rb
|
46
|
+
# config.app_config.login = {
|
47
|
+
# :module => :PDS,
|
48
|
+
# :cookie_name => "user_credentials_is_the_default"
|
49
|
+
# :remember_me => true|false
|
50
|
+
# :remember_me_for => seconds, e.g. 5.minutes }
|
51
|
+
#
|
52
|
+
# == Further Implementation Details
|
53
|
+
# === Persisting a UserSession in AuthLogic
|
54
|
+
# When persisting a UserSession, Authlogic attempts to create the UserSession based on information available
|
55
|
+
# without having to perform an actual login by calling the :persisting? method. Authologic provides several callbacks from the :persisting?
|
56
|
+
# method, e.g. :before_persisting, :persist, :after_persisting. We're using the :persist callback and setting it to :on_every_request.
|
57
|
+
#
|
58
|
+
# === Validating a UserSession in AuthLogic
|
59
|
+
# When validating a UserSession, Authlogic attempts to create the UserSession based on information available
|
60
|
+
# from login by calling the :valid? method. Authologic provides several callbacks from the :valid?
|
61
|
+
# method, e.g. :before_validation, :validate, :after_validation. We're using the :validate callback and setting it to :after_login.
|
62
|
+
#
|
63
|
+
# === Access to the controller in UserSession
|
64
|
+
# The class that UserSession extends, Authologic::Session::Base, has an explicit handle to the current controller via the instance method
|
65
|
+
# :controller. This gives our custom instance methods the access to cookies, session information, loggers, etc. and also allows them to
|
66
|
+
# perform redirects and renders.
|
67
|
+
#
|
68
|
+
# === :before_login vs. :login_url
|
69
|
+
# :before_login allows for customized processing before the UserSessionController invokes a redirect or render to a /login page. It is
|
70
|
+
# is fully generic and can be used for any custom purposes. :login_url is specific for the case of logging in from a remote sytem. The
|
71
|
+
# two methods can be used in conjuction, but any redirects or renders performed in :before_login, will supercede a redirect to :login_url.
|
72
|
+
#
|
73
|
+
# === UserSession#get_user vs. UserSession#attempted_record
|
74
|
+
# Both UserSession#get_user and UserSession#attempted_record provide access to the instance variable @attempted_record, but
|
75
|
+
# UserSession#get_user set the instance variable to either an existing User (based on the username parameter), or creates a new User
|
76
|
+
# for use by implementing systems. If custom implementations want to interact directly with UserSession#attempted_record and
|
77
|
+
# @attempted_record, they are welcome to do so.
|
78
|
+
module Session
|
79
|
+
def self.included(klass)
|
80
|
+
klass.class_eval do
|
81
|
+
extend Config
|
82
|
+
include AuthpdsCallbackMethods
|
83
|
+
include InstanceMethods
|
84
|
+
include AuthlogicCallbackMethods
|
85
|
+
persist :persist_session
|
86
|
+
validate :after_login
|
87
|
+
before_destroy :before_logout
|
88
|
+
after_destroy :after_logout
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module Config
|
93
|
+
# Base pds url
|
94
|
+
def pds_url(value = nil)
|
95
|
+
rw_config(:pds_url, value)
|
96
|
+
end
|
97
|
+
alias_method :pds_url=, :pds_url
|
98
|
+
|
99
|
+
# Name of the system
|
100
|
+
def calling_system(value = nil)
|
101
|
+
rw_config(:calling_system, value, "authpds")
|
102
|
+
end
|
103
|
+
alias_method :calling_system=, :calling_system
|
104
|
+
|
105
|
+
# Does the system allow anonymous access?
|
106
|
+
def anonymous(value = nil)
|
107
|
+
rw_config(:anonymous, value, true)
|
108
|
+
end
|
109
|
+
alias_method :anonymous=, :anonymous
|
110
|
+
|
111
|
+
# Mapping of PDS attributes
|
112
|
+
def pds_attributes(value = nil)
|
113
|
+
rw_config(:pds_attributes, value, {:id => "id", :email => "email", :firstname => "name", :lastname => "name" })
|
114
|
+
end
|
115
|
+
alias_method :pds_attributes=, :pds_attributes
|
116
|
+
|
117
|
+
# Custom redirect logout url
|
118
|
+
def redirect_logout_url(value = nil)
|
119
|
+
rw_config(:redirect_logout_url, value, "")
|
120
|
+
end
|
121
|
+
alias_method :redirect_logout_url=, :redirect_logout_url
|
122
|
+
|
123
|
+
# Custom url to redirect to in case of system outage
|
124
|
+
def login_inaccessible_url(value = nil)
|
125
|
+
rw_config(:login_inaccessible_url, value, "")
|
126
|
+
end
|
127
|
+
alias_method :redirect_logout_url=, :redirect_logout_url
|
128
|
+
|
129
|
+
# PDS user method to call to identify record
|
130
|
+
def pds_record_identifier(value = nil)
|
131
|
+
rw_config(:pds_record_identifier, value, :id)
|
132
|
+
end
|
133
|
+
alias_method :pds_record_identifier=, :pds_record_identifier
|
134
|
+
end
|
135
|
+
|
136
|
+
module AuthpdsCallbackMethods
|
137
|
+
# Hook for more complicated logic to determine PDS user record identifier
|
138
|
+
def pds_record_identifier
|
139
|
+
self.class.pds_record_identifier
|
140
|
+
end
|
141
|
+
|
142
|
+
# Hook to determine if we should set up an SSO session
|
143
|
+
def valid_sso_session?
|
144
|
+
return false
|
145
|
+
end
|
146
|
+
|
147
|
+
# Hook to provide additional authorization requirements
|
148
|
+
def additional_authorization
|
149
|
+
return true
|
150
|
+
end
|
151
|
+
|
152
|
+
# Hook to add additional user attributes.
|
153
|
+
def additional_attributes
|
154
|
+
{}
|
155
|
+
end
|
156
|
+
|
157
|
+
# Hook to update expiration date if necessary
|
158
|
+
def expiration_date
|
159
|
+
1.week.ago
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
module InstanceMethods
|
164
|
+
require "cgi"
|
165
|
+
|
166
|
+
def self.included(klass)
|
167
|
+
klass.class_eval do
|
168
|
+
cookie_key "#{calling_system}_credentials"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Called by the user session controller login is initiated.
|
173
|
+
# Precedes :login_url
|
174
|
+
def before_login(params={})
|
175
|
+
end
|
176
|
+
|
177
|
+
# URL to redirect to for login.
|
178
|
+
# Preceded by :before_login
|
179
|
+
def login_url(params={})
|
180
|
+
return "#{self.class.pds_url}/pds?func=load-login&institute=#{institution_attributes["link_code"]}&calling_system=#{self.class.calling_system}&url=#{CGI::escape(validate_url(params))}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# URL to redirect to after logout.
|
184
|
+
def logout_url(params={})
|
185
|
+
return "#{self.class.pds_url}/pds?func=logout&url=#{CGI::escape(CGI::escape(self.class.redirect_logout_url))}"
|
186
|
+
end
|
187
|
+
|
188
|
+
# URL to redirect to in the case of establishing a SSO session.
|
189
|
+
def sso_url(params=nil)
|
190
|
+
return "#{self.class.pds_url}pds?func=sso&institute=#{institution_attributes["link_code"]}&calling_system=#{self.class.calling_system}&url=#{CGI::escape(validate_url(params))}"
|
191
|
+
end
|
192
|
+
|
193
|
+
def pds_user
|
194
|
+
begin
|
195
|
+
@pds_user ||= Authpds::Exlibris::Pds::BorInfo.new(self.class.pds_url, self.class.calling_system, pds_handle, pds_attributes) unless pds_handle.nil?
|
196
|
+
return @pds_user unless @pds_user.nil? or @pds_user.error
|
197
|
+
rescue Exception => e
|
198
|
+
# Delete the PDS_HANDLE, since this isn't working.
|
199
|
+
# controller.cookies.delete(:PDS_HANDLE) unless pds_handle.nil?
|
200
|
+
handle_login_exception e
|
201
|
+
return nil
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
def authenticated?
|
207
|
+
authenticate
|
208
|
+
end
|
209
|
+
|
210
|
+
def authenticate
|
211
|
+
return false if controller.cookies["#{self.class.calling_system}_inaccessible".to_sym] == session_id
|
212
|
+
# If PDS session already established, authenticate
|
213
|
+
return true unless pds_user.nil?
|
214
|
+
# Establish a PDS session if the user logged in via an alternative SSO mechanism and this isn't being called after login
|
215
|
+
controller.redirect_to sso_url({
|
216
|
+
:return_url => controller.request.url }) if valid_sso_user? unless controller.params["action"] =="validate" or controller.performed?
|
217
|
+
# Otherwise, do not authenticate
|
218
|
+
return false
|
219
|
+
end
|
220
|
+
|
221
|
+
def authorized?
|
222
|
+
# Set all the information that is needed to make an authorization decision
|
223
|
+
set_record and return authorize
|
224
|
+
end
|
225
|
+
|
226
|
+
def authorize
|
227
|
+
# If PDS user is not nil (PDS session already established), authorize
|
228
|
+
!pds_user.nil? && additional_authorization
|
229
|
+
end
|
230
|
+
|
231
|
+
# Get the record associated with this PDS user.
|
232
|
+
def get_record(username)
|
233
|
+
raise ArgumentError.new("Argument Error in #{self.class}. :pds_record_identifier given.") if pds_record_identifier.nil?
|
234
|
+
record = klass.send(:find_by_username, username)
|
235
|
+
record = klass.new :username => username if record.nil?
|
236
|
+
return record
|
237
|
+
end
|
238
|
+
|
239
|
+
# Set the record information associated with this PDS user.
|
240
|
+
def set_record
|
241
|
+
self.attempted_record = get_record(pds_user.send(pds_record_identifier))
|
242
|
+
self.attempted_record.expiration_date = expiration_date
|
243
|
+
# Do this part only if user data has expired.
|
244
|
+
if self.attempted_record.expired?
|
245
|
+
pds_attributes.each { |user_attr, pds_attr|
|
246
|
+
self.attempted_record.send("#{user_attr}=".to_sym, pds_user.send(pds_attr.to_sym)) if user.respond_to?("#{user_attr}=".to_sym) }
|
247
|
+
# Set default pds user attributes
|
248
|
+
pds_attributes.each_key { |user_attr|
|
249
|
+
self.attempted_record.user_attributes = {
|
250
|
+
user_attr.to_sym => pds_user.send(user_attr.to_sym) }}
|
251
|
+
end
|
252
|
+
self.attempted_record.user_attributes= additional_attributes
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns the URL for validating a UserSession on return from a remote login system.
|
256
|
+
def validate_url(params={})
|
257
|
+
url = controller.url_for(:controller => '/', :action => :validate, :return_url => controller.user_session_redirect_url(params[:return_url]))
|
258
|
+
return url if params.nil? or params.empty?
|
259
|
+
url << "?" if url.match('\?').nil?
|
260
|
+
params.each do |key, value|
|
261
|
+
next if [:controller, :action, :return_url].include?(key)
|
262
|
+
url << "&#{self.class.calling_system}_#{key}=#{value}"
|
263
|
+
end
|
264
|
+
return url
|
265
|
+
end
|
266
|
+
|
267
|
+
def institution_attributes
|
268
|
+
@institution_attributes =
|
269
|
+
(controller.current_primary_institution.nil? or controller.current_primary_institution.login_attributes.nil?) ?
|
270
|
+
{} : controller.current_primary_institution.login_attributes
|
271
|
+
end
|
272
|
+
|
273
|
+
def pds_attributes
|
274
|
+
@pds_attributes ||= self.class.pds_attributes
|
275
|
+
end
|
276
|
+
|
277
|
+
def session_id
|
278
|
+
@session_id ||=
|
279
|
+
(controller.session.respond_to?(:session_id)) ?
|
280
|
+
(controller.session.session_id) ?
|
281
|
+
controller.session.session_id : controller.session[:session_id] : controller.session[:session_id]
|
282
|
+
end
|
283
|
+
|
284
|
+
def anonymous?
|
285
|
+
self.class.anonymous
|
286
|
+
end
|
287
|
+
|
288
|
+
def pds_handle
|
289
|
+
return controller.cookies[:PDS_HANDLE] || controller.params[:pds_handle]
|
290
|
+
end
|
291
|
+
|
292
|
+
def handle_login_exception(error)
|
293
|
+
# Set a cookie saying that we've got some invalid stuff going on
|
294
|
+
# in this session. Either PDS is screwy, OpenSSO is screwy, or both.
|
295
|
+
# Either way, we want to skip logging in since it's problematic (if anonymous).
|
296
|
+
controller.cookies["#{self.class.calling_system}_inaccessible".to_sym] = {
|
297
|
+
:value => session_id,
|
298
|
+
:path => "/" } if anonymous?
|
299
|
+
# If anonymous access isn't allowed, we can't rightfully set the cookie.
|
300
|
+
# We probably should send to a system down page.
|
301
|
+
controller.redirect_to(self.class.login_inaccessible_url)
|
302
|
+
alert_the_authorities error
|
303
|
+
end
|
304
|
+
|
305
|
+
def alert_the_authorities(error)
|
306
|
+
controller.logger.error("Error in #{self.class}. Something is amiss with PDS authentication. #{error.message}")
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
module AuthlogicCallbackMethods
|
311
|
+
private
|
312
|
+
# Callback method from Authlogic.
|
313
|
+
# Called while trying to persist the session.
|
314
|
+
def persist_session
|
315
|
+
destroy unless (authenticated? and authorized?) or anonymous?
|
316
|
+
end
|
317
|
+
|
318
|
+
# Callback method from Authlogic.
|
319
|
+
# Called while validating on session save.
|
320
|
+
def after_login
|
321
|
+
authenticated? and authorized?
|
322
|
+
end
|
323
|
+
|
324
|
+
# Callback method from Authlogic.
|
325
|
+
# Called before destroying UserSession.
|
326
|
+
def before_logout
|
327
|
+
end
|
328
|
+
|
329
|
+
# Callback method from Authlogic.
|
330
|
+
# Called after destroying UserSession.
|
331
|
+
def after_logout
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|