casablanca 0.0.2
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/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
|