authpds 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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
@@ -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
@@ -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