rubycas-client 2.3.8 → 2.3.9.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.simplecov +7 -0
- data/.travis.yml +2 -0
- data/Gemfile +18 -5
- data/Gemfile.lock +46 -13
- data/Guardfile +11 -0
- data/History.txt +22 -0
- data/README.rdoc +2 -2
- data/Rakefile +0 -12
- data/VERSION +1 -1
- data/lib/casclient.rb +1 -1
- data/lib/casclient/client.rb +75 -70
- data/lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb +6 -2
- data/lib/casclient/frameworks/rails/filter.rb +4 -1
- data/lib/casclient/responses.rb +64 -57
- data/lib/casclient/tickets/storage.rb +29 -20
- data/lib/casclient/tickets/storage/active_record_ticket_store.rb +11 -6
- data/rubycas-client.gemspec +50 -17
- data/spec/.gitignore +1 -0
- data/spec/casclient/client_spec.rb +93 -0
- data/spec/casclient/frameworks/rails/filter_spec.rb +27 -36
- data/spec/casclient/tickets/storage/active_record_ticket_store_spec.rb +6 -0
- data/spec/casclient/tickets/storage_spec.rb +44 -0
- data/spec/casclient/validation_response_spec.rb +97 -3
- data/spec/database.yml +7 -0
- data/spec/spec_helper.rb +31 -8
- data/spec/support/action_controller_helpers.rb +30 -0
- data/spec/support/active_record_helpers.rb +48 -0
- data/spec/support/local_hash_ticket_store.rb +48 -0
- data/spec/support/local_hash_ticket_store_spec.rb +5 -0
- data/spec/support/shared_examples_for_ticket_stores.rb +137 -0
- metadata +119 -30
@@ -22,8 +22,12 @@ class CasProxyCallbackController < ActionController::Base
|
|
22
22
|
render :text => "Okay, the server is up, but please specify a pgtIou and pgtId." and return unless pgtIou and pgtId
|
23
23
|
|
24
24
|
# TODO: pstore contents should probably be encrypted...
|
25
|
-
|
26
|
-
|
25
|
+
|
26
|
+
if Rails::VERSION::MAJOR > 2
|
27
|
+
casclient = RubyCAS::Filter.client
|
28
|
+
else
|
29
|
+
casclient = CASClient::Frameworks::Rails::Filter.client
|
30
|
+
end
|
27
31
|
|
28
32
|
casclient.ticket_store.save_pgt_iou(pgtIou, pgtId)
|
29
33
|
|
@@ -218,7 +218,10 @@ module CASClient
|
|
218
218
|
end
|
219
219
|
|
220
220
|
def unauthorized!(controller, vr = nil)
|
221
|
-
format =
|
221
|
+
format = nil
|
222
|
+
unless controller.request.format.nil?
|
223
|
+
format = controller.request.format.to_sym
|
224
|
+
end
|
222
225
|
format = (format == :js ? :json : format)
|
223
226
|
case format
|
224
227
|
when :xml, :json
|
data/lib/casclient/responses.rb
CHANGED
@@ -2,39 +2,39 @@ module CASClient
|
|
2
2
|
module XmlResponse
|
3
3
|
attr_reader :xml, :parse_datetime
|
4
4
|
attr_reader :failure_code, :failure_message
|
5
|
-
|
5
|
+
|
6
6
|
def check_and_parse_xml(raw_xml)
|
7
7
|
begin
|
8
|
-
doc = REXML::Document.new(raw_xml)
|
8
|
+
doc = REXML::Document.new(raw_xml, :raw => :all)
|
9
9
|
rescue REXML::ParseException => e
|
10
10
|
raise BadResponseException,
|
11
11
|
"MALFORMED CAS RESPONSE:\n#{raw_xml.inspect}\n\nEXCEPTION:\n#{e}"
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
unless doc.elements && doc.elements["cas:serviceResponse"]
|
15
15
|
raise BadResponseException,
|
16
16
|
"This does not appear to be a valid CAS response (missing cas:serviceResponse root element)!\nXML DOC:\n#{doc.to_s}"
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
return doc.elements["cas:serviceResponse"].elements[1]
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def to_s
|
23
23
|
xml.to_s
|
24
24
|
end
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# Represents a response from the CAS server to a 'validate' request
|
28
28
|
# (i.e. after validating a service/proxy ticket).
|
29
29
|
class ValidationResponse
|
30
30
|
include XmlResponse
|
31
|
-
|
31
|
+
|
32
32
|
attr_reader :protocol, :user, :pgt_iou, :proxies, :extra_attributes
|
33
|
-
|
33
|
+
|
34
34
|
def initialize(raw_text, options={})
|
35
35
|
parse(raw_text, options)
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def parse(raw_text, options)
|
39
39
|
raise BadResponseException,
|
40
40
|
"CAS response is empty/blank." if raw_text.blank?
|
@@ -45,17 +45,17 @@ module CASClient
|
|
45
45
|
@user = $~[2]
|
46
46
|
return
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
@xml = check_and_parse_xml(raw_text)
|
50
|
-
|
50
|
+
|
51
51
|
# if we got this far then we've got a valid XML response, so we're doing CAS 2.0
|
52
52
|
@protocol = 2.0
|
53
|
-
|
53
|
+
|
54
54
|
if is_success?
|
55
55
|
cas_user = @xml.elements["cas:user"]
|
56
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')
|
60
60
|
if proxy_els.size > 0
|
61
61
|
@proxies = []
|
@@ -63,13 +63,18 @@ module CASClient
|
|
63
63
|
@proxies << el.text
|
64
64
|
end
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
@extra_attributes = {}
|
68
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
69
|
inner_text = el.cdatas.length > 0 ? el.cdatas.join('') : el.text
|
70
|
-
|
70
|
+
name = el.name
|
71
|
+
unless (attrs = el.attributes).empty?
|
72
|
+
name = attrs['name']
|
73
|
+
inner_text = attrs['value']
|
74
|
+
end
|
75
|
+
@extra_attributes.merge! name => inner_text
|
71
76
|
end
|
72
|
-
|
77
|
+
|
73
78
|
# unserialize extra attributes
|
74
79
|
@extra_attributes.each do |k, v|
|
75
80
|
@extra_attributes[k] = parse_extra_attribute_value(v, options[:encode_extra_attributes_as])
|
@@ -85,26 +90,28 @@ module CASClient
|
|
85
90
|
|
86
91
|
def parse_extra_attribute_value(value, encode_extra_attributes_as)
|
87
92
|
attr_value = if value.blank?
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
93
|
+
nil
|
94
|
+
elsif !encode_extra_attributes_as
|
95
|
+
begin
|
96
|
+
YAML.load(value)
|
97
|
+
rescue ArgumentError => e
|
98
|
+
raise ArgumentError, "Error parsing extra attribute with value #{value} as YAML: #{e}"
|
99
|
+
end
|
100
|
+
else
|
101
|
+
if encode_extra_attributes_as == :json
|
102
|
+
begin
|
103
|
+
JSON.parse(value)
|
104
|
+
rescue JSON::ParserError
|
105
|
+
value
|
106
|
+
end
|
107
|
+
elsif encode_extra_attributes_as == :raw
|
108
|
+
value
|
109
|
+
else
|
110
|
+
YAML.load(value)
|
111
|
+
end
|
112
|
+
end
|
106
113
|
|
107
|
-
unless (attr_value.kind_of?
|
114
|
+
unless attr_value.kind_of?(Enumerable) || attr_value.kind_of?(TrueClass) || attr_value.kind_of?(FalseClass) || attr_value.nil?
|
108
115
|
attr_value.to_s
|
109
116
|
else
|
110
117
|
attr_value
|
@@ -114,30 +121,30 @@ module CASClient
|
|
114
121
|
def is_success?
|
115
122
|
(instance_variable_defined?(:@valid) && @valid) || (protocol > 1.0 && xml.name == "authenticationSuccess")
|
116
123
|
end
|
117
|
-
|
124
|
+
|
118
125
|
def is_failure?
|
119
126
|
(instance_variable_defined?(:@valid) && !@valid) || (protocol > 1.0 && xml.name == "authenticationFailure" )
|
120
127
|
end
|
121
128
|
end
|
122
|
-
|
129
|
+
|
123
130
|
# Represents a response from the CAS server to a proxy ticket request
|
124
131
|
# (i.e. after requesting a proxy ticket).
|
125
132
|
class ProxyResponse
|
126
133
|
include XmlResponse
|
127
|
-
|
134
|
+
|
128
135
|
attr_reader :proxy_ticket
|
129
|
-
|
136
|
+
|
130
137
|
def initialize(raw_text, options={})
|
131
138
|
parse(raw_text)
|
132
139
|
end
|
133
|
-
|
140
|
+
|
134
141
|
def parse(raw_text)
|
135
142
|
raise BadResponseException,
|
136
143
|
"CAS response is empty/blank." if raw_text.blank?
|
137
144
|
@parse_datetime = Time.now
|
138
|
-
|
145
|
+
|
139
146
|
@xml = check_and_parse_xml(raw_text)
|
140
|
-
|
147
|
+
|
141
148
|
if is_success?
|
142
149
|
@proxy_ticket = @xml.elements["cas:proxyTicket"].text.strip if @xml.elements["cas:proxyTicket"]
|
143
150
|
elsif is_failure?
|
@@ -147,43 +154,43 @@ module CASClient
|
|
147
154
|
# this should never happen, since the response should already have been recognized as invalid
|
148
155
|
raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
|
149
156
|
end
|
150
|
-
|
157
|
+
|
151
158
|
end
|
152
|
-
|
159
|
+
|
153
160
|
def is_success?
|
154
161
|
xml.name == "proxySuccess"
|
155
162
|
end
|
156
|
-
|
163
|
+
|
157
164
|
def is_failure?
|
158
165
|
xml.name == "proxyFailure"
|
159
166
|
end
|
160
167
|
end
|
161
|
-
|
168
|
+
|
162
169
|
# Represents a response from the CAS server to a login request
|
163
170
|
# (i.e. after submitting a username/password).
|
164
171
|
class LoginResponse
|
165
172
|
attr_reader :tgt, :ticket, :service_redirect_url
|
166
173
|
attr_reader :failure_message
|
167
|
-
|
174
|
+
|
168
175
|
def initialize(http_response = nil, options={})
|
169
176
|
parse_http_response(http_response) if http_response
|
170
177
|
end
|
171
|
-
|
178
|
+
|
172
179
|
def parse_http_response(http_response)
|
173
180
|
header = http_response.to_hash
|
174
|
-
|
181
|
+
|
175
182
|
# FIXME: this regexp might be incorrect...
|
176
183
|
if header['set-cookie'] &&
|
177
|
-
|
178
|
-
|
184
|
+
header['set-cookie'].first &&
|
185
|
+
header['set-cookie'].first =~ /tgt=([^&]+);/
|
179
186
|
@tgt = $~[1]
|
180
187
|
end
|
181
|
-
|
188
|
+
|
182
189
|
location = header['location'].first if header['location'] && header['location'].first
|
183
190
|
if location =~ /ticket=([^&]+)/
|
184
191
|
@ticket = $~[1]
|
185
192
|
end
|
186
|
-
|
193
|
+
|
187
194
|
if not ((http_response.kind_of?(Net::HTTPSuccess) || http_response.kind_of?(Net::HTTPFound)) && @ticket.present?)
|
188
195
|
@failure = true
|
189
196
|
# Try to extract the error message -- this only works with RubyCAS-Server.
|
@@ -195,19 +202,19 @@ module CASClient
|
|
195
202
|
@failure_message = body
|
196
203
|
end
|
197
204
|
end
|
198
|
-
|
205
|
+
|
199
206
|
@service_redirect_url = location
|
200
207
|
end
|
201
|
-
|
208
|
+
|
202
209
|
def is_success?
|
203
210
|
!@failure && !ticket.blank?
|
204
211
|
end
|
205
|
-
|
212
|
+
|
206
213
|
def is_failure?
|
207
214
|
@failure == true
|
208
215
|
end
|
209
216
|
end
|
210
|
-
|
217
|
+
|
211
218
|
class BadResponseException < CASException
|
212
219
|
end
|
213
220
|
end
|
@@ -4,14 +4,16 @@ module CASClient
|
|
4
4
|
class AbstractTicketStore
|
5
5
|
|
6
6
|
attr_accessor :log
|
7
|
-
|
7
|
+
def log
|
8
|
+
@log ||= CASClient::LoggerWrapper.new
|
9
|
+
end
|
8
10
|
|
9
|
-
def process_single_sign_out(
|
11
|
+
def process_single_sign_out(st)
|
10
12
|
|
11
|
-
session_id, session = get_session_for_service_ticket(
|
13
|
+
session_id, session = get_session_for_service_ticket(st)
|
12
14
|
if session
|
13
15
|
session.destroy
|
14
|
-
log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{
|
16
|
+
log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{st.inspect}.")
|
15
17
|
else
|
16
18
|
log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
|
17
19
|
end
|
@@ -19,16 +21,17 @@ module CASClient
|
|
19
21
|
if session_id
|
20
22
|
log.info("Single-sign-out for service ticket #{session_id.inspect} completed successfuly.")
|
21
23
|
else
|
22
|
-
log.debug("No session id found for CAS ticket #{
|
24
|
+
log.debug("No session id found for CAS ticket #{st}")
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def get_session_for_service_ticket(st)
|
27
|
-
session_id = read_service_session_lookup(
|
28
|
-
|
29
|
-
|
29
|
+
session_id = read_service_session_lookup(st)
|
30
|
+
unless session_id.nil?
|
31
|
+
# This feels a bit hackish, but there isn't really a better way to go about it that I am aware of yet
|
32
|
+
session = ActiveRecord::SessionStore.session_class.find_by_session_id(session_id)
|
30
33
|
else
|
31
|
-
log.warn("Couldn't destroy session
|
34
|
+
log.warn("Couldn't destroy session service ticket #{st} because no corresponding session id could be found.")
|
32
35
|
end
|
33
36
|
[session_id, session]
|
34
37
|
end
|
@@ -53,6 +56,12 @@ module CASClient
|
|
53
56
|
def read_service_session_lookup(st)
|
54
57
|
raise 'Implement this in a subclass!'
|
55
58
|
end
|
59
|
+
|
60
|
+
def session_id_from_controller(controller)
|
61
|
+
session_id = controller.request.session_options[:id] || controller.session.session_id
|
62
|
+
raise CASClient::CASException, "Failed to extract session_id from controller" if session_id.nil?
|
63
|
+
session_id
|
64
|
+
end
|
56
65
|
end
|
57
66
|
|
58
67
|
# A Ticket Store that keeps it's tickets in a directory on the local filesystem.
|
@@ -83,10 +92,10 @@ module CASClient
|
|
83
92
|
# Rails session id.
|
84
93
|
# Returns the filename of the lookup file created.
|
85
94
|
def store_service_session_lookup(st, controller)
|
86
|
-
raise CASException, "No service_ticket specified."
|
87
|
-
raise CASException, "No controller specified."
|
95
|
+
raise CASException, "No service_ticket specified." if st.nil?
|
96
|
+
raise CASException, "No controller specified." if controller.nil?
|
88
97
|
|
89
|
-
sid = controller
|
98
|
+
sid = session_id_from_controller(controller)
|
90
99
|
|
91
100
|
st = st.ticket if st.kind_of? ServiceTicket
|
92
101
|
f = File.new(filename_of_service_session_lookup(st), 'w')
|
@@ -100,11 +109,11 @@ module CASClient
|
|
100
109
|
# cas_sess.<session ticket> file created in a prior call to
|
101
110
|
# #store_service_session_lookup.
|
102
111
|
def read_service_session_lookup(st)
|
103
|
-
raise CASException, "No service_ticket specified."
|
112
|
+
raise CASException, "No service_ticket specified." if st.nil?
|
104
113
|
|
105
114
|
st = st.ticket if st.kind_of? ServiceTicket
|
106
115
|
ssl_filename = filename_of_service_session_lookup(st)
|
107
|
-
return
|
116
|
+
return IO.read(ssl_filename) if File.exists?(ssl_filename)
|
108
117
|
end
|
109
118
|
|
110
119
|
# Removes a stored relationship between a ServiceTicket and a local
|
@@ -113,7 +122,7 @@ module CASClient
|
|
113
122
|
#
|
114
123
|
# See #store_service_session_lookup.
|
115
124
|
def cleanup_service_session_lookup(st)
|
116
|
-
raise CASException, "No service_ticket specified."
|
125
|
+
raise CASException, "No service_ticket specified." if st.nil?
|
117
126
|
|
118
127
|
st = st.ticket if st.kind_of? ServiceTicket
|
119
128
|
ssl_filename = filename_of_service_session_lookup(st)
|
@@ -121,6 +130,9 @@ module CASClient
|
|
121
130
|
end
|
122
131
|
|
123
132
|
def save_pgt_iou(pgt_iou, pgt)
|
133
|
+
raise CASException, "Invalid pgt_iou" if pgt_iou.nil?
|
134
|
+
raise CASException, "Invalid pgt" if pgt.nil?
|
135
|
+
|
124
136
|
# TODO: pstore contents should probably be encrypted...
|
125
137
|
pstore = open_pstore
|
126
138
|
|
@@ -135,17 +147,14 @@ module CASClient
|
|
135
147
|
pstore = open_pstore
|
136
148
|
|
137
149
|
pgt = nil
|
150
|
+
# TODO: need to periodically clean the storage, otherwise it will just keep growing
|
138
151
|
pstore.transaction do
|
139
152
|
pgt = pstore[pgt_iou]
|
153
|
+
pstore.delete pgt_iou
|
140
154
|
end
|
141
155
|
|
142
156
|
raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgt
|
143
157
|
|
144
|
-
# TODO: need to periodically clean the storage, otherwise it will just keep growing
|
145
|
-
pstore.transaction do
|
146
|
-
pstore.delete pgt_iou
|
147
|
-
end
|
148
|
-
|
149
158
|
pgt
|
150
159
|
end
|
151
160
|
|
@@ -19,25 +19,30 @@ module CASClient
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def store_service_session_lookup(st, controller)
|
22
|
-
|
22
|
+
raise CASException, "No service_ticket specified." unless st
|
23
|
+
raise CASException, "No controller specified." unless controller
|
23
24
|
|
24
25
|
st = st.ticket if st.kind_of? ServiceTicket
|
25
26
|
session = controller.session
|
26
27
|
session[:service_ticket] = st
|
27
28
|
end
|
28
29
|
|
29
|
-
def
|
30
|
+
def read_service_session_lookup(st)
|
31
|
+
raise CASException, "No service_ticket specified." unless st
|
30
32
|
st = st.ticket if st.kind_of? ServiceTicket
|
31
33
|
session = ActiveRecord::SessionStore::Session.find_by_service_ticket(st)
|
32
|
-
|
33
|
-
[session_id, session]
|
34
|
+
session ? session.session_id : nil
|
34
35
|
end
|
35
36
|
|
36
37
|
def cleanup_service_session_lookup(st)
|
37
38
|
#no cleanup needed for this ticket store
|
39
|
+
#we still raise the exception for API compliance
|
40
|
+
raise CASException, "No service_ticket specified." unless st
|
38
41
|
end
|
39
42
|
|
40
43
|
def save_pgt_iou(pgt_iou, pgt)
|
44
|
+
raise CASClient::CASException.new("Invalid pgt_iou") if pgt_iou.nil?
|
45
|
+
raise CASClient::CASException.new("Invalid pgt") if pgt.nil?
|
41
46
|
pgtiou = CasPgtiou.create(:pgt_iou => pgt_iou, :pgt_id => pgt)
|
42
47
|
end
|
43
48
|
|
@@ -45,9 +50,9 @@ module CASClient
|
|
45
50
|
raise CASException, "No pgt_iou specified. Cannot retrieve the pgt." unless pgt_iou
|
46
51
|
|
47
52
|
pgtiou = CasPgtiou.find_by_pgt_iou(pgt_iou)
|
48
|
-
pgt = pgtiou.pgt_id
|
49
53
|
|
50
|
-
raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless
|
54
|
+
raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgtiou
|
55
|
+
pgt = pgtiou.pgt_id
|
51
56
|
|
52
57
|
pgtiou.destroy
|
53
58
|
|