rubycas-client 2.2.1 → 2.3.0.rc1

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