rubycas-client 2.3.8 → 2.3.9.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.
- 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
|
|