rubycas-client 2.2.1 → 2.3.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +30 -0
  3. data/History.txt +21 -0
  4. data/README.rdoc +24 -20
  5. data/Rakefile +43 -54
  6. data/VERSION +1 -0
  7. data/examples/rails/README +16 -0
  8. data/examples/rails/app/controllers/advanced_example_controller.rb +31 -0
  9. data/examples/rails/app/controllers/application.rb +2 -0
  10. data/examples/rails/app/controllers/simple_example_controller.rb +16 -0
  11. data/examples/rails/app/views/advanced_example/index.html.erb +13 -0
  12. data/examples/rails/app/views/advanced_example/my_account.html.erb +11 -0
  13. data/examples/rails/app/views/simple_example/index.html.erb +6 -0
  14. data/examples/rails/config/boot.rb +109 -0
  15. data/examples/rails/config/environment.rb +39 -0
  16. data/examples/rails/config/environments/development.rb +17 -0
  17. data/examples/rails/config/environments/production.rb +22 -0
  18. data/examples/rails/config/environments/test.rb +22 -0
  19. data/examples/rails/config/initializers/inflections.rb +10 -0
  20. data/examples/rails/config/initializers/mime_types.rb +5 -0
  21. data/examples/rails/config/initializers/new_rails_defaults.rb +17 -0
  22. data/examples/rails/config/routes.rb +4 -0
  23. data/examples/rails/log/development.log +946 -0
  24. data/examples/rails/log/production.log +0 -0
  25. data/examples/rails/log/server.log +0 -0
  26. data/examples/rails/log/test.log +0 -0
  27. data/examples/rails/script/about +4 -0
  28. data/examples/rails/script/console +3 -0
  29. data/examples/rails/script/server +3 -0
  30. data/lib/casclient/client.rb +49 -36
  31. data/lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb +5 -39
  32. data/lib/casclient/frameworks/rails/filter.rb +86 -113
  33. data/lib/casclient/responses.rb +29 -16
  34. data/lib/casclient/tickets/storage/active_record_ticket_store.rb +67 -0
  35. data/lib/casclient/tickets/storage.rb +167 -0
  36. data/lib/casclient/tickets.rb +3 -3
  37. data/lib/casclient.rb +3 -2
  38. data/lib/rubycas-client.rb +1 -5
  39. data/rails_generators/active_record_ticket_store/USAGE +9 -0
  40. data/rails_generators/active_record_ticket_store/active_record_ticket_store_generator.rb +29 -0
  41. data/rails_generators/active_record_ticket_store/templates/README +1 -0
  42. data/rails_generators/active_record_ticket_store/templates/migration.rb +24 -0
  43. data/rubycas-client.gemspec +103 -0
  44. data/test/teststrap.rb +10 -0
  45. data/test/units/casclient/frameworks/rails/filter_test.rb +184 -0
  46. metadata +148 -47
  47. data/Manifest.txt +0 -23
  48. data/examples/merb/README.textile +0 -12
  49. data/examples/merb/Rakefile +0 -35
  50. data/examples/merb/merb.thor +0 -2020
  51. data/examples/merb/merb_auth_cas.rb +0 -67
  52. data/examples/merb/spec/spec_helper.rb +0 -24
  53. data/init.rb +0 -6
  54. data/lib/casclient/frameworks/merb/filter.rb +0 -105
  55. data/lib/casclient/frameworks/merb/strategy.rb +0 -110
  56. data/lib/casclient/version.rb +0 -9
  57. data/setup.rb +0 -1585
@@ -31,15 +31,14 @@ module CASClient
31
31
 
32
32
  attr_reader :protocol, :user, :pgt_iou, :proxies, :extra_attributes
33
33
 
34
- def initialize(raw_text)
35
- parse(raw_text)
34
+ def initialize(raw_text, options={})
35
+ parse(raw_text, options)
36
36
  end
37
37
 
38
- def parse(raw_text)
38
+ def parse(raw_text, options)
39
39
  raise BadResponseException,
40
40
  "CAS response is empty/blank." if raw_text.blank?
41
41
  @parse_datetime = Time.now
42
-
43
42
  if raw_text =~ /^(yes|no)\n(.*?)\n$/m
44
43
  @protocol = 1.0
45
44
  @valid = $~[1] == 'yes'
@@ -53,7 +52,8 @@ module CASClient
53
52
  @protocol = 2.0
54
53
 
55
54
  if is_success?
56
- @user = @xml.elements["cas:user"].text.strip if @xml.elements["cas:user"]
55
+ cas_user = @xml.elements["cas:user"]
56
+ @user = cas_user.text.strip if cas_user
57
57
  @pgt_iou = @xml.elements["cas:proxyGrantingTicket"].text.strip if @xml.elements["cas:proxyGrantingTicket"]
58
58
 
59
59
  proxy_els = @xml.elements.to_a('//cas:authenticationSuccess/cas:proxies/cas:proxy')
@@ -65,16 +65,32 @@ module CASClient
65
65
  end
66
66
 
67
67
  @extra_attributes = {}
68
- @xml.elements.to_a('//cas:authenticationSuccess/*').each do |el|
69
- @extra_attributes.merge!(Hash.from_xml(el.to_s)) unless el.prefix == 'cas'
68
+ @xml.elements.to_a('//cas:authenticationSuccess/cas:attributes/* | //cas:authenticationSuccess/*[local-name() != \'proxies\' and local-name() != \'proxyGrantingTicket\' and local-name() != \'user\' and local-name() != \'attributes\']').each do |el|
69
+ # generating the hash requires prefixes to be defined, so add all of the namespaces
70
+ el.namespaces.each {|k,v| el.add_namespace(k,v)}
71
+ @extra_attributes.merge!(Hash.from_xml(el.to_s))
70
72
  end
71
73
 
72
74
  # unserialize extra attributes
73
75
  @extra_attributes.each do |k, v|
74
76
  if v.blank?
75
77
  @extra_attributes[k] = nil
76
- else
77
- @extra_attributes[k] = YAML.load(v)
78
+ elsif !options[:encode_extra_attributes_as]
79
+ begin
80
+ @extra_attributes[k] = YAML.load(v)
81
+ rescue ArgumentError
82
+ raise ArgumentError, "Did not find :encode_extra_attributes_as config parameter, hence default encoding scheme is YAML but CAS response recieved in encoded differently "
83
+ end
84
+ else
85
+ if options[:encode_extra_attributes_as] == :json
86
+ begin
87
+ @extra_attributes[k] = JSON.parse(v)
88
+ rescue JSON::ParserError
89
+ @extra_attributes[k] = YAML.load(v)
90
+ end
91
+ else
92
+ @extra_attributes[k] = YAML.load(v)
93
+ end
78
94
  end
79
95
  end
80
96
  elsif is_failure?
@@ -84,9 +100,8 @@ module CASClient
84
100
  # this should never happen, since the response should already have been recognized as invalid
85
101
  raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
86
102
  end
87
-
88
103
  end
89
-
104
+
90
105
  def is_success?
91
106
  (instance_variable_defined?(:@valid) && @valid) || (protocol > 1.0 && xml.name == "authenticationSuccess")
92
107
  end
@@ -103,7 +118,7 @@ module CASClient
103
118
 
104
119
  attr_reader :proxy_ticket
105
120
 
106
- def initialize(raw_text)
121
+ def initialize(raw_text, options={})
107
122
  parse(raw_text)
108
123
  end
109
124
 
@@ -141,7 +156,7 @@ module CASClient
141
156
  attr_reader :tgt, :ticket, :service_redirect_url
142
157
  attr_reader :failure_message
143
158
 
144
- def initialize(http_response = nil)
159
+ def initialize(http_response = nil, options={})
145
160
  parse_http_response(http_response) if http_response
146
161
  end
147
162
 
@@ -160,9 +175,7 @@ module CASClient
160
175
  @ticket = $~[1]
161
176
  end
162
177
 
163
- if (http_response.kind_of?(Net::HTTPSuccess) || http_response.kind_of?(Net::HTTPFound)) && @ticket.present?
164
- log.info("Login was successful for ticket: #{@ticket.inspect}.")
165
- else
178
+ if not ((http_response.kind_of?(Net::HTTPSuccess) || http_response.kind_of?(Net::HTTPFound)) && @ticket.present?)
166
179
  @failure = true
167
180
  # Try to extract the error message -- this only works with RubyCAS-Server.
168
181
  # For other servers we just return the entire response body (i.e. the whole error page).
@@ -0,0 +1,67 @@
1
+ module CASClient
2
+ module Tickets
3
+ module Storage
4
+
5
+ # A Ticket Store that keeps it's ticket in database tables using ActiveRecord.
6
+ #
7
+ # Services Tickets are stored in an extra column add to the ActiveRecord sessions table.
8
+ # Proxy Granting Tickets and their IOUs are stored in the cas_pgtious table.
9
+ #
10
+ # This ticket store takes the following config parameters
11
+ # :pgtious_table_name - the name of the table
12
+ class ActiveRecordTicketStore < AbstractTicketStore
13
+
14
+ def initialize(config={})
15
+ config ||= {}
16
+ if config[:pgtious_table_name]
17
+ CasPgtiou.set_table_name = config[:pgtious_table_name]
18
+ end
19
+ end
20
+
21
+ def store_service_session_lookup(st, controller)
22
+ #get the session from the rack env using ActiveRecord::SessionStore::SESSION_RECORD_KEY = 'rack.session.record'
23
+
24
+ st = st.ticket if st.kind_of? ServiceTicket
25
+ session = controller.request.env[ActiveRecord::SessionStore::SESSION_RECORD_KEY]
26
+ session.service_ticket = st
27
+ end
28
+
29
+ def get_session_for_service_ticket(st)
30
+ st = st.ticket if st.kind_of? ServiceTicket
31
+ session = ActiveRecord::SessionStore::Session.find_by_service_ticket(st)
32
+ session_id = session ? session.session_id : nil
33
+ [session_id, session]
34
+ end
35
+
36
+ def cleanup_service_session_lookup(st)
37
+ #no cleanup needed for this ticket store
38
+ end
39
+
40
+ def save_pgt_iou(pgt_iou, pgt)
41
+ pgtiou = CasPgtiou.create(:pgt_iou => pgt_iou, :pgt_id => pgt)
42
+ end
43
+
44
+ def retrieve_pgt(pgt_iou)
45
+ raise CASException, "No pgt_iou specified. Cannot retrieve the pgt." unless pgt_iou
46
+
47
+ pgtiou = CasPgtiou.find_by_pgt_iou(pgt_iou)
48
+ pgt = pgtiou.pgt_id
49
+
50
+ raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgt
51
+
52
+ pgtiou.destroy
53
+
54
+ pgt
55
+
56
+ end
57
+
58
+ end
59
+
60
+ class CasPgtiou < ActiveRecord::Base
61
+ #t.string :pgt_iou, :null => false
62
+ #t.string :pgt_id, :null => false
63
+ #t.timestamps
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,167 @@
1
+ module CASClient
2
+ module Tickets
3
+ module Storage
4
+ class AbstractTicketStore
5
+
6
+ attr_accessor :log
7
+ @log = CASClient::LoggerWrapper.new
8
+
9
+ def process_single_sign_out(si)
10
+
11
+ session_id, session = get_session_for_service_ticket(si)
12
+ if session
13
+ session.destroy
14
+ log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{si.inspect}.")
15
+ else
16
+ log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
17
+ end
18
+
19
+ if session_id
20
+ log.info("Single-sign-out for service ticket #{session_id.inspect} completed successfuly.")
21
+ else
22
+ log.debug("No session id found for CAS ticket #{si}")
23
+ end
24
+ end
25
+
26
+ def get_session_for_service_ticket(st)
27
+ session_id = read_service_session_lookup(si)
28
+ if session_id
29
+ session = ActiveRecord::SessionStore::Session.find_by_session_id(session_id)
30
+ else
31
+ log.warn("Couldn't destroy session with SessionIndex #{si} because no corresponding session id could be looked up.")
32
+ end
33
+ [session_id, session]
34
+ end
35
+
36
+ def store_service_session_lookup(st, controller)
37
+ raise 'Implement this in a subclass!'
38
+ end
39
+
40
+ def cleanup_service_session_lookup(st)
41
+ raise 'Implement this in a subclass!'
42
+ end
43
+
44
+ def save_pgt_iou(pgt_iou, pgt)
45
+ raise 'Implement this in a subclass!'
46
+ end
47
+
48
+ def retrieve_pgt(pgt_iou)
49
+ raise 'Implement this in a subclass!'
50
+ end
51
+
52
+ protected
53
+ def read_service_session_lookup(st)
54
+ raise 'Implement this in a subclass!'
55
+ end
56
+ end
57
+
58
+ # A Ticket Store that keeps it's tickets in a directory on the local filesystem.
59
+ # Service tickets are stored under tmp/sessions by default
60
+ # and Proxy Granting Tickets and their IOUs are stored in tmp/cas_pgt.pstore
61
+ # This Ticket Store works fine for small sites but will most likely have
62
+ # concurrency problems under heavy load. It also requires that all your
63
+ # worker processes have access to a shared file system.
64
+ #
65
+ # This ticket store takes the following config parameters
66
+ # :storage_dir - The directory to store data in. Defaults to RAILS_ROOT/tmp
67
+ # :service_session_lookup_dir - The directory to store Service Ticket/Session ID files in. Defaults to :storage_dir/sessions
68
+ # :pgt_store_path - The location to store the pgt PStore file. Defaults to :storage_dir/cas_pgt.pstore
69
+ class LocalDirTicketStore < AbstractTicketStore
70
+ require 'pstore'
71
+
72
+ DEFAULT_TMP_DIR = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/tmp" : "#{Dir.pwd}/tmp"
73
+
74
+ def initialize(config={})
75
+ config ||= {}
76
+ @tmp_dir = config[:storage_dir] || DEFAULT_TMP_DIR
77
+ @service_session_lookup_dir = config[:service_session_lookup_dir] || "#{@tmp_dir}/sessions"
78
+ @pgt_store_path = config[:pgt_store_path] || "#{@tmp_dir}/cas_pgt.pstore"
79
+ end
80
+
81
+ # Creates a file in tmp/sessions linking a SessionTicket
82
+ # with the local Rails session id. The file is named
83
+ # cas_sess.<session ticket> and its text contents is the corresponding
84
+ # Rails session id.
85
+ # Returns the filename of the lookup file created.
86
+ def store_service_session_lookup(st, controller)
87
+ raise CASException, "No service_ticket specified." unless st
88
+ raise CASException, "No controller specified." unless controller
89
+
90
+ sid = controller.request.session_options[:id] || controller.session.session_id
91
+
92
+ st = st.ticket if st.kind_of? ServiceTicket
93
+ f = File.new(filename_of_service_session_lookup(st), 'w')
94
+ f.write(sid)
95
+ f.close
96
+ return f.path
97
+ end
98
+
99
+ # Returns the local Rails session ID corresponding to the given
100
+ # ServiceTicket. This is done by reading the contents of the
101
+ # cas_sess.<session ticket> file created in a prior call to
102
+ # #store_service_session_lookup.
103
+ def read_service_session_lookup(st)
104
+ raise CASException, "No service_ticket specified." unless st
105
+
106
+ st = st.ticket if st.kind_of? ServiceTicket
107
+ ssl_filename = filename_of_service_session_lookup(st)
108
+ return File.exists?(ssl_filename) && IO.read(ssl_filename)
109
+ end
110
+
111
+ # Removes a stored relationship between a ServiceTicket and a local
112
+ # Rails session id. This should be called when the session is being
113
+ # closed.
114
+ #
115
+ # See #store_service_session_lookup.
116
+ def cleanup_service_session_lookup(st)
117
+ raise CASException, "No service_ticket specified." unless st
118
+
119
+ st = st.ticket if st.kind_of? ServiceTicket
120
+ ssl_filename = filename_of_service_session_lookup(st)
121
+ File.delete(ssl_filename) if File.exists?(ssl_filename)
122
+ end
123
+
124
+ def save_pgt_iou(pgt_iou, pgt)
125
+ # TODO: pstore contents should probably be encrypted...
126
+ pstore = open_pstore
127
+
128
+ pstore.transaction do
129
+ pstore[pgt_iou] = pgt
130
+ end
131
+ end
132
+
133
+ def retrieve_pgt(pgt_iou)
134
+ raise CASException, "No pgt_iou specified. Cannot retrieve the pgt." unless pgt_iou
135
+
136
+ pstore = open_pstore
137
+
138
+ pgt = nil
139
+ pstore.transaction do
140
+ pgt = pstore[pgt_iou]
141
+ end
142
+
143
+ raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgt
144
+
145
+ # TODO: need to periodically clean the storage, otherwise it will just keep growing
146
+ pstore.transaction do
147
+ pstore.delete pgt_iou
148
+ end
149
+
150
+ pgt
151
+ end
152
+
153
+ private
154
+
155
+ # Returns the path and filename of the service session lookup file.
156
+ def filename_of_service_session_lookup(st)
157
+ st = st.ticket if st.kind_of? ServiceTicket
158
+ return "#{@service_session_lookup_dir}/cas_sess.#{st}"
159
+ end
160
+
161
+ def open_pstore
162
+ PStore.new(@pgt_store_path)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -2,7 +2,7 @@ module CASClient
2
2
  # Represents a CAS service ticket.
3
3
  class ServiceTicket
4
4
  attr_reader :ticket, :service, :renew
5
- attr_accessor :response
5
+ attr_accessor :user, :extra_attributes, :pgt_iou, :success, :failure_code, :failure_message
6
6
 
7
7
  def initialize(ticket, service, renew = false)
8
8
  @ticket = ticket
@@ -11,11 +11,11 @@ module CASClient
11
11
  end
12
12
 
13
13
  def is_valid?
14
- response.is_success?
14
+ success
15
15
  end
16
16
 
17
17
  def has_been_validated?
18
- not response.nil?
18
+ not user.nil?
19
19
  end
20
20
  end
21
21
 
data/lib/casclient.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'uri'
2
2
  require 'cgi'
3
+ require 'logger'
3
4
  require 'net/https'
4
5
  require 'rexml/document'
5
6
 
@@ -65,7 +66,7 @@ end
65
66
  require 'casclient/tickets'
66
67
  require 'casclient/responses'
67
68
  require 'casclient/client'
68
- require 'casclient/version'
69
+ require 'casclient/tickets/storage'
69
70
 
70
71
  # Detect legacy configuration and show appropriate error message
71
72
  module CAS
@@ -86,4 +87,4 @@ module CAS
86
87
  end
87
88
  end
88
89
  end
89
- end
90
+ end
@@ -1,5 +1 @@
1
- begin
2
- require 'casclient'
3
- rescue MissingSourceFile
4
- require 'lib/casclient'
5
- end
1
+ require 'casclient'
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create a migration to add the service_ticket column to the sessions
3
+ table and create the cas_pgtious table for proxy ticket storage.
4
+ Pass the migration name as an optional parameter. The migration name
5
+ defaults to CreateActiveRecordTicketStore.
6
+
7
+ Requirements:
8
+ You need to already have created the ActiveRecord::SessionStore sessions
9
+ table.
@@ -0,0 +1,29 @@
1
+ class ActiveRecordTicketStoreGenerator < Rails::Generator::NamedBase
2
+
3
+ def initialize(runtime_args, runtime_options = {})
4
+ runtime_args << 'create_active_record_ticket_store' if runtime_args.empty?
5
+ super
6
+ end
7
+
8
+ def manifest
9
+ record do |m|
10
+ m.migration_template 'migration.rb', 'db/migrate',
11
+ :assigns => { :session_table_name => default_session_table_name, :pgtiou_table_name => default_pgtiou_table_name }
12
+ m.readme "README"
13
+ end
14
+ end
15
+
16
+ protected
17
+ def banner
18
+ "Usage: #{$0} #{spec.name} [CreateActiveRecordTicketStore] [options]"
19
+ end
20
+
21
+ def default_session_table_name
22
+ ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session'
23
+ end
24
+
25
+ def default_pgtiou_table_name
26
+ ActiveRecord::Base.pluralize_table_names ? 'cas_pgtiou'.pluralize : 'cas_pgtiou'
27
+ end
28
+
29
+ end
@@ -0,0 +1 @@
1
+ You need to make sure you have already created the sessions table for the ActiveRecord::SessionStore
@@ -0,0 +1,24 @@
1
+ class <%= class_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :<%= session_table_name %>, :service_ticket, :string
4
+
5
+ add_index :<%= session_table_name %>, :service_ticket
6
+
7
+ create_table :<%= pgtiou_table_name %> do |t|
8
+ t.string :pgt_iou, :null => false
9
+ t.string :pgt_id, :null => false
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :<%= pgtiou_table_name %>, :pgt_iou, :unique => true
14
+ end
15
+
16
+ def self.down
17
+ drop_table :<%= pgtiou_table_name %>
18
+
19
+ remove_index :<%= session_table_name %>, :service_ticket
20
+
21
+ remove_column :<%= session_table_name %>, :service_ticket
22
+ end
23
+ end
24
+