casablanca 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/Manifest.txt +16 -0
- data/README.textile +87 -0
- data/Rakefile +15 -0
- data/bin/casablanca +9 -0
- data/init.rb +3 -0
- data/lib/casablanca/cli.rb +20 -0
- data/lib/casablanca/client.rb +163 -0
- data/lib/casablanca/filters/rails.rb +58 -0
- data/lib/casablanca/response_parsers.rb +50 -0
- data/lib/casablanca.rb +5 -0
- data/test/test_client.rb +100 -0
- data/test/test_helper.rb +77 -0
- data/test/test_parser.rb +22 -0
- data/test/test_rails_filter.rb +103 -0
- data/test/test_ticket.rb +71 -0
- metadata +84 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.textile
|
4
|
+
Rakefile
|
5
|
+
init.rb
|
6
|
+
bin/casablanca
|
7
|
+
lib/casablanca.rb
|
8
|
+
lib/casablanca/cli.rb
|
9
|
+
lib/casablanca/client.rb
|
10
|
+
lib/casablanca/filters/rails.rb
|
11
|
+
lib/casablanca/response_parsers.rb
|
12
|
+
test/test_client.rb
|
13
|
+
test/test_helper.rb
|
14
|
+
test/test_parser.rb
|
15
|
+
test/test_rails_filter.rb
|
16
|
+
test/test_ticket.rb
|
data/README.textile
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
h1. Casablanca
|
2
|
+
|
3
|
+
h2. Description
|
4
|
+
|
5
|
+
Casablanca is a single sign-on client for the CAS 2.0 protocol.
|
6
|
+
It can be run from the commandline and as a filter for Rails.
|
7
|
+
|
8
|
+
sudo gem install p8-casablanca
|
9
|
+
|
10
|
+
|
11
|
+
h2. Usage
|
12
|
+
|
13
|
+
Commandline:
|
14
|
+
|
15
|
+
casablanca
|
16
|
+
|
17
|
+
In IRB:
|
18
|
+
|
19
|
+
require 'casablanca'
|
20
|
+
|
21
|
+
C = Casablanca::CommandLineClient.new({ :cas_server_url => "http://localhost:4567",
|
22
|
+
:service_url => "http://localhost:3000" })
|
23
|
+
|
24
|
+
ticket = C.get_service_ticket('admin', 'admin')
|
25
|
+
C.authenticate_ticket(ticket)
|
26
|
+
|
27
|
+
|
28
|
+
In a Rails project:
|
29
|
+
- environment.rb:
|
30
|
+
|
31
|
+
Casablanca::RailsFilter.config do |config|
|
32
|
+
config[:cas_server_url] = "http://localhost:4567"
|
33
|
+
config[:service_url] = "http://localhost:3000"
|
34
|
+
end
|
35
|
+
|
36
|
+
- Add the following to application.rb:
|
37
|
+
|
38
|
+
before_filter Casablanca::RailsFilter
|
39
|
+
|
40
|
+
def current_person
|
41
|
+
@current_person ||= login_from_cas unless @current_person == false
|
42
|
+
end
|
43
|
+
|
44
|
+
def login_from_cas
|
45
|
+
if session[:cas_user]
|
46
|
+
person = Person.find_by_name(session[:cas_user])
|
47
|
+
logout_killing_session! unless person
|
48
|
+
person
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
- Add the following to you logout action
|
53
|
+
|
54
|
+
Casablanca::RailsFilter.logout(self)
|
55
|
+
|
56
|
+
h2. TODO
|
57
|
+
|
58
|
+
* Add logging
|
59
|
+
* Add extra attributes returned from the server
|
60
|
+
* Implement gateway and proxy
|
61
|
+
* Check for single signout
|
62
|
+
* Check for endless redirects
|
63
|
+
|
64
|
+
h2. LICENSE:
|
65
|
+
|
66
|
+
(The MIT License)
|
67
|
+
|
68
|
+
Copyright (c) 2009 Petrik de Heus
|
69
|
+
|
70
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
71
|
+
a copy of this software and associated documentation files (the
|
72
|
+
'Software'), to deal in the Software without restriction, including
|
73
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
74
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
75
|
+
permit persons to whom the Software is furnished to do so, subject to
|
76
|
+
the following conditions:
|
77
|
+
|
78
|
+
The above copyright notice and this permission notice shall be
|
79
|
+
included in all copies or substantial portions of the Software.
|
80
|
+
|
81
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
82
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
83
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
84
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
85
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
86
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
87
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require 'lib/casablanca.rb'
|
6
|
+
|
7
|
+
Hoe.new('casablanca', Casablanca::VERSION) do |p|
|
8
|
+
p.developer('FIX', 'FIX@example.com')
|
9
|
+
end
|
10
|
+
|
11
|
+
# require 'metric_fu'
|
12
|
+
#
|
13
|
+
# MetricFu::Configuration.run do |config|
|
14
|
+
# config.coverage = { :test_files => ['test/**/test_*.rb'] }
|
15
|
+
# end
|
data/bin/casablanca
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
irb = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb'
|
3
|
+
options = { :sandbox => false, :irb => irb }
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
libs << " -r #{File.dirname(__FILE__)}/../lib/casablanca.rb"
|
7
|
+
libs << " -r #{File.dirname(__FILE__)}/../lib/casablanca/cli.rb"
|
8
|
+
|
9
|
+
exec "#{options[:irb]} #{libs} --simple-prompt"
|
data/init.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
config = { :cas_server_url => "http://localhost:4567", :service_url => "http://localhost:3000" }
|
2
|
+
INFO = %(
|
3
|
+
=====================================================
|
4
|
+
CASABLANCA CLIENT CONSOLE (#{Casablanca::VERSION})
|
5
|
+
|
6
|
+
Use C for a configured client (#{config.inspect})
|
7
|
+
Example:
|
8
|
+
|
9
|
+
t = C.get_service_ticket('admin', 'admin')
|
10
|
+
C.authenticate_ticket(t)
|
11
|
+
|
12
|
+
The configuration can be changed:
|
13
|
+
C.cas_server_url = "http://example.com/cas_server"
|
14
|
+
C.service_url = "http://example.com/application"
|
15
|
+
|
16
|
+
)
|
17
|
+
|
18
|
+
C = @client = Casablanca::CommandLineClient.new(config)
|
19
|
+
|
20
|
+
puts INFO
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/https'
|
4
|
+
require 'rexml/document'
|
5
|
+
|
6
|
+
module Casablanca
|
7
|
+
|
8
|
+
class Client
|
9
|
+
attr_accessor :cas_server_url, :service_url
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
raise ":cas_server_url is required" unless config[:cas_server_url]
|
13
|
+
@cas_server_url = config[:cas_server_url]
|
14
|
+
@service_url = config[:service_url]
|
15
|
+
end
|
16
|
+
|
17
|
+
def authenticate_ticket(ticket)
|
18
|
+
response = request_validation(ticket)
|
19
|
+
ticket.authenticate(response)
|
20
|
+
end
|
21
|
+
|
22
|
+
def login_url
|
23
|
+
url = "#{@cas_server_url}/login?service=#{@service_url}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def logout_url
|
27
|
+
"#{@cas_server_url}/logout"
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_url
|
31
|
+
"#{@cas_server_url}/proxyValidate"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def request_validation(ticket)
|
37
|
+
raise "ticket.service_url cannot be empty" if ticket.service_url.nil? || ticket.service_url.strip == ""
|
38
|
+
uri = URI.parse(validate_url)
|
39
|
+
uri.merge_query(ticket.to_request_params)
|
40
|
+
response = get(uri)
|
41
|
+
puts "#{@cas_server_url} #{response.inspect}:\n#{response.body}"
|
42
|
+
unless response.kind_of?(Net::HTTPSuccess)
|
43
|
+
raise ResponseError, "#{response.code}, #{response.body}"
|
44
|
+
end
|
45
|
+
response.body
|
46
|
+
end
|
47
|
+
|
48
|
+
def get(uri)
|
49
|
+
https(uri) do |h|
|
50
|
+
h.get("#{uri.path}?#{uri.query}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def https(uri)
|
55
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
56
|
+
https.use_ssl = (uri.scheme == 'https')
|
57
|
+
begin
|
58
|
+
https.start do |h|
|
59
|
+
yield(h)
|
60
|
+
end
|
61
|
+
rescue Errno::ECONNREFUSED => error
|
62
|
+
raise CasServerException
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class CommandLineClient < Client
|
69
|
+
|
70
|
+
def login(username, password)
|
71
|
+
post(URI.parse(login_url), {:username => username, :password => password, :service => service_url})
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_service_ticket(username, password)
|
75
|
+
location = login(username, password)['location']
|
76
|
+
query = {}
|
77
|
+
URI.parse(location).query.collect{|q| k,v = q.split('='); query[k] = v }
|
78
|
+
Ticket.new(query['ticket'], @service_url)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def post(uri, form_data)
|
84
|
+
req = Net::HTTP::Post.new(uri.path)
|
85
|
+
req.set_form_data(form_data, ';')
|
86
|
+
https(uri) do |h|
|
87
|
+
h.request(req)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class Ticket
|
93
|
+
attr_accessor :user, :failure_code, :failure_message
|
94
|
+
attr_reader :service_url, :ticket
|
95
|
+
|
96
|
+
def initialize(ticket, service_url, renew = false)
|
97
|
+
@service_url = service_url
|
98
|
+
@ticket = ticket
|
99
|
+
@renew = renew
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.from_hash(hash)
|
103
|
+
ticket = Ticket.new(hash[:ticket], hash[:service_url], hash[:renew])
|
104
|
+
ticket.user = hash[:user]
|
105
|
+
ticket
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_request_params
|
109
|
+
params = {:service => @service_url,
|
110
|
+
:ticket => @ticket }
|
111
|
+
params[:renew] = 1 if @renew
|
112
|
+
params
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_hash
|
116
|
+
props = {}
|
117
|
+
props[:user] = @user if authenticated?
|
118
|
+
props[:renew] = @renew if @renew
|
119
|
+
props[:service_url] = @service_url
|
120
|
+
props[:ticket] = @ticket
|
121
|
+
props
|
122
|
+
end
|
123
|
+
|
124
|
+
def authenticated?
|
125
|
+
!!@user
|
126
|
+
end
|
127
|
+
|
128
|
+
def authenticate(body)
|
129
|
+
response = CasResponseParser.parse(self, body)
|
130
|
+
authenticated?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class ResponseError < Exception
|
135
|
+
end
|
136
|
+
|
137
|
+
class CasServerException < Exception
|
138
|
+
end
|
139
|
+
|
140
|
+
class UnknownTicketType < Exception
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class URI::HTTP
|
145
|
+
def merge_query(hash)
|
146
|
+
q = query ? query + '&' : ''
|
147
|
+
self.query = "#{q}#{hash_to_uri_array(hash)}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def hash_to_uri_array(hash)
|
151
|
+
hash.collect do |name, value|
|
152
|
+
if value.kind_of? Array
|
153
|
+
value.map {|v| stringify_param(name, v) }
|
154
|
+
else
|
155
|
+
stringify_param(name, value)
|
156
|
+
end
|
157
|
+
end.join('&')
|
158
|
+
end
|
159
|
+
|
160
|
+
def stringify_param(name, value)
|
161
|
+
"#{CGI::escape(name.to_s)}=#{CGI::escape(value.to_s)}"
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Casablanca
|
2
|
+
class RailsFilter
|
3
|
+
@@client = nil
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def client=client
|
8
|
+
@@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def client
|
12
|
+
@@client
|
13
|
+
end
|
14
|
+
|
15
|
+
def config
|
16
|
+
config = {}
|
17
|
+
yield config
|
18
|
+
@@client = Client.new(config)
|
19
|
+
end
|
20
|
+
|
21
|
+
def filter(controller)
|
22
|
+
return true if previous_ticket(controller) && !controller.params[:renew]
|
23
|
+
ticket = Ticket.new(controller.params[:ticket], @@client.service_url, controller.params[:renew])
|
24
|
+
if @@client.authenticate_ticket(ticket)
|
25
|
+
puts "Ticket authenticated"
|
26
|
+
controller.session[:cas_user] = ticket.user
|
27
|
+
controller.session[:cas_ticket] = ticket.to_hash
|
28
|
+
return true
|
29
|
+
else
|
30
|
+
puts "Ticket authentication failed: #{ticket.failure_message}"
|
31
|
+
controller.session[:cas_user] = nil
|
32
|
+
controller.session[:cas_ticket] = nil
|
33
|
+
controller.send(:redirect_to, login_url)
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def login_url
|
39
|
+
@@client.login_url
|
40
|
+
end
|
41
|
+
|
42
|
+
def logout(controller)
|
43
|
+
controller.send(:reset_session)
|
44
|
+
controller.send(:redirect_to, @@client.logout_url)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def previous_ticket(controller)
|
50
|
+
hash = controller.session[:cas_ticket]
|
51
|
+
return nil unless hash
|
52
|
+
Ticket.from_hash(hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Casablanca
|
2
|
+
|
3
|
+
class CasResponseParser
|
4
|
+
def protocol
|
5
|
+
self.class.to_s.gsub(/(Casablanca::Cas_|_ResponseParser)/, '').gsub('_', '.').to_f
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.parse(ticket, body)
|
9
|
+
raise ResponseError, "Response body is empty" if body.nil? || body.strip == ""
|
10
|
+
#return Cas_1_0_Parser.new(body) if ?
|
11
|
+
response = Cas_2_0_ResponseParser.new(body)
|
12
|
+
ticket.user = response.user
|
13
|
+
unless response.authenticated?
|
14
|
+
ticket.failure_code = response.failure_code
|
15
|
+
ticket.failure_message = response.failure_message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Cas_2_0_ResponseParser < CasResponseParser
|
21
|
+
def initialize(xml)
|
22
|
+
doc = REXML::Document.new(xml)
|
23
|
+
@xml = doc.elements['cas:serviceResponse'].elements[1]
|
24
|
+
end
|
25
|
+
|
26
|
+
def user
|
27
|
+
strip_text(@xml.elements['cas:user'])
|
28
|
+
end
|
29
|
+
|
30
|
+
def authenticated?
|
31
|
+
@xml.name == 'authenticationSuccess'
|
32
|
+
end
|
33
|
+
|
34
|
+
def failure_code
|
35
|
+
@xml.elements['//cas:authenticationFailure'].attributes['code']
|
36
|
+
end
|
37
|
+
|
38
|
+
def failure_message
|
39
|
+
strip_text(@xml.elements['//cas:authenticationFailure'])
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def strip_text(tag)
|
45
|
+
tag.text.strip if tag
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
data/lib/casablanca.rb
ADDED
data/test/test_client.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class TestClient < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@client = Client.new(:cas_server_url => "http://localhost:4567", :service_url => "http://localhost:3000")
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_config
|
9
|
+
assert_equal @client.service_url, "http://localhost:3000"
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_config_requires_cas_server_url
|
13
|
+
assert_raises(RuntimeError) do
|
14
|
+
@client = Client.new({})
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_authenticate_ticket
|
19
|
+
service_ticket = get_service_ticket
|
20
|
+
@client = Client.new(:cas_server_url => "http://localhost:4567", :service_url => "http://localhost:3000")
|
21
|
+
mock_authenticate_ticket(VALID_REQUEST)
|
22
|
+
@client.authenticate_ticket(service_ticket)
|
23
|
+
assert_equal 'admin', service_ticket.user
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_validate_expired_ticket
|
27
|
+
mock_authenticate_ticket(INVALID_TICKET)
|
28
|
+
ticket = 'ST-1231341579r871C5757B79767C21E'
|
29
|
+
service_ticket = Ticket.new(ticket, 'http://localhost:3000', true)
|
30
|
+
@client.authenticate_ticket(service_ticket)
|
31
|
+
assert_equal 'INVALID_TICKET', service_ticket.failure_code
|
32
|
+
#assert_equal "Ticket 'ST-1231341579r871C5757B79767C21E' has already been used up.", ticket.failure_message
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_validate_invalid_ticket
|
36
|
+
mock_authenticate_ticket(INVALID_TICKET)
|
37
|
+
ticket = 'ST-1231242314r72465638160B31E8D1'
|
38
|
+
service_ticket = Ticket.new(ticket, 'http://localhost:3000', true)
|
39
|
+
@client.authenticate_ticket(service_ticket)
|
40
|
+
assert_equal 'INVALID_TICKET', service_ticket.failure_code
|
41
|
+
assert_equal "Ticket ST-1231242314r72465638160B31E8D1 not recognized.", service_ticket.failure_message
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_authenticate_ticket_with_empty_service_url
|
45
|
+
service_ticket = Ticket.new('ticket', nil)
|
46
|
+
assert_raises(RuntimeError) do
|
47
|
+
@client.authenticate_ticket(service_ticket)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_login_url
|
52
|
+
assert_equal 'http://localhost:4567/login?service=http://localhost:3000', @client.login_url
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_logout_url
|
56
|
+
assert_equal 'http://localhost:4567/logout', @client.logout_url
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_validate_url
|
60
|
+
assert_equal 'http://localhost:4567/proxyValidate', @client.validate_url
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
class TestCommandLineClient < Test::Unit::TestCase
|
66
|
+
def setup
|
67
|
+
@client = CommandLineClient.new(:cas_server_url => "http://localhost:4567", :service_url => "http://localhost:3000")
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_login
|
71
|
+
mock_get_service_ticket
|
72
|
+
res = @client.login('admin', 'admin')
|
73
|
+
assert_equal '', res.body
|
74
|
+
assert_equal '303', res.code
|
75
|
+
assert_equal 0, res['location'] =~ /^http:\/\/localhost:3000\?ticket=ST-/
|
76
|
+
assert_equal 61, res['location'].size
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_get_service_ticket
|
80
|
+
mock_get_service_ticket
|
81
|
+
ticket = @client.get_service_ticket('admin', 'admin')
|
82
|
+
assert_equal 0, ticket.ticket =~ /^ST-/
|
83
|
+
assert_equal 32, ticket.ticket.size
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
class TestURIHTTP < Test::Unit::TestCase
|
89
|
+
def test_merge_query
|
90
|
+
uri = URI.parse('http://localhost:4567/login')
|
91
|
+
uri.merge_query({:order_by => ['1', '2']})
|
92
|
+
assert_equal 'order_by=1&order_by=2', uri.query
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_merge_query_with_existing_query
|
96
|
+
uri = URI.parse('http://localhost:4567/login?search=ah')
|
97
|
+
uri.merge_query({:order_by => ['1', '2']})
|
98
|
+
assert_equal 'search=ah&order_by=1&order_by=2', uri.query
|
99
|
+
end
|
100
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'casablanca.rb')))
|
2
|
+
require(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'casablanca', 'filters', 'rails.rb')))
|
3
|
+
require 'test/unit'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
# set to false if you're integration testing against a real server
|
8
|
+
MOCK_REQUESTS = true
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
include Casablanca
|
12
|
+
|
13
|
+
def mock_authenticate_ticket(body)
|
14
|
+
if MOCK_REQUESTS
|
15
|
+
@client.expects(:get).returns(MockResponse.new(body, '200', :location => 'http://localhost:3000?ticket=ST-1231341579r871C5757B79767C21E'))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def mock_get_service_ticket
|
20
|
+
if MOCK_REQUESTS
|
21
|
+
@client.expects(:post).returns(MockResponse.new('', '303', :location => 'http://localhost:3000?ticket=ST-1231341579r871C5757B79767C21E'))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_service_ticket
|
26
|
+
cli = CommandLineClient.new(:cas_server_url => "http://localhost:4567", :service_url => "http://localhost:3000")
|
27
|
+
if MOCK_REQUESTS
|
28
|
+
cli.expects(:post).returns(MockResponse.new('', '303', :location => 'http://localhost:3000?ticket=ST-1231341579r871C5757B79767C21E'))
|
29
|
+
end
|
30
|
+
cli.get_service_ticket('admin', 'admin')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class MockResponse < Net::HTTPResponse
|
35
|
+
attr_accessor :body, :code
|
36
|
+
def initialize(body, code=200, header={})
|
37
|
+
@body, @code, @header = body, code, header
|
38
|
+
end
|
39
|
+
|
40
|
+
def []= key, value
|
41
|
+
@header[key.to_sym] = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def [] key
|
45
|
+
@header[key.to_sym]
|
46
|
+
end
|
47
|
+
|
48
|
+
def kind_of?(klass)
|
49
|
+
if klass == Net::HTTPSuccess
|
50
|
+
code.to_i == 200
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
VALID_REQUEST = %(
|
56
|
+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
57
|
+
<cas:authenticationSuccess>
|
58
|
+
<cas:user>admin</cas:user>
|
59
|
+
</cas:authenticationSuccess>
|
60
|
+
</cas:serviceResponse>
|
61
|
+
)
|
62
|
+
|
63
|
+
INVALID_REQUEST = %(
|
64
|
+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
65
|
+
<cas:authenticationFailure code="INVALID_REQUEST">
|
66
|
+
Ticket or service parameter was missing in the request.
|
67
|
+
</cas:authenticationFailure>
|
68
|
+
</cas:serviceResponse>
|
69
|
+
)
|
70
|
+
|
71
|
+
INVALID_TICKET = %(
|
72
|
+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
73
|
+
<cas:authenticationFailure code="INVALID_TICKET">
|
74
|
+
Ticket ST-1231242314r72465638160B31E8D1 not recognized.
|
75
|
+
</cas:authenticationFailure>
|
76
|
+
</cas:serviceResponse>
|
77
|
+
)
|
data/test/test_parser.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class TestCas_2_0_ResponseParser < Test::Unit::TestCase
|
4
|
+
def test_parse_valid_ticket
|
5
|
+
response = Cas_2_0_ResponseParser.new(VALID_REQUEST)
|
6
|
+
assert_equal 'admin', response.user
|
7
|
+
assert_equal 2.0, response.protocol
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_parse_invalid_request
|
11
|
+
response = Cas_2_0_ResponseParser.new(INVALID_REQUEST)
|
12
|
+
assert_equal 'INVALID_REQUEST', response.failure_code
|
13
|
+
assert_equal 'Ticket or service parameter was missing in the request.', response.failure_message
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_parse_invalid_ticket
|
17
|
+
response = Cas_2_0_ResponseParser.new(INVALID_TICKET)
|
18
|
+
assert_equal 'INVALID_TICKET', response.failure_code
|
19
|
+
assert_equal 'Ticket ST-1231242314r72465638160B31E8D1 not recognized.', response.failure_message
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
require 'action_pack'
|
3
|
+
class TestRailsFilter < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@client = Client.new(:cas_server_url => "http://localhost:4567", :service_url => "http://localhost:3000")
|
6
|
+
RailsFilter.client = @client
|
7
|
+
@controller = Controller.new
|
8
|
+
@controller.params = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_login_url
|
12
|
+
assert_equal 'http://localhost:4567/login?service=http://localhost:3000', RailsFilter.login_url
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_config
|
16
|
+
Casablanca::RailsFilter.config do |config|
|
17
|
+
config[:cas_server_url] = "http://example.com/cas_server"
|
18
|
+
config[:service_url] = "http://example.com/application"
|
19
|
+
end
|
20
|
+
assert_equal "http://example.com/cas_server", RailsFilter.client.cas_server_url
|
21
|
+
assert_equal "http://example.com/application", RailsFilter.client.service_url
|
22
|
+
end
|
23
|
+
# def test_filter_requires_config
|
24
|
+
# RailsFilter.config = nil
|
25
|
+
# assert_raises(RuntimeError) do
|
26
|
+
# RailsFilter.filter(Controller.new)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
|
30
|
+
def test_logout
|
31
|
+
RailsFilter.logout(@controller)
|
32
|
+
assert_equal({}, @controller.session)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_filter_invalid_attempt
|
36
|
+
mock_authenticate_ticket(INVALID_TICKET)
|
37
|
+
@controller.session = {}
|
38
|
+
assert_equal false, RailsFilter.filter(@controller)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_filter_authenticated
|
42
|
+
service_ticket = get_service_ticket
|
43
|
+
params = {:ticket => service_ticket.ticket}
|
44
|
+
mock_authenticate_ticket(VALID_REQUEST)
|
45
|
+
@controller.params = params
|
46
|
+
assert_equal true, RailsFilter.filter(@controller)
|
47
|
+
assert_session('admin', { :ticket => service_ticket.ticket, :user => 'admin', :service_url => 'http://localhost:3000' })
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_filter_same_ticket
|
51
|
+
params = {:ticket => 'a'}
|
52
|
+
@controller.session = { :cas_ticket => params, :cas_user => 'admin' }
|
53
|
+
@controller.params = params
|
54
|
+
assert_equal true, RailsFilter.filter(@controller)
|
55
|
+
assert_session('admin', params)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_filter_resets_sessions_for_renew
|
59
|
+
mock_authenticate_ticket(INVALID_TICKET)
|
60
|
+
@controller.session[:cas_ticket] = { :ticket => 'a', :service_url => 'b' }
|
61
|
+
@controller.params = {:renew => true }
|
62
|
+
assert_equal false, RailsFilter.filter(@controller)
|
63
|
+
assert_session(nil, nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
def assert_session(user, ticket)
|
67
|
+
assert_equal ticket, @controller.session[:cas_ticket]
|
68
|
+
assert_equal user, @controller.session[:cas_user]
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
class Controller # < ActionController::Base
|
74
|
+
attr_accessor :params, :session
|
75
|
+
def initialize
|
76
|
+
@session = {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def request
|
80
|
+
Request.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def url_for(url)
|
84
|
+
url
|
85
|
+
end
|
86
|
+
|
87
|
+
def redirect_to(url)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def reset_session
|
93
|
+
@session = {}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Request
|
98
|
+
def headers
|
99
|
+
{}
|
100
|
+
end
|
101
|
+
def post?
|
102
|
+
end
|
103
|
+
end
|
data/test/test_ticket.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class TestTicket < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@ticket = Ticket.new('ST-1231242314r72465638160B31E8D1', 'http://localhost:3000')
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_create_service_ticket
|
9
|
+
ticket = Ticket.new('ST-1231242314r72465638160B31E8D1', 'http://localhost:3000')
|
10
|
+
assert_equal 'ST-1231242314r72465638160B31E8D1', ticket.to_hash[:ticket]
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_create_proxy_ticket
|
14
|
+
ticket = Ticket.new('PT-1231242314r72465638160B31E8D1', 'http://localhost:3000')
|
15
|
+
assert_equal 'PT-1231242314r72465638160B31E8D1', ticket.to_hash[:ticket]
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_to_hash
|
19
|
+
ticket = Ticket.new('ST-1231242314r72465638160B31E8D1', 'http://localhost:3000')
|
20
|
+
assert_equal 'ST-1231242314r72465638160B31E8D1', ticket.to_hash[:ticket]
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_from_hash
|
24
|
+
props = {:ticket => 'ticket',
|
25
|
+
:service_url => "http://localhost:3000",
|
26
|
+
:renew => 1,
|
27
|
+
:user => 'admin' }
|
28
|
+
ticket = Ticket.from_hash(props)
|
29
|
+
assert_equal props, ticket.to_hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_to_request_params
|
33
|
+
ticket = Ticket.new('ticket', 'http://localhost:3000')
|
34
|
+
expected = {:ticket => 'ticket',
|
35
|
+
:service => "http://localhost:3000" }
|
36
|
+
assert_equal(expected, ticket.to_request_params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_to_request_params_with_renew
|
40
|
+
ticket = Ticket.new('ticket', 'http://localhost:3000', true)
|
41
|
+
expected = {:ticket => 'ticket',
|
42
|
+
:service => "http://localhost:3000",
|
43
|
+
:renew => 1 }
|
44
|
+
assert_equal(expected, ticket.to_request_params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_authenticate_valid_ticket
|
48
|
+
@ticket.authenticate(VALID_REQUEST)
|
49
|
+
assert_equal 'admin', @ticket.user
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_authenticate_invalid_request_resets_ticket_to_unauthenticated
|
53
|
+
@ticket.authenticate(VALID_REQUEST)
|
54
|
+
assert_equal true, @ticket.authenticated?
|
55
|
+
@ticket.authenticate(INVALID_REQUEST)
|
56
|
+
assert_equal false, @ticket.authenticated?
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_authenticate_invalid_request
|
60
|
+
@ticket.authenticate(INVALID_REQUEST)
|
61
|
+
assert_equal 'INVALID_REQUEST', @ticket.failure_code
|
62
|
+
assert_equal 'Ticket or service parameter was missing in the request.', @ticket.failure_message
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_authenticate_invalid_ticket
|
66
|
+
@ticket.authenticate(INVALID_TICKET)
|
67
|
+
assert_equal 'INVALID_TICKET', @ticket.failure_code
|
68
|
+
assert_equal 'Ticket ST-1231242314r72465638160B31E8D1 not recognized.', @ticket.failure_message
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: casablanca
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- FIX
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-19 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.8.2
|
24
|
+
version:
|
25
|
+
description: A single sign-on client for the CAS 2.0 protocol
|
26
|
+
email:
|
27
|
+
- FIX@example.com
|
28
|
+
executables:
|
29
|
+
- casablanca
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- History.txt
|
34
|
+
- Manifest.txt
|
35
|
+
files:
|
36
|
+
- History.txt
|
37
|
+
- Manifest.txt
|
38
|
+
- README.textile
|
39
|
+
- Rakefile
|
40
|
+
- init.rb
|
41
|
+
- bin/casablanca
|
42
|
+
- lib/casablanca.rb
|
43
|
+
- lib/casablanca/cli.rb
|
44
|
+
- lib/casablanca/client.rb
|
45
|
+
- lib/casablanca/filters/rails.rb
|
46
|
+
- lib/casablanca/response_parsers.rb
|
47
|
+
- test/test_client.rb
|
48
|
+
- test/test_helper.rb
|
49
|
+
- test/test_parser.rb
|
50
|
+
- test/test_rails_filter.rb
|
51
|
+
- test/test_ticket.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://rubyforge.org/projects/casablanca/
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --main
|
57
|
+
- README.txt
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project: casablanca
|
75
|
+
rubygems_version: 1.2.0
|
76
|
+
signing_key:
|
77
|
+
specification_version: 2
|
78
|
+
summary: A single sign-on client for the CAS 2.0 protocol
|
79
|
+
test_files:
|
80
|
+
- test/test_client.rb
|
81
|
+
- test/test_helper.rb
|
82
|
+
- test/test_parser.rb
|
83
|
+
- test/test_rails_filter.rb
|
84
|
+
- test/test_ticket.rb
|