aker 3.0.0.pre
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/CHANGELOG.md +210 -0
- data/README.md +282 -0
- data/assets/aker/form/login.css +73 -0
- data/assets/aker/form/login.html.erb +44 -0
- data/lib/aker/authorities/automatic_access.rb +36 -0
- data/lib/aker/authorities/composite.rb +301 -0
- data/lib/aker/authorities/static.rb +283 -0
- data/lib/aker/authorities/support/find_sole_user.rb +24 -0
- data/lib/aker/authorities/support.rb +9 -0
- data/lib/aker/authorities.rb +46 -0
- data/lib/aker/cas/authority.rb +79 -0
- data/lib/aker/cas/configuration_helper.rb +85 -0
- data/lib/aker/cas/middleware/logout_responder.rb +49 -0
- data/lib/aker/cas/middleware/ticket_remover.rb +35 -0
- data/lib/aker/cas/middleware.rb +6 -0
- data/lib/aker/cas/proxy_mode.rb +108 -0
- data/lib/aker/cas/rack_proxy_callback.rb +188 -0
- data/lib/aker/cas/service_mode.rb +88 -0
- data/lib/aker/cas/service_url.rb +62 -0
- data/lib/aker/cas/user_ext.rb +64 -0
- data/lib/aker/cas.rb +31 -0
- data/lib/aker/central_parameters.rb +101 -0
- data/lib/aker/configuration.rb +534 -0
- data/lib/aker/deprecation.rb +105 -0
- data/lib/aker/form/custom_views_mode.rb +80 -0
- data/lib/aker/form/login_form_asset_provider.rb +56 -0
- data/lib/aker/form/middleware/custom_view_login_responder.rb +19 -0
- data/lib/aker/form/middleware/login_renderer.rb +72 -0
- data/lib/aker/form/middleware/login_responder.rb +71 -0
- data/lib/aker/form/middleware/logout_responder.rb +26 -0
- data/lib/aker/form/middleware.rb +10 -0
- data/lib/aker/form/mode.rb +118 -0
- data/lib/aker/form.rb +26 -0
- data/lib/aker/group.rb +67 -0
- data/lib/aker/group_membership.rb +162 -0
- data/lib/aker/ldap/authority.rb +392 -0
- data/lib/aker/ldap/user_ext.rb +19 -0
- data/lib/aker/ldap.rb +22 -0
- data/lib/aker/modes/base.rb +85 -0
- data/lib/aker/modes/http_basic.rb +100 -0
- data/lib/aker/modes/support/attempted_path.rb +22 -0
- data/lib/aker/modes/support/rfc_2617.rb +32 -0
- data/lib/aker/modes/support.rb +12 -0
- data/lib/aker/modes.rb +48 -0
- data/lib/aker/rack/authenticate.rb +37 -0
- data/lib/aker/rack/configuration_helper.rb +18 -0
- data/lib/aker/rack/default_logout_responder.rb +36 -0
- data/lib/aker/rack/environment_helper.rb +34 -0
- data/lib/aker/rack/facade.rb +102 -0
- data/lib/aker/rack/failure.rb +69 -0
- data/lib/aker/rack/logout.rb +63 -0
- data/lib/aker/rack/request_ext.rb +19 -0
- data/lib/aker/rack/session_timer.rb +95 -0
- data/lib/aker/rack/setup.rb +77 -0
- data/lib/aker/rack.rb +107 -0
- data/lib/aker/test/helpers.rb +22 -0
- data/lib/aker/test.rb +8 -0
- data/lib/aker/user.rb +231 -0
- data/lib/aker/version.rb +3 -0
- data/lib/aker.rb +51 -0
- data/spec/aker/aker-sample.yml +11 -0
- data/spec/aker/authorities/automatic_access_spec.rb +52 -0
- data/spec/aker/authorities/composite_spec.rb +488 -0
- data/spec/aker/authorities/nu-schema.jar +0 -0
- data/spec/aker/authorities/static_spec.rb +455 -0
- data/spec/aker/authorities/support/find_sole_user_spec.rb +33 -0
- data/spec/aker/authorities_spec.rb +16 -0
- data/spec/aker/cas/authority_spec.rb +106 -0
- data/spec/aker/cas/configuration_helper_spec.rb +92 -0
- data/spec/aker/cas/middleware/logout_responder_spec.rb +47 -0
- data/spec/aker/cas/middleware/ticket_remover_spec.rb +49 -0
- data/spec/aker/cas/proxy_mode_spec.rb +185 -0
- data/spec/aker/cas/rack_proxy_callback_spec.rb +190 -0
- data/spec/aker/cas/service_mode_spec.rb +122 -0
- data/spec/aker/cas/service_url_spec.rb +114 -0
- data/spec/aker/cas/user_ext_spec.rb +27 -0
- data/spec/aker/cas_spec.rb +19 -0
- data/spec/aker/central_parameters_spec.rb +44 -0
- data/spec/aker/configuration_spec.rb +465 -0
- data/spec/aker/deprecation_spec.rb +115 -0
- data/spec/aker/form/a_form_mode.rb +129 -0
- data/spec/aker/form/custom_views_mode_spec.rb +34 -0
- data/spec/aker/form/login_form_asset_provider_spec.rb +80 -0
- data/spec/aker/form/middleware/a_form_login_responder.rb +89 -0
- data/spec/aker/form/middleware/custom_view_login_responder_spec.rb +47 -0
- data/spec/aker/form/middleware/login_renderer_spec.rb +56 -0
- data/spec/aker/form/middleware/login_responder_spec.rb +34 -0
- data/spec/aker/form/middleware/logout_responder_spec.rb +55 -0
- data/spec/aker/form/mode_spec.rb +15 -0
- data/spec/aker/form_spec.rb +11 -0
- data/spec/aker/group_membership_spec.rb +208 -0
- data/spec/aker/group_spec.rb +66 -0
- data/spec/aker/ldap/authority_spec.rb +414 -0
- data/spec/aker/ldap/ldap-users.ldif +197 -0
- data/spec/aker/ldap_spec.rb +11 -0
- data/spec/aker/modes/a_aker_mode.rb +41 -0
- data/spec/aker/modes/http_basic_spec.rb +127 -0
- data/spec/aker/modes/support/attempted_path_spec.rb +32 -0
- data/spec/aker/modes_spec.rb +11 -0
- data/spec/aker/rack/authenticate_spec.rb +78 -0
- data/spec/aker/rack/default_logout_responder_spec.rb +67 -0
- data/spec/aker/rack/facade_spec.rb +154 -0
- data/spec/aker/rack/failure_spec.rb +151 -0
- data/spec/aker/rack/logout_spec.rb +63 -0
- data/spec/aker/rack/request_ext_spec.rb +29 -0
- data/spec/aker/rack/session_timer_spec.rb +134 -0
- data/spec/aker/rack/setup_spec.rb +87 -0
- data/spec/aker/rack_spec.rb +216 -0
- data/spec/aker/test/helpers_spec.rb +44 -0
- data/spec/aker/user_spec.rb +362 -0
- data/spec/aker_spec.rb +80 -0
- data/spec/deprecation_helper.rb +58 -0
- data/spec/java_helper.rb +5 -0
- data/spec/logger_helper.rb +17 -0
- data/spec/matchers.rb +31 -0
- data/spec/mock_builder.rb +25 -0
- data/spec/spec_helper.rb +52 -0
- metadata +265 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require File.expand_path("../../../../spec_helper", __FILE__)
|
|
2
|
+
require "rack/test"
|
|
3
|
+
|
|
4
|
+
module Aker::Cas::Middleware
|
|
5
|
+
describe LogoutResponder do
|
|
6
|
+
include Rack::Test::Methods
|
|
7
|
+
|
|
8
|
+
let(:app) do
|
|
9
|
+
Rack::Builder.new do
|
|
10
|
+
use Aker::Cas::Middleware::LogoutResponder
|
|
11
|
+
run lambda { |env| [404, { "Content-Type" => "text/plain" }, []] }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:configuration) do
|
|
16
|
+
Aker::Configuration.new do
|
|
17
|
+
cas_parameters :logout_url => 'https://cas.example.edu/logout'
|
|
18
|
+
rack_parameters :logout_path => '/some/logout'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:env) do
|
|
23
|
+
{ 'aker.configuration' => configuration }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#call' do
|
|
27
|
+
it 'redirects to the CAS logout URL on GET {configured logout path}' do
|
|
28
|
+
get "/some/logout", {}, env
|
|
29
|
+
|
|
30
|
+
last_response.should be_redirect
|
|
31
|
+
last_response.location.should == 'https://cas.example.edu/logout'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "does not respond to other paths" do
|
|
35
|
+
get "/", {}, env
|
|
36
|
+
|
|
37
|
+
last_response.status.should == 404
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "does not respond to other methods" do
|
|
41
|
+
post "/some/logout", {}, env
|
|
42
|
+
|
|
43
|
+
last_response.status.should == 404
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require File.expand_path("../../../../spec_helper", __FILE__)
|
|
2
|
+
require "rack/test"
|
|
3
|
+
|
|
4
|
+
module Aker::Cas::Middleware
|
|
5
|
+
describe TicketRemover do
|
|
6
|
+
include Rack::Test::Methods
|
|
7
|
+
|
|
8
|
+
let(:app) do
|
|
9
|
+
Rack::Builder.new do
|
|
10
|
+
use Aker::Cas::Middleware::TicketRemover
|
|
11
|
+
run lambda { |env| [404, { "Content-Type" => "text/plain" }, ['Requested content']] }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:env) do
|
|
16
|
+
{ }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '#call' do
|
|
20
|
+
it 'does nothing if not authenticated' do
|
|
21
|
+
get '/foo?ticket=ST-45&q=bar', {}, env
|
|
22
|
+
|
|
23
|
+
last_response.body.should == 'Requested content'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'does nothing if no ticket is present' do
|
|
27
|
+
get '/foo?q=bar', {}, env
|
|
28
|
+
|
|
29
|
+
last_response.body.should == 'Requested content'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'ticket is present and the user is authenticated' do
|
|
33
|
+
before do
|
|
34
|
+
env['aker.check'] = Aker::Rack::Facade.new(Aker.configuration, Aker::User.new('jo'))
|
|
35
|
+
|
|
36
|
+
get '/foo?ticket=ST-45&q=bar', {}, env
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'sends a permanent redirect' do
|
|
40
|
+
last_response.status.should == 301
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'redirects to the same URI without the ticket' do
|
|
44
|
+
last_response.headers['Location'].should == 'http://example.org/foo?q=bar'
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require File.expand_path("../../../spec_helper", __FILE__)
|
|
2
|
+
require File.expand_path("../../modes/a_aker_mode", __FILE__)
|
|
3
|
+
require 'rack'
|
|
4
|
+
|
|
5
|
+
module Aker::Cas
|
|
6
|
+
describe ProxyMode do
|
|
7
|
+
before do
|
|
8
|
+
@env = Rack::MockRequest.env_for('/')
|
|
9
|
+
@scope = mock
|
|
10
|
+
@mode = ProxyMode.new(@env, @scope)
|
|
11
|
+
@env['aker.configuration'] = Aker::Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it_should_behave_like "a aker mode"
|
|
15
|
+
|
|
16
|
+
describe "#key" do
|
|
17
|
+
it "is :cas_proxy" do
|
|
18
|
+
ProxyMode.key.should == :cas_proxy
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "#kind" do
|
|
23
|
+
it "is :cas_proxy" do
|
|
24
|
+
@mode.kind.should == :cas_proxy
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#credentials" do
|
|
29
|
+
it "finds two credentials" do
|
|
30
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy PT-1foo"
|
|
31
|
+
|
|
32
|
+
@mode.credentials.size.should == 2
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "[0]" do
|
|
36
|
+
it "finds a proxy ticket that starts with PT" do
|
|
37
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy PT-1foo"
|
|
38
|
+
|
|
39
|
+
@mode.credentials.first.should == "PT-1foo"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "finds a proxy ticket that starts with ST" do
|
|
43
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy ST-9936-bam"
|
|
44
|
+
|
|
45
|
+
@mode.credentials.first.should == "ST-9936-bam"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "ignores an out-of-spec proxy ticket" do
|
|
49
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy MyAttackVector"
|
|
50
|
+
|
|
51
|
+
@mode.credentials.should == []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "ignores tickets that contain characters not in [^0-9A-Za-z-]" do
|
|
55
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy ST-@@&%#"
|
|
56
|
+
|
|
57
|
+
@mode.credentials.should == []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "returns an empty array if no proxy ticket is present" do
|
|
61
|
+
@mode.credentials.should == []
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe "[1]" do
|
|
66
|
+
it "is the service_url" do
|
|
67
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy ST-1701"
|
|
68
|
+
|
|
69
|
+
@env["SERVER_PORT"] = 80
|
|
70
|
+
@env["SERVER_HOST"] = "example.org"
|
|
71
|
+
@env["rack.url_scheme"] = "http"
|
|
72
|
+
|
|
73
|
+
@mode.credentials[1].should == "http://example.org"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "#valid?" do
|
|
79
|
+
it "returns false if there is no authorization header" do
|
|
80
|
+
@mode.should_not be_valid
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "returns false if the authorization header is for a different scheme" do
|
|
84
|
+
@env["HTTP_AUTHORIZATION"] = "Basic " + Base64.encode64("bas:baz")
|
|
85
|
+
|
|
86
|
+
@mode.should_not be_valid
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "returns true if the authorization header is for CasProxy" do
|
|
90
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy PT-5-256a"
|
|
91
|
+
|
|
92
|
+
@mode.should be_valid
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#scheme" do
|
|
97
|
+
it "returns CasProxy" do
|
|
98
|
+
@mode.scheme.should == "CasProxy"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe "#challenge" do
|
|
103
|
+
it "includes the scheme and the realm" do
|
|
104
|
+
@mode.challenge.should == "CasProxy realm=\"Aker\""
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "#service_url" do
|
|
109
|
+
before do
|
|
110
|
+
@env["rack.url_scheme"] = "http"
|
|
111
|
+
@env["SERVER_NAME"] = "local.example.net"
|
|
112
|
+
@env["SERVER_PORT"] = "80"
|
|
113
|
+
@env["SCRIPT_NAME"] = "/api"
|
|
114
|
+
@env["PATH_INFO"] = "/people/josephine"
|
|
115
|
+
@env["QUERY_STRING"] = "all=true"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "includes the host and script name but not the path info or query string" do
|
|
119
|
+
@mode.service_url.should == "http://local.example.net/api"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "prefers the HTTP Host header over the server name and port if present" do
|
|
123
|
+
@env["HTTP_HOST"] = "virtual.example.com:3400"
|
|
124
|
+
@env["SERVER_PORT"] = "8080"
|
|
125
|
+
@mode.service_url.should == "http://virtual.example.com:3400/api"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "does not end with a slash if the script name is blank" do
|
|
129
|
+
@env["SCRIPT_NAME"] = ""
|
|
130
|
+
@mode.service_url.should == "http://local.example.net"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
describe "for http" do
|
|
134
|
+
it "does not include the port if it is 80" do
|
|
135
|
+
@mode.service_url.should == "http://local.example.net/api"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "includes the port if it isn't 80" do
|
|
139
|
+
@env["SERVER_PORT"] = "3000"
|
|
140
|
+
@mode.service_url.should == "http://local.example.net:3000/api"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe "for https" do
|
|
145
|
+
before do
|
|
146
|
+
@env['rack.url_scheme'] = "https"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "does not include the port if it is 443" do
|
|
150
|
+
@env["SERVER_PORT"] = "443"
|
|
151
|
+
@mode.service_url.should == "https://local.example.net/api"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "includes the port if it isn't 443" do
|
|
155
|
+
@env["SERVER_PORT"] = "3003"
|
|
156
|
+
@mode.service_url.should == "https://local.example.net:3003/api"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe "#authenticate!" do
|
|
162
|
+
before do
|
|
163
|
+
@authority = mock
|
|
164
|
+
@mode.stub!(:authority => @authority)
|
|
165
|
+
@env["HTTP_AUTHORIZATION"] = "CasProxy PT-1foo"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "signals success if the proxy ticket is good" do
|
|
169
|
+
user = stub
|
|
170
|
+
@authority.should_receive(:valid_credentials?).
|
|
171
|
+
with(:cas_proxy, "PT-1foo", "http://example.org").and_return(user)
|
|
172
|
+
@mode.should_receive(:success!).with(user)
|
|
173
|
+
|
|
174
|
+
@mode.authenticate!
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "does not signal success if the proxy ticket is bad" do
|
|
178
|
+
@authority.stub!(:valid_credentials? => nil)
|
|
179
|
+
@mode.should_not_receive(:success!)
|
|
180
|
+
|
|
181
|
+
@mode.authenticate!
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
|
2
|
+
require 'rack'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Aker::Cas
|
|
6
|
+
shared_examples_for "rack proxy callback handler" do
|
|
7
|
+
describe "/receive_pgt" do
|
|
8
|
+
describe "with both a pgtId and pgtIou" do
|
|
9
|
+
before do
|
|
10
|
+
@response = Rack::MockRequest.new(@app).
|
|
11
|
+
get("/receive_pgt?pgtId=PGT-baz&pgtIou=PGTIOU-foo")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "stores the pair" do
|
|
15
|
+
pstore = PStore.new(@store_filename)
|
|
16
|
+
pstore.transaction do
|
|
17
|
+
pstore["PGTIOU-foo"].should == "PGT-baz"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "returns success" do
|
|
22
|
+
@response.status.should == 200
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "gives a worthwhile message" do
|
|
26
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
27
|
+
@response.body.should =~ /PGT and PGTIOU received./
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "without a pgtId" do
|
|
32
|
+
before do
|
|
33
|
+
@response = Rack::MockRequest.new(@app).
|
|
34
|
+
get("/receive_pgt?pgtIou=PGTIOU-foo")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "is a bad request" do
|
|
38
|
+
@response.status.should == 400
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "gives a worthwile error message" do
|
|
42
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
43
|
+
@response.body.should ==
|
|
44
|
+
"pgtId is a required query parameter." <<
|
|
45
|
+
"\nSee section 2.5.4 of the CAS protocol specification."
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "without a pgtIou" do
|
|
50
|
+
before do
|
|
51
|
+
@response = Rack::MockRequest.new(@app).
|
|
52
|
+
get("/receive_pgt?pgtId=PGT-foo")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "is a bad request" do
|
|
56
|
+
@response.status.should == 400
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "gives a worthwile error message" do
|
|
60
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
61
|
+
@response.body.should ==
|
|
62
|
+
"pgtIou is a required query parameter." <<
|
|
63
|
+
"\nSee section 2.5.4 of the CAS protocol specification."
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "without either" do
|
|
68
|
+
before do
|
|
69
|
+
@response = Rack::MockRequest.new(@app).
|
|
70
|
+
get("/receive_pgt")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "is OK, because the JA-SIG CAS server expects it to be" do
|
|
74
|
+
@response.status.should == 200
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "gives a worthwile error message" do
|
|
78
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
79
|
+
@response.body.should ==
|
|
80
|
+
"Both pgtId and pgtIou are required query parameters." <<
|
|
81
|
+
"\nSee section 2.5.4 of the CAS protocol specification."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "/retrieve_pgt" do
|
|
87
|
+
describe "with a known pgtIou" do
|
|
88
|
+
before do
|
|
89
|
+
pstore = PStore.new(@store_filename)
|
|
90
|
+
pstore.transaction do
|
|
91
|
+
pstore["PGTIOU-678"] = "PGT-876"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@response = Rack::MockRequest.new(@app).
|
|
95
|
+
get("/retrieve_pgt?pgtIou=PGTIOU-678")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "is successful" do
|
|
99
|
+
@response.status.should == 200
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "returns the PGT" do
|
|
103
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
104
|
+
@response.body.should == "PGT-876"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "deletes the PGT from the store once it has been retrieved" do
|
|
108
|
+
pstore = PStore.new(@store_filename)
|
|
109
|
+
pstore.transaction do
|
|
110
|
+
pstore["PGTIOU-678"].should be_nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "with an unknown pgtIou" do
|
|
116
|
+
before do
|
|
117
|
+
@response = Rack::MockRequest.new(@app).
|
|
118
|
+
get("/retrieve_pgt?pgtIou=PGTIOU-1234")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "returns 404" do
|
|
122
|
+
@response.status.should == 404
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "provides a helpful message" do
|
|
126
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
127
|
+
@response.body.should ==
|
|
128
|
+
"pgtIou=PGTIOU-1234 does not exist. Perhaps it has already been retrieved."
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe "without a pgtIou" do
|
|
133
|
+
before do
|
|
134
|
+
@response = Rack::MockRequest.new(@app).
|
|
135
|
+
get("/retrieve_pgt")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "is a bad request" do
|
|
139
|
+
@response.status.should == 400
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "provides a worthwhile error message" do
|
|
143
|
+
@response.headers["Content-Type"].should == "text/plain"
|
|
144
|
+
@response.body.should ==
|
|
145
|
+
"pgtIou is a required query parameter."
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe RackProxyCallback do
|
|
152
|
+
before do
|
|
153
|
+
@store_filename = "#{tmpdir}/#{File.basename(__FILE__)}.pstore"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
after do
|
|
157
|
+
FileUtils.rm @store_filename if File.exist?(@store_filename)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe "as middleware" do
|
|
161
|
+
before do
|
|
162
|
+
endpoint = lambda { |env| [200, {}, ["App invoked"]] }
|
|
163
|
+
@app = RackProxyCallback.new(endpoint, :store => @store_filename)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it_should_behave_like "rack proxy callback handler"
|
|
167
|
+
|
|
168
|
+
it "invokes the app for other paths" do
|
|
169
|
+
Rack::MockRequest.new(@app).get("/foo").should =~ /App invoked/
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "requires a filename for the store" do
|
|
173
|
+
lambda { RackProxyCallback.new(Object.new) }.
|
|
174
|
+
should raise_error(/Please specify a filename for the PGT store/)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe "as an application" do
|
|
179
|
+
before do
|
|
180
|
+
@app = RackProxyCallback.application :store => @store_filename
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it_should_behave_like "rack proxy callback handler"
|
|
184
|
+
|
|
185
|
+
it "404s for non-pgt requests" do
|
|
186
|
+
Rack::MockRequest.new(@app).get("/foo").status.should == 404
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require File.expand_path("../../../spec_helper", __FILE__)
|
|
2
|
+
require File.expand_path("../../modes/a_aker_mode", __FILE__)
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Aker::Cas
|
|
7
|
+
describe ServiceMode do
|
|
8
|
+
before do
|
|
9
|
+
@env = ::Rack::MockRequest.env_for('/')
|
|
10
|
+
@scope = mock
|
|
11
|
+
@mode = ServiceMode.new(@env, @scope)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it_should_behave_like "a aker mode"
|
|
15
|
+
|
|
16
|
+
describe "#key" do
|
|
17
|
+
it "is :cas" do
|
|
18
|
+
ServiceMode.key.should == :cas
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "#valid?" do
|
|
23
|
+
it "returns false if the ST parameter is not in the query string" do
|
|
24
|
+
@mode.should_not be_valid
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "returns true if the ticket parameter is in the query string" do
|
|
28
|
+
@env['QUERY_STRING'] = 'ticket=ST-1foo'
|
|
29
|
+
|
|
30
|
+
@mode.should be_valid
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "#kind" do
|
|
35
|
+
it "is :cas" do
|
|
36
|
+
@mode.kind.should == :cas
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "#credentials" do
|
|
41
|
+
before do
|
|
42
|
+
@env["PATH_INFO"] = "/qu/ux"
|
|
43
|
+
@env["QUERY_STRING"] = "foo=baz"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "when there's a service ticket" do
|
|
47
|
+
before do
|
|
48
|
+
@env["QUERY_STRING"] << "&ticket=ST-1foo"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "returns two elements" do
|
|
52
|
+
@mode.credentials.size.should == 2
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "returns the service ticket first" do
|
|
56
|
+
@mode.credentials[0].should == "ST-1foo"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns the service URL second" do
|
|
60
|
+
@mode.credentials[1].should == "http://example.org/qu/ux?foo=baz"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "returns nil if no service ticket was supplied" do
|
|
65
|
+
@mode.credentials.should be_nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe "#authenticate!" do
|
|
70
|
+
before do
|
|
71
|
+
@authority = mock
|
|
72
|
+
@env['aker.authority'] = @authority
|
|
73
|
+
@env['QUERY_STRING'] = 'ticket=ST-1foo'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "signals success if the service ticket is good" do
|
|
77
|
+
user = stub
|
|
78
|
+
@authority.should_receive(:valid_credentials?).
|
|
79
|
+
with(:cas, 'ST-1foo', "http://example.org/").and_return(user)
|
|
80
|
+
@mode.should_receive(:success!).with(user)
|
|
81
|
+
|
|
82
|
+
@mode.authenticate!
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "does not signal success if the service ticket is bad" do
|
|
86
|
+
@authority.stub!(:valid_credentials? => nil)
|
|
87
|
+
@mode.should_not_receive(:success!)
|
|
88
|
+
|
|
89
|
+
@mode.authenticate!
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe "#on_ui_failure" do
|
|
94
|
+
before do
|
|
95
|
+
@env['aker.configuration'] = Aker::Configuration.new do
|
|
96
|
+
cas_parameters :login_url => 'https://cas.example.edu/login'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "redirects to the CAS server's login page" do
|
|
101
|
+
response = @mode.on_ui_failure
|
|
102
|
+
location = URI.parse(response.location)
|
|
103
|
+
response.should be_redirect
|
|
104
|
+
|
|
105
|
+
location.scheme.should == "https"
|
|
106
|
+
location.host.should == "cas.example.edu"
|
|
107
|
+
location.path.should == "/login"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "includes the service URL" do
|
|
111
|
+
@env["PATH_INFO"] = "/foo?a=b&c=d"
|
|
112
|
+
|
|
113
|
+
actual_uri.query.should == "service=http%3A%2F%2Fexample.org%2Ffoo%3Fa%3Db%26c%3Dd"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def actual_uri
|
|
117
|
+
response = @mode.on_ui_failure
|
|
118
|
+
URI.parse(response.location)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
require 'rack/request'
|
|
4
|
+
require 'rack/mock'
|
|
5
|
+
|
|
6
|
+
module Aker::Cas
|
|
7
|
+
describe ServiceUrl do
|
|
8
|
+
let(:env) { ::Rack::MockRequest.env_for('/') }
|
|
9
|
+
|
|
10
|
+
shared_examples_for '#service_url' do
|
|
11
|
+
it 'is the URL the user was trying to reach' do
|
|
12
|
+
env['PATH_INFO'] = '/qu/ux'
|
|
13
|
+
env['QUERY_STRING'] = 'a=b'
|
|
14
|
+
actual_url.should == 'http://example.org/qu/ux?a=b'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe 'with warden\'s "attempted path" in the environment' do
|
|
18
|
+
before do
|
|
19
|
+
set_attempted_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'uses it if present' do
|
|
23
|
+
env['PATH_INFO'] = '/unauthenticated'
|
|
24
|
+
|
|
25
|
+
actual_url.should == 'http://example.org/foo/quux'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'includes the port if not the default for http' do
|
|
29
|
+
env['rack.url_scheme'] = 'http'
|
|
30
|
+
env['SERVER_PORT'] = 81
|
|
31
|
+
|
|
32
|
+
actual_url.should == 'http://example.org:81/foo/quux'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'includes the port if not the default for https' do
|
|
36
|
+
env['rack.url_scheme'] = 'https'
|
|
37
|
+
env['SERVER_PORT'] = 80
|
|
38
|
+
|
|
39
|
+
actual_url.should == 'https://example.org:80/foo/quux'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "filters out a service ticket that's the sole parameter" do
|
|
44
|
+
env['QUERY_STRING'] = 'ticket=ST-foo'
|
|
45
|
+
actual_url.should == 'http://example.org/'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "filters out a service ticket that's the first parameter of several" do
|
|
49
|
+
env['QUERY_STRING'] = 'ticket=ST-bar&foo=baz'
|
|
50
|
+
actual_url.should == 'http://example.org/?foo=baz'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "filters out a service ticket that's the last parameter of several" do
|
|
54
|
+
env['QUERY_STRING'] = 'foo=baz&ticket=ST-bar'
|
|
55
|
+
actual_url.should == 'http://example.org/?foo=baz'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "filters out a service ticket that's in the middle of several" do
|
|
59
|
+
env['QUERY_STRING'] = 'foo=baz&ticket=ST-bar&zazz=quux'
|
|
60
|
+
actual_url.should == 'http://example.org/?foo=baz&zazz=quux'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context 'when mixed in' do
|
|
65
|
+
it_behaves_like '#service_url'
|
|
66
|
+
|
|
67
|
+
class WithRequest
|
|
68
|
+
include ServiceUrl
|
|
69
|
+
|
|
70
|
+
attr_reader :request, :env
|
|
71
|
+
|
|
72
|
+
def initialize(env)
|
|
73
|
+
@env = env
|
|
74
|
+
@request = Rack::Request.new(env)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class WithAttemptedPath < WithRequest
|
|
79
|
+
include Aker::Modes::Support::AttemptedPath
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
let(:with_request_only) { WithRequest.new(env).service_url }
|
|
83
|
+
let(:with_attempted_path) { WithAttemptedPath.new(env).service_url }
|
|
84
|
+
let(:actual_url) { with_attempted_path }
|
|
85
|
+
|
|
86
|
+
def set_attempted_path
|
|
87
|
+
env['warden.options'] = { :attempted_path => '/foo/quux' }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe 'when the host class does not have an #attempted_path method' do
|
|
91
|
+
it 'does nothing with the attempted path' do
|
|
92
|
+
env['PATH_INFO'] = '/foo/bar'
|
|
93
|
+
set_attempted_path
|
|
94
|
+
with_request_only.should == 'http://example.org/foo/bar'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context 'when called directly' do
|
|
100
|
+
it_behaves_like '#service_url'
|
|
101
|
+
|
|
102
|
+
let(:request) { ::Rack::Request.new(env) }
|
|
103
|
+
let(:actual_url) { ServiceUrl.service_url(request, attempted_path) }
|
|
104
|
+
|
|
105
|
+
def attempted_path
|
|
106
|
+
@attempted_path
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def set_attempted_path
|
|
110
|
+
@attempted_path = '/foo/quux'
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|