gds-sso 21.0.0 → 21.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1cce044b9dcc019806b1a70222fb30c99abce4df707245ae7c4d795b92bba8d7
4
- data.tar.gz: 966b8b0c9ef7b95ee5ec5229c8f087f1870447d4281b1b7b6bcc5fc5627155a6
3
+ metadata.gz: 72938e39cf2466b8a2dc2d22aca03d64ff33ab2a8c5d11c0c57f3710a2935205
4
+ data.tar.gz: 042b5c966919ab345d9ea3dc9e47197947fdd75e51ad263b0f0f038397c09fa9
5
5
  SHA512:
6
- metadata.gz: 3f3fe06419fd98b0018ff60ce33f45fe6a0173ac2c632009f2ece0d821539ae12503ad5f0a85716d2db16811e3e3bff4e234122ebff71a708fbcba13339bee78
7
- data.tar.gz: eb36c389d730f589805c6e17bbdcdbf30f545cd2874431283082a437b0668312e5be7faa062f58149f9e5ea11f653c6ff52e67acace2213ad28f9383896912dc
6
+ metadata.gz: fdca3de72981e79aa420da0b3d6c4d689fba034d3d3cda4874082efe58bcf056128242ab09a4266b8d11f91da500d47b665e434ccb805a3f6a799d6d29f765f4
7
+ data.tar.gz: ad252eb2aeaf3986b68b54a15f7f4622f4d56e730d509690e1e80c77be6411201ebf618ba68efebaff825fe731a111de186b439b6834a4c1de91782745658335
data/config/routes.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  Rails.application.routes.draw do
2
+ put "/auth/gds/api/users/:uid", to: "api/user#update"
3
+ post "/auth/gds/api/users/:uid/reauth", to: "api/user#reauth"
4
+
2
5
  next if GDS::SSO::Config.api_only
3
6
 
4
7
  get "/auth/gds/callback", to: "authentications#callback", as: :gds_sign_in
5
8
  get "/auth/gds/sign_out", to: "authentications#sign_out", as: :gds_sign_out
6
- get "/auth/failure", to: "authentications#failure", as: :auth_failure
7
- put "/auth/gds/api/users/:uid", to: "api/user#update"
8
- post "/auth/gds/api/users/:uid/reauth", to: "api/user#reauth"
9
+ get "/auth/failure", to: "authentications#failure", as: :auth_failure
9
10
  end
@@ -8,7 +8,12 @@ module GDS
8
8
  return true if GDS::SSO::Config.api_only
9
9
 
10
10
  if GDS::SSO::Config.api_request_matcher
11
- return GDS::SSO::Config.api_request_matcher.call(Rack::Request.new(env))
11
+ request = Rack::Request.new(env)
12
+
13
+ gds_sso_api_request_matcher = GDS::SSO::Config.gds_sso_api_request_matcher
14
+ return true if gds_sso_api_request_matcher&.call(request)
15
+
16
+ return GDS::SSO::Config.api_request_matcher.call(request)
12
17
  end
13
18
 
14
19
  !bearer_token(env).nil?
@@ -58,6 +58,7 @@ module GDS
58
58
 
59
59
  module MockBearerToken
60
60
  def self.locate(_token_string)
61
+ return unless ENV["GDS_SSO_MOCK_INVALID"].to_s.empty?
61
62
  return GDS::SSO.test_user if GDS::SSO.test_user
62
63
 
63
64
  dummy_api_user = GDS::SSO::Config.user_klass.where(email: "dummyapiuser@domain.com").first
@@ -31,6 +31,11 @@ module GDS
31
31
 
32
32
  mattr_accessor :api_request_matcher
33
33
 
34
+ # A matcher for GDS SSO's own API for updating user details. Shouldn't
35
+ # need configuring unless you mount the API at a different location.
36
+ mattr_accessor :gds_sso_api_request_matcher
37
+ @@gds_sso_api_request_matcher = ->(request) { request.path.start_with?("/auth/gds/api/") }
38
+
34
39
  mattr_accessor :intercept_401_responses
35
40
  @@intercept_401_responses = true
36
41
 
@@ -1,5 +1,5 @@
1
1
  module GDS
2
2
  module SSO
3
- VERSION = "21.0.0".freeze
3
+ VERSION = "21.1.0".freeze
4
4
  end
5
5
  end
@@ -5,7 +5,7 @@ Rails.application.routes.draw do
5
5
  get "/restricted" => "example#restricted"
6
6
  get "/this-requires-execute-permission" => "example#this_requires_execute_permission"
7
7
 
8
- constraints(GDS::SSO::AuthorisedUserConstraint.new("execute")) do
8
+ constraints(GDS::SSO::AuthorisedUserConstraint.new("constraint")) do
9
9
  get "/constraint-restricted" => "example#constraint_restricted"
10
10
  end
11
11
  end
@@ -0,0 +1,144 @@
1
+ require "spec_helper"
2
+
3
+ describe "Api::UserController", type: :request do
4
+ shared_examples "rejects a request from an unauthenticated user" do |method, path|
5
+ it "rejects the request when a user is unauthenticated" do
6
+ stub_failed_signon_user_request
7
+ public_send(method, path, headers: { "Authorization" => "Bearer anything" })
8
+ expect(response).to have_http_status(:unauthorized)
9
+ end
10
+ end
11
+
12
+ shared_examples "rejects a request from an authenticated user lacking permission" do |method, path|
13
+ it "rejects the request when a user is authenticated but lacking permission" do
14
+ stub_successful_signon_user_request(permissions: [])
15
+ public_send(method, path, headers: { "Authorization" => "Bearer anything" })
16
+ expect(response).to have_http_status(:forbidden)
17
+ end
18
+ end
19
+
20
+ shared_examples "operates as an API endpoint if api_request_matcher doesn't match it" do |method, path|
21
+ it "doesn't redirect to /auth/gds because it's recognised as an API request" do
22
+ allow(GDS::SSO::Config)
23
+ .to receive(:api_request_matcher)
24
+ .and_return(->(_request) { false })
25
+
26
+ stub_failed_signon_user_request
27
+ public_send(method, path, headers: { "Authorization" => "Bearer anything" })
28
+ expect(response.media_type).to eq("application/json")
29
+ end
30
+ end
31
+
32
+ shared_examples "redirects to /auth/gds if gds_sso_api_request_matcher is configured to not match" do |method, path|
33
+ it "fails if gds_sso_api_request_matcher is configured to not match" do
34
+ allow(GDS::SSO::Config)
35
+ .to receive(:api_request_matcher)
36
+ .and_return(->(_request) { false })
37
+
38
+ allow(GDS::SSO::Config)
39
+ .to receive(:gds_sso_api_request_matcher)
40
+ .and_return(nil)
41
+
42
+ public_send(method, path, headers: { "Authorization" => "Bearer anything" })
43
+ expect(response).to redirect_to("/auth/gds")
44
+ end
45
+ end
46
+
47
+ describe "PUT /auth/gds/api/users/:uid" do
48
+ shared_params = [:put, "/auth/gds/api/users/#{SecureRandom.uuid}"]
49
+ it_behaves_like "rejects a request from an unauthenticated user", *shared_params
50
+ it_behaves_like "rejects a request from an authenticated user lacking permission", *shared_params
51
+ it_behaves_like "operates as an API endpoint if api_request_matcher doesn't match it", *shared_params
52
+ it_behaves_like "redirects to /auth/gds if gds_sso_api_request_matcher is configured to not match", *shared_params
53
+
54
+ it "updates an existing user" do
55
+ stub_successful_signon_user_request(permissions: %w[user_update_permission])
56
+
57
+ user = User.create!(
58
+ uid: SecureRandom.uuid,
59
+ email: "user@example.com",
60
+ name: "Example User",
61
+ permissions: [],
62
+ )
63
+
64
+ put "/auth/gds/api/users/#{user.uid}",
65
+ headers: { "Authorization" => "Bearer anything" },
66
+ params: user_update_params(user, { "name" => "John Matrix" }),
67
+ as: :json
68
+
69
+ expect(response).to have_http_status(:success)
70
+ expect(response.body).to eq("")
71
+ expect(user.reload.name).to eq("John Matrix")
72
+ end
73
+
74
+ it "creates a new user if a user does not exist" do
75
+ stub_successful_signon_user_request(permissions: %w[user_update_permission])
76
+
77
+ user = User.new(
78
+ uid: SecureRandom.uuid,
79
+ email: "user@example.com",
80
+ name: "Example User",
81
+ permissions: [],
82
+ )
83
+
84
+ put "/auth/gds/api/users/#{user.uid}",
85
+ headers: { "Authorization" => "Bearer anything" },
86
+ params: user_update_params(user),
87
+ as: :json
88
+
89
+ expect(response).to have_http_status(:success)
90
+ expect(response.body).to eq("")
91
+ expect(User.last.uid).to eq(user.uid)
92
+ end
93
+ end
94
+
95
+ describe "POST /auth/gds/api/users/:uid/reauth" do
96
+ shared_params = [:post, "/auth/gds/api/users/#{SecureRandom.uuid}/reauth"]
97
+ it_behaves_like "rejects a request from an unauthenticated user", *shared_params
98
+ it_behaves_like "rejects a request from an authenticated user lacking permission", *shared_params
99
+ it_behaves_like "operates as an API endpoint if api_request_matcher doesn't match it", *shared_params
100
+ it_behaves_like "redirects to /auth/gds if gds_sso_api_request_matcher is configured to not match", *shared_params
101
+
102
+ it "flags a user that exists as remotely signed out" do
103
+ stub_successful_signon_user_request(permissions: %w[user_update_permission])
104
+
105
+ user = User.create!(
106
+ uid: SecureRandom.uuid,
107
+ email: "user@example.com",
108
+ name: "Example User",
109
+ permissions: [],
110
+ )
111
+
112
+ expect {
113
+ post "/auth/gds/api/users/#{user.uid}/reauth",
114
+ headers: { "Authorization" => "Bearer anything" }
115
+ }.to change { user.reload.remotely_signed_out }.to(true)
116
+
117
+ expect(response).to have_http_status(:success)
118
+ expect(response.body).to eq("")
119
+ end
120
+
121
+ it "responds successfully even if the user doesn't exist" do
122
+ stub_successful_signon_user_request(permissions: %w[user_update_permission])
123
+
124
+ user = User.new(
125
+ uid: SecureRandom.uuid,
126
+ email: "user@example.com",
127
+ name: "Example User",
128
+ permissions: [],
129
+ )
130
+
131
+ post "/auth/gds/api/users/#{user.uid}/reauth",
132
+ headers: { "Authorization" => "Bearer anything" }
133
+
134
+ expect(response).to have_http_status(:success)
135
+ expect(response.body).to eq("")
136
+ end
137
+ end
138
+
139
+ def user_update_params(user, modifications = {})
140
+ fields = %i[uid name email permissions organisation_slug organisation_content_id disabled]
141
+ user_details = user.as_json(only: fields).merge(modifications)
142
+ { "user" => user_details }
143
+ end
144
+ end
@@ -0,0 +1,77 @@
1
+ require "spec_helper"
2
+
3
+ describe "AuthenticationController", type: :request do
4
+ describe "GET /auth/gds/callback" do
5
+ it "fails without a valid state param" do
6
+ get "/auth/gds/callback"
7
+
8
+ expect(response).to redirect_to("/auth/failure?message=csrf_detected&strategy=gds")
9
+ end
10
+
11
+ it "redirects to the attempted url if a user was restricted earlier in the session" do
12
+ get "/restricted"
13
+
14
+ state = request_to_establish_oauth_state
15
+
16
+ stub_signon_oauth_token_request
17
+ stub_successful_signon_user_request
18
+
19
+ get "/auth/gds/callback?state=#{state}"
20
+ expect(response).to redirect_to("/restricted")
21
+ end
22
+
23
+ it "redirects to the root path if the user hadn't tried to access a restricted url" do
24
+ state = request_to_establish_oauth_state
25
+
26
+ stub_signon_oauth_token_request
27
+ stub_successful_signon_user_request
28
+
29
+ get "/auth/gds/callback?state=#{state}"
30
+ expect(response).to redirect_to("/")
31
+ end
32
+
33
+ it "uses the OAuth2 proof key for code exchange feature for increased security" do
34
+ get "/auth/gds"
35
+
36
+ expect(response).to have_http_status(:redirect)
37
+ location = URI.parse(response.location)
38
+ query = Rack::Utils.parse_query(location.query)
39
+
40
+ expect(location.path).to eq("/oauth/authorize")
41
+ expect(query).to include("code_challenge", "code_challenge_method")
42
+
43
+ token_request = stub_request(:post, "http://signon/oauth/access_token")
44
+ .with(body: hash_including("code_verifier"))
45
+
46
+ get "/auth/gds/callback?state=#{query['state']}"
47
+
48
+ expect(token_request).to have_been_made
49
+ end
50
+ end
51
+
52
+ describe "GET /auth/failure" do
53
+ it "responds successfully" do
54
+ get "/auth/failure"
55
+
56
+ expect(response).to have_http_status(:success)
57
+ end
58
+ end
59
+
60
+ describe "GET /auth/gds/sign_out" do
61
+ it "redirects to signon sign out" do
62
+ get "/auth/gds/sign_out"
63
+
64
+ expect(response).to redirect_to("http://signon/users/sign_out")
65
+ end
66
+
67
+ it "logs an authenticated user out" do
68
+ authenticate_with_stub_signon
69
+
70
+ get "/auth/gds/sign_out"
71
+
72
+ # access a restricted route to assert we're logged out
73
+ get "/restricted"
74
+ expect(response).to redirect_to("/auth/gds")
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,264 @@
1
+ require "spec_helper"
2
+
3
+ describe "Integration tests with a demo app", type: :request do
4
+ describe "accessing a route that doesn't require authentication" do
5
+ it "allows access" do
6
+ get "/not-restricted"
7
+ expect(response).to have_http_status(:success)
8
+ expect(response.body).to eq("jabberwocky")
9
+ end
10
+ end
11
+
12
+ describe "accessing a route the requires authentication" do
13
+ context "when GDS::SSO isn't configured to treat this as an explicit API request" do
14
+ it "redirects an unauthenticated request to sign-in" do
15
+ get "/restricted"
16
+ expect(response).to redirect_to("/auth/gds")
17
+ end
18
+
19
+ it "responds successfully for an authenticated user" do
20
+ authenticate_with_stub_signon
21
+
22
+ get "/restricted"
23
+ expect(response).to have_http_status(:success)
24
+ expect(response.body).to eq("restricted kablooie")
25
+ end
26
+
27
+ it "redirects to sign-in if a user is authenticated but remotely signed out" do
28
+ authenticate_with_stub_signon
29
+ User.last.set_remotely_signed_out!
30
+
31
+ get "/restricted"
32
+ expect(response).to redirect_to("/auth/gds")
33
+ end
34
+
35
+ it "redirects to sign-in if a user's session has expired" do
36
+ authenticate_with_stub_signon
37
+
38
+ travel_to(Time.now.utc + GDS::SSO::Config.auth_valid_for + 1.second) do
39
+ get "/restricted"
40
+ expect(response).to redirect_to("/auth/gds")
41
+ end
42
+ end
43
+
44
+ it "allows access when given a valid bearer token" do
45
+ stub_successful_signon_user_request
46
+
47
+ get "/restricted", headers: { "Authorization" => "Bearer 123" }
48
+ expect(response).to have_http_status(:success)
49
+ expect(response.body).to eq("restricted kablooie")
50
+ end
51
+
52
+ it "restricts access when given an invalid bearer token" do
53
+ stub_failed_signon_user_request
54
+
55
+ get "/restricted", headers: { "Authorization" => "Bearer invalid" }
56
+ expect(response).to have_http_status(:unauthorized)
57
+ expect_invalid_bearer_token_response(response)
58
+ end
59
+ end
60
+
61
+ context "when an application is configured as API only" do
62
+ before { allow(GDS::SSO::Config).to receive(:api_only).and_return(true) }
63
+
64
+ it "allows access when given a valid bearer token" do
65
+ stub_successful_signon_user_request
66
+
67
+ get "/restricted", headers: { "Authorization" => "Bearer 123" }
68
+ expect(response).to have_http_status(:success)
69
+ expect(response.body).to eq("restricted kablooie")
70
+ end
71
+
72
+ it "rejects a request without a bearer token" do
73
+ get "/restricted"
74
+ expect(response).to have_http_status(:unauthorized)
75
+ expect_missing_bearer_token_response(response)
76
+ end
77
+
78
+ it "restricts access for an invalid bearer token" do
79
+ stub_failed_signon_user_request
80
+
81
+ get "/restricted", headers: { "Authorization" => "Bearer invalid" }
82
+ expect(response).to have_http_status(:unauthorized)
83
+ expect_invalid_bearer_token_response(response)
84
+ end
85
+ end
86
+
87
+ context "when API requests are differentiated by api_request_matcher" do
88
+ it "treats a match as an API request" do
89
+ allow(GDS::SSO::Config)
90
+ .to receive(:api_request_matcher)
91
+ .and_return(->(request) { request.path == "/restricted" })
92
+
93
+ get "/restricted"
94
+ expect(response).to have_http_status(:unauthorized)
95
+ expect_missing_bearer_token_response(response)
96
+ end
97
+
98
+ it "treats a non-match as a non-API request" do
99
+ allow(GDS::SSO::Config)
100
+ .to receive(:api_request_matcher)
101
+ .and_return(->(_request) { false })
102
+
103
+ get "/restricted"
104
+ expect(response).to redirect_to("/auth/gds")
105
+ end
106
+ end
107
+ end
108
+
109
+ describe "when accessing routes without authentication and using the mock strategies" do
110
+ before { use_mock_strategies }
111
+
112
+ it "allows a user access without authentication" do
113
+ # non-bearer token mock requests require a user to exist
114
+ User.create!(
115
+ uid: SecureRandom.uuid,
116
+ email: "user@example.com",
117
+ name: "Example User",
118
+ permissions: [],
119
+ )
120
+
121
+ get "/restricted"
122
+ expect(response).to have_http_status(:success)
123
+ expect(response.body).to eq("restricted kablooie")
124
+ end
125
+
126
+ it "can be configured to fail authentication with an env var" do
127
+ ClimateControl.modify("GDS_SSO_MOCK_INVALID" => "1") do
128
+ get "/restricted"
129
+ expect(response).to redirect_to("/auth/gds")
130
+ end
131
+ end
132
+
133
+ it "allows an API request without a bearer token" do
134
+ allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
135
+
136
+ get "/restricted"
137
+ expect(response).to have_http_status(:success)
138
+ expect(response.body).to eq("restricted kablooie")
139
+ end
140
+
141
+ it "can be configured to fail API authentication with an env var" do
142
+ allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
143
+
144
+ ClimateControl.modify("GDS_SSO_MOCK_INVALID" => "1") do
145
+ get "/restricted"
146
+ expect(response).to have_http_status(:unauthorized)
147
+ end
148
+ end
149
+ end
150
+
151
+ describe "when accessing a route that requires permission" do
152
+ context "when GDS::SSO isn't configured to treat this as an explicit API request" do
153
+ it "allows a user with the permission to access the resource" do
154
+ authenticate_with_stub_signon(permissions: %w[execute])
155
+
156
+ get "/this-requires-execute-permission"
157
+ expect(response).to have_http_status(:success)
158
+ expect(response.body).to eq("you have execute permission")
159
+ end
160
+
161
+ it "restricts a user lacking the permission" do
162
+ authenticate_with_stub_signon
163
+
164
+ get "/this-requires-execute-permission"
165
+ expect(response).to have_http_status(:forbidden)
166
+ expect(response.body).to include("Sorry, you don't seem to have the execute permission for this app.")
167
+ end
168
+ end
169
+
170
+ context "when GDS::SSO is configured to treat the request as an API request" do
171
+ before { allow(GDS::SSO::Config).to receive(:api_only).and_return(true) }
172
+
173
+ it "allows a user with the permission to access the resource" do
174
+ stub_successful_signon_user_request(permissions: %w[execute])
175
+
176
+ get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
177
+ expect(response).to have_http_status(:success)
178
+ expect(response.body).to eq("you have execute permission")
179
+ end
180
+
181
+ it "restricts a user lacking the permission" do
182
+ stub_successful_signon_user_request
183
+
184
+ get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
185
+ expect(response).to have_http_status(:forbidden)
186
+ expect_json_response(response, { "message" => "Sorry, you don't seem to have the execute permission for this app." })
187
+ end
188
+ end
189
+
190
+ context "when using bearer token for auth with the mock strategy" do
191
+ before { use_mock_strategies }
192
+
193
+ it "automatically grants permissions configured in GDS:SSO::Config.additional_mock_permissions_required" do
194
+ allow(GDS::SSO::Config).to receive(:additional_mock_permissions_required).and_return(%w[execute])
195
+
196
+ stub_successful_signon_user_request
197
+
198
+ get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
199
+ expect(response).to have_http_status(:success)
200
+ expect(response.body).to eq("you have execute permission")
201
+ end
202
+
203
+ it "doesn't grant access without that config" do
204
+ allow(GDS::SSO::Config).to receive(:additional_mock_permissions_required).and_return(nil)
205
+
206
+ stub_successful_signon_user_request
207
+
208
+ get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
209
+ expect(response).to have_http_status(:forbidden)
210
+ expect_json_response(response, { "message" => "Sorry, you don't seem to have the execute permission for this app." })
211
+ end
212
+ end
213
+ end
214
+
215
+ context "when accessing a route that is restricted by the authorised user constraint" do
216
+ it "allows access when an authenticated user has correct permissions" do
217
+ authenticate_with_stub_signon(permissions: %w[constraint])
218
+
219
+ get "/constraint-restricted"
220
+ expect(response).to have_http_status(:success)
221
+ expect(response.body).to eq("constraint restricted")
222
+ end
223
+
224
+ it "redirects an unauthenticated request to signon" do
225
+ get "/constraint-restricted"
226
+
227
+ expect(response).to redirect_to("/auth/gds")
228
+ end
229
+
230
+ it "restricts access when an authenticated user does not have the correct permissions" do
231
+ authenticate_with_stub_signon
232
+
233
+ get "/constraint-restricted"
234
+ expect(response).to have_http_status(:forbidden)
235
+ end
236
+ end
237
+
238
+ def expect_missing_bearer_token_response(response)
239
+ expect(response.headers).to include("WWW-Authenticate" => 'Bearer error="invalid_request"')
240
+ expect_json_response(response, { "message" => "No bearer token was provided" })
241
+ end
242
+
243
+ def expect_invalid_bearer_token_response(response)
244
+ expect(response.headers).to include("WWW-Authenticate" => 'Bearer error="invalid_token"')
245
+ expect_json_response(response, { "message" => "Bearer token does not appear to be valid" })
246
+ end
247
+
248
+ def expect_json_response(response, json)
249
+ expect(response.media_type).to eq("application/json")
250
+ expect(response.parsed_body).to eq(json)
251
+ end
252
+
253
+ def use_mock_strategies
254
+ # Using allow_any_instance_of because it's hard to access the instance
255
+ # of the class used within the Rails middleware
256
+ allow_any_instance_of(Warden::Config).to receive(:[]).and_call_original
257
+ allow_any_instance_of(Warden::Config)
258
+ .to receive(:[])
259
+ .with(:default_strategies)
260
+ .and_return({ _all: %i[mock_gds_sso gds_bearer_token] })
261
+
262
+ allow(Warden::OAuth2.config).to receive(:token_model).and_return(GDS::SSO::MockBearerToken)
263
+ end
264
+ end
data/spec/spec_helper.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  # Bad things happen if we don't ;-)
3
3
  ENV["GDS_SSO_STRATEGY"] = "real"
4
4
 
5
- require "capybara/rspec"
5
+ require "climate_control"
6
6
  require "webmock/rspec"
7
7
  require "combustion"
8
8
 
@@ -12,15 +12,10 @@ Combustion.initialize! :all do
12
12
  end
13
13
 
14
14
  require "rspec/rails"
15
- require "capybara/rails"
16
15
  WebMock.disable_net_connect!
17
16
 
18
17
  Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].sort.each { |f| require f }
19
18
 
20
- Capybara.register_driver :rack_test do |app|
21
- Capybara::RackTest::Driver.new(app, follow_redirects: false)
22
- end
23
-
24
19
  RSpec.configure do |config|
25
20
  config.run_all_when_everything_filtered = true
26
21
  config.filter_run :focus
@@ -32,6 +27,20 @@ RSpec.configure do |config|
32
27
  # --seed 1234
33
28
  config.order = "random"
34
29
 
30
+ config.include ActiveSupport::Testing::TimeHelpers
35
31
  config.include Warden::Test::Helpers
36
- config.include Capybara::DSL
32
+ config.include RequestHelpers, type: :request
33
+ config.before(:each, type: :request) do
34
+ # we reload routes each test as GDS::SSO::Config affects what routes are
35
+ # available, we only want to run this once routes are loaded otherwise
36
+ # we can lose app routes
37
+ routes_reloader = Rails.application.routes_reloader
38
+
39
+ # Routes changed in Rails 8 to be lazily loaded so this wasn't a problem
40
+ # before Rails 8.
41
+ # TODO: remove this line once Rails 7 support is removed
42
+ next unless routes_reloader.respond_to?(:loaded)
43
+
44
+ routes_reloader.reload! if routes_reloader.loaded
45
+ end
37
46
  end
@@ -0,0 +1,45 @@
1
+ module RequestHelpers
2
+ def authenticate_with_stub_signon(permissions: [])
3
+ state = request_to_establish_oauth_state
4
+
5
+ stub_signon_oauth_token_request
6
+ stub_successful_signon_user_request(permissions:)
7
+
8
+ get "/auth/gds/callback?code=code&state=#{state}"
9
+ expect(response).to have_http_status(:redirect)
10
+ end
11
+
12
+ def request_to_establish_oauth_state
13
+ get "/auth/gds"
14
+ expect(response).to have_http_status(:redirect)
15
+ location = URI.parse(response.location)
16
+ query = Rack::Utils.parse_query(location.query)
17
+ query.fetch("state")
18
+ end
19
+
20
+ def stub_signon_oauth_token_request
21
+ stub_request(:post, "http://signon/oauth/access_token")
22
+ .to_return(body: { access_token: "token" }.to_json,
23
+ headers: { content_type: "application/json" })
24
+ end
25
+
26
+ def stub_successful_signon_user_request(permissions: [])
27
+ stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
28
+ .to_return(
29
+ body: {
30
+ user: {
31
+ uid: "123",
32
+ email: "test-user@example.com",
33
+ name: "Test User",
34
+ permissions:,
35
+ },
36
+ }.to_json,
37
+ headers: { content_type: "application/json" },
38
+ )
39
+ end
40
+
41
+ def stub_failed_signon_user_request
42
+ stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
43
+ .to_return(status: 401)
44
+ end
45
+ end
@@ -18,31 +18,49 @@ describe GDS::SSO::ApiAccess do
18
18
  expect(described_class.api_call?({})).to be(true)
19
19
  end
20
20
 
21
- it "returns true if the request matches the api_request_matcher" do
22
- allow(GDS::SSO::Config)
23
- .to receive(:api_request_matcher)
24
- .and_return(->(request) { request.path == "/api" })
21
+ context "when an api_request_matcher has been configured" do
22
+ before do
23
+ allow(GDS::SSO::Config)
24
+ .to receive(:api_request_matcher)
25
+ .and_return(->(request) { request.path == "/api" })
26
+ end
25
27
 
26
- env = Rack::MockRequest.env_for("/api")
27
- expect(described_class.api_call?(env)).to be(true)
28
- end
28
+ it "returns true if the request matches the api_request_matcher" do
29
+ env = Rack::MockRequest.env_for("/api")
30
+ expect(described_class.api_call?(env)).to be(true)
31
+ end
29
32
 
30
- it "returns false if the request doesn't match the api_request_matcher" do
31
- allow(GDS::SSO::Config)
32
- .to receive(:api_request_matcher)
33
- .and_return(->(request) { request.path == "/api" })
33
+ it "returns true if the request is for GDS SSO API at default location" do
34
+ env = Rack::MockRequest.env_for("/auth/gds/api/#{SecureRandom.uuid}")
35
+ expect(described_class.api_call?(env)).to be(true)
36
+ end
34
37
 
35
- env = Rack::MockRequest.env_for("/other")
36
- expect(described_class.api_call?(env)).to be(false)
37
- end
38
+ it "returns true if it matches a configured gds_sso_api_request_matcher" do
39
+ allow(GDS::SSO::Config)
40
+ .to receive(:gds_sso_api_request_matcher)
41
+ .and_return(->(request) { request.path == "/special/gds-sso/route" })
38
42
 
39
- it "returns true if a bearer token is present" do
40
- env = { "HTTP_AUTHORIZATION" => "Bearer 1234:5678" }
41
- expect(described_class.api_call?(env)).to be(true)
43
+ env = Rack::MockRequest.env_for("/special/gds-sso/route")
44
+ expect(described_class.api_call?(env)).to be(true)
45
+ end
46
+
47
+ it "returns false if the request doesn't match the gds_sso_api_request_matcher or api_request_matcher" do
48
+ allow(GDS::SSO::Config).to receive(:gds_sso_api_request_matcher).and_return(nil)
49
+
50
+ env = Rack::MockRequest.env_for("/other")
51
+ expect(described_class.api_call?(env)).to be(false)
52
+ end
42
53
  end
43
54
 
44
- it "returns false otherwise" do
45
- expect(described_class.api_call?({})).to be(false)
55
+ context "when an api_request_matcher has not been configured" do
56
+ it "returns true if a bearer token is present" do
57
+ env = { "HTTP_AUTHORIZATION" => "Bearer 1234:5678" }
58
+ expect(described_class.api_call?(env)).to be(true)
59
+ end
60
+
61
+ it "returns false if nothing indicates an API call" do
62
+ expect(described_class.api_call?({})).to be(false)
63
+ end
46
64
  end
47
65
  end
48
66
 
@@ -10,6 +10,12 @@ describe GDS::SSO::MockBearerToken do
10
10
  expect(described_class.locate("anything")).to be(test_user)
11
11
  end
12
12
 
13
+ it "returns nil if ENV['GDS_SSO_MOCK_INVALID'] is set" do
14
+ ClimateControl.modify("GDS_SSO_MOCK_INVALID" => "1") do
15
+ expect(described_class.locate("anything")).to be_nil
16
+ end
17
+ end
18
+
13
19
  it "doesn't modify the permissions of GDS::SSO.test_user" do
14
20
  test_user = TestUser.new(permissions: [])
15
21
  allow(GDS::SSO).to receive(:test_user).and_return(test_user)
@@ -14,11 +14,12 @@ describe Warden::SessionSerializer do
14
14
 
15
15
  describe "serializing a user" do
16
16
  it "should return the uid and an ISO 8601 string timestamp" do
17
- Timecop.freeze
18
- result = @serializer.serialize(@user)
17
+ freeze_time do
18
+ result = @serializer.serialize(@user)
19
19
 
20
- expect(result).to eq([1234, Time.now.utc.iso8601])
21
- expect(result.last).to be_a(String)
20
+ expect(result).to eq([1234, Time.now.utc.iso8601])
21
+ expect(result.last).to be_a(String)
22
+ end
22
23
  end
23
24
 
24
25
  it "should return nil if the user has no uid" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gds-sso
3
3
  version: !ruby/object:Gem::Version
4
- version: 21.0.0
4
+ version: 21.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GOV.UK Dev
@@ -122,19 +122,19 @@ dependencies:
122
122
  - !ruby/object:Gem::Version
123
123
  version: 0.0.1
124
124
  - !ruby/object:Gem::Dependency
125
- name: capybara
125
+ name: climate_control
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
- version: '3'
130
+ version: '1'
131
131
  type: :development
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '3'
137
+ version: '1'
138
138
  - !ruby/object:Gem::Dependency
139
139
  name: combustion
140
140
  requirement: !ruby/object:Gem::Requirement
@@ -183,14 +183,14 @@ dependencies:
183
183
  requirements:
184
184
  - - '='
185
185
  - !ruby/object:Gem::Version
186
- version: 5.0.2
186
+ version: 5.1.18
187
187
  type: :development
188
188
  prerelease: false
189
189
  version_requirements: !ruby/object:Gem::Requirement
190
190
  requirements:
191
191
  - - '='
192
192
  - !ruby/object:Gem::Version
193
- version: 5.0.2
193
+ version: 5.1.18
194
194
  - !ruby/object:Gem::Dependency
195
195
  name: sqlite3
196
196
  requirement: !ruby/object:Gem::Requirement
@@ -205,20 +205,6 @@ dependencies:
205
205
  - - "~>"
206
206
  - !ruby/object:Gem::Version
207
207
  version: '2.1'
208
- - !ruby/object:Gem::Dependency
209
- name: timecop
210
- requirement: !ruby/object:Gem::Requirement
211
- requirements:
212
- - - "~>"
213
- - !ruby/object:Gem::Version
214
- version: '0.9'
215
- type: :development
216
- prerelease: false
217
- version_requirements: !ruby/object:Gem::Requirement
218
- requirements:
219
- - - "~>"
220
- - !ruby/object:Gem::Version
221
- version: '0.9'
222
208
  - !ruby/object:Gem::Dependency
223
209
  name: webmock
224
210
  requirement: !ruby/object:Gem::Requirement
@@ -264,8 +250,6 @@ files:
264
250
  - lib/gds-sso/version.rb
265
251
  - lib/gds-sso/warden_config.rb
266
252
  - lib/omniauth/strategies/gds.rb
267
- - spec/controller/api_user_controller_spec.rb
268
- - spec/controller/controller_methods_spec.rb
269
253
  - spec/internal/app/assets/config/manifest.js
270
254
  - spec/internal/app/controllers/application_controller.rb
271
255
  - spec/internal/app/controllers/example_controller.rb
@@ -274,17 +258,20 @@ files:
274
258
  - spec/internal/config/initializers/gds-sso.rb
275
259
  - spec/internal/config/routes.rb
276
260
  - spec/internal/db/schema.rb
261
+ - spec/request/api/user_spec.rb
262
+ - spec/request/authentication_spec.rb
263
+ - spec/request/demo_app_spec.rb
277
264
  - spec/spec_helper.rb
278
265
  - spec/support/controller_spy.rb
266
+ - spec/support/request_helpers.rb
279
267
  - spec/support/serializable_user.rb
280
268
  - spec/support/test_user.rb
281
- - spec/support/timecop.rb
282
- - spec/system/authentication_and_authorisation_spec.rb
283
269
  - spec/unit/api_access_spec.rb
284
270
  - spec/unit/authorise_user_spec.rb
285
271
  - spec/unit/authorised_user_constraint_spec.rb
286
272
  - spec/unit/bearer_token_spec.rb
287
273
  - spec/unit/config_spec.rb
274
+ - spec/unit/controller_methods_spec.rb
288
275
  - spec/unit/gds_sso_spec.rb
289
276
  - spec/unit/mock_bearer_token_spec.rb
290
277
  - spec/unit/railtie_spec.rb
@@ -312,8 +299,6 @@ rubygems_version: 3.7.1
312
299
  specification_version: 4
313
300
  summary: Client for GDS' OAuth 2-based SSO
314
301
  test_files:
315
- - spec/controller/api_user_controller_spec.rb
316
- - spec/controller/controller_methods_spec.rb
317
302
  - spec/internal/app/assets/config/manifest.js
318
303
  - spec/internal/app/controllers/application_controller.rb
319
304
  - spec/internal/app/controllers/example_controller.rb
@@ -322,17 +307,20 @@ test_files:
322
307
  - spec/internal/config/initializers/gds-sso.rb
323
308
  - spec/internal/config/routes.rb
324
309
  - spec/internal/db/schema.rb
310
+ - spec/request/api/user_spec.rb
311
+ - spec/request/authentication_spec.rb
312
+ - spec/request/demo_app_spec.rb
325
313
  - spec/spec_helper.rb
326
314
  - spec/support/controller_spy.rb
315
+ - spec/support/request_helpers.rb
327
316
  - spec/support/serializable_user.rb
328
317
  - spec/support/test_user.rb
329
- - spec/support/timecop.rb
330
- - spec/system/authentication_and_authorisation_spec.rb
331
318
  - spec/unit/api_access_spec.rb
332
319
  - spec/unit/authorise_user_spec.rb
333
320
  - spec/unit/authorised_user_constraint_spec.rb
334
321
  - spec/unit/bearer_token_spec.rb
335
322
  - spec/unit/config_spec.rb
323
+ - spec/unit/controller_methods_spec.rb
336
324
  - spec/unit/gds_sso_spec.rb
337
325
  - spec/unit/mock_bearer_token_spec.rb
338
326
  - spec/unit/railtie_spec.rb
@@ -1,114 +0,0 @@
1
- require "spec_helper"
2
-
3
- def user_update_json
4
- {
5
- "user" => {
6
- "uid" => @user_to_update.uid,
7
- "name" => "Joshua Marshall",
8
- "email" => "user@domain.com",
9
- "permissions" => ["signin", "new permission"],
10
- "organisation_slug" => "justice-league",
11
- "organisation_content_id" => "aae1319e-5788-4677-998c-f1a53af528d0",
12
- "disabled" => false,
13
- },
14
- }.to_json
15
- end
16
-
17
- describe Api::UserController, type: :controller do
18
- before :each do
19
- user_to_update_attrs = [{
20
- uid: "a1s2d3#{rand(10_000)}",
21
- email: "old@domain.com",
22
- name: "Moshua Jarshall",
23
- permissions: %w[signin],
24
- }]
25
-
26
- signon_sso_push_user_attrs = [{
27
- uid: "a1s2d3#{rand(10_000)}",
28
- email: "ssopushuser@legit.com",
29
- name: "SSO Push user",
30
- permissions: %w[signin user_update_permission],
31
- }]
32
-
33
- @user_to_update = User.create!(*user_to_update_attrs)
34
- @signon_sso_push_user = User.create!(*signon_sso_push_user_attrs)
35
- end
36
-
37
- describe "PUT update" do
38
- it "should deny access to anybody but the API user (or a user with 'user_update_permission')" do
39
- malicious_user = User.new({
40
- uid: "2",
41
- name: "User",
42
- permissions: %w[signin],
43
- })
44
-
45
- request.env["warden"] = double("stub warden", authenticate!: true, authenticated?: true, user: malicious_user)
46
-
47
- request.env["RAW_POST_DATA"] = user_update_json
48
- put :update, body: user_update_json, params: { uid: @user_to_update.uid }
49
-
50
- expect(response.status).to eq(403)
51
- end
52
-
53
- it "should create/update the user record in the same way as the OAuth callback" do
54
- # Test that it authenticates
55
- request.env["warden"] = double("mock warden")
56
- expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
57
- expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
58
- expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
59
-
60
- request.env["RAW_POST_DATA"] = user_update_json
61
- put :update, body: user_update_json, params: { uid: @user_to_update.uid }
62
-
63
- @user_to_update.reload
64
- expect(@user_to_update.name).to eq("Joshua Marshall")
65
- expect(@user_to_update.email).to eq("user@domain.com")
66
- expect(@user_to_update.permissions).to eq(["signin", "new permission"])
67
- expect(@user_to_update.organisation_slug).to eq("justice-league")
68
- expect(@user_to_update.organisation_content_id).to eq("aae1319e-5788-4677-998c-f1a53af528d0")
69
- expect(response.content_type).to eq("text/plain")
70
- end
71
- end
72
-
73
- describe "POST reauth" do
74
- it "should deny access to anybody but the API user (or a user with 'user_update_permission')" do
75
- malicious_user = User.new({
76
- uid: "2",
77
- name: "User",
78
- permissions: %w[signin],
79
- })
80
-
81
- request.env["warden"] = double("stub warden", authenticate!: true, authenticated?: true, user: malicious_user)
82
-
83
- post :reauth, params: { uid: @user_to_update.uid }
84
-
85
- expect(response.status).to eq(403)
86
- end
87
-
88
- it "should return success if user record doesn't exist" do
89
- request.env["warden"] = double("mock warden")
90
- expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
91
- expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
92
- expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
93
-
94
- post :reauth, params: { uid: "nonexistent-user" }
95
-
96
- expect(response.status).to eq(200)
97
- expect(response.content_type).to eq("text/plain")
98
- end
99
-
100
- it "should set remotely_signed_out to true on the user" do
101
- # Test that it authenticates
102
- request.env["warden"] = double("mock warden")
103
- expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
104
- expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
105
- expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
106
-
107
- post :reauth, params: { uid: @user_to_update.uid }
108
-
109
- @user_to_update.reload
110
- expect(@user_to_update).to be_remotely_signed_out
111
- expect(response.content_type).to eq("text/plain")
112
- end
113
- end
114
- end
@@ -1,7 +0,0 @@
1
- require "timecop"
2
-
3
- RSpec.configure do |config|
4
- config.after :each do
5
- Timecop.return
6
- end
7
- end
@@ -1,245 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe "Authenication and authorisation" do
4
- context "omniauth request phase" do
5
- let(:redirect_url) { URI.parse(page.response_headers["Location"]) }
6
- let(:authorize_params) { Rack::Utils.parse_query(redirect_url.query) }
7
-
8
- before do
9
- visit "/auth/gds"
10
- end
11
-
12
- it "includes pkce code_challenge_method in request for /oauth/authorize" do
13
- expect(redirect_url.path).to eql("/oauth/authorize")
14
- expect(authorize_params["code_challenge_method"]).to eq("S256")
15
- end
16
-
17
- it "includes pkce code_challenge in request for /oauth/authorize" do
18
- expect(redirect_url.path).to eql("/oauth/authorize")
19
- expect(authorize_params["code_challenge"]).to be_present
20
- end
21
- end
22
-
23
- context "omniauth callback phase" do
24
- it "includes pkce code_verifier in request for /oauth/access_token" do
25
- visit "/auth/gds"
26
-
27
- redirect_url = URI.parse(page.response_headers["Location"])
28
- expect(redirect_url.path).to eql("/oauth/authorize")
29
- state = Rack::Utils.parse_query(redirect_url.query)["state"]
30
-
31
- stub_request(:post, "http://signon/oauth/access_token")
32
-
33
- visit "/auth/gds/callback?state=#{state}"
34
-
35
- expect(WebMock).to have_requested(:post, "http://signon/oauth/access_token")
36
- .with(body: hash_including({ "code_verifier" => /.*/ }))
37
- end
38
- end
39
-
40
- context "when accessing a route that doesn't require permissions or authentication" do
41
- it "allows access" do
42
- visit "/not-restricted"
43
- expect(page).to have_content("jabberwocky")
44
- end
45
- end
46
-
47
- context "when accessing a route that requires authentication" do
48
- it "redirects an unauthenticated request to signon" do
49
- # We manually follow the redirects because we have configured capybara
50
- # to not follow redirects (and thus allow testing an external redirect)
51
- visit "/restricted"
52
- expect(page.response_headers["Location"]).to match("/auth/gds")
53
- visit page.response_headers["Location"]
54
- expect(page.response_headers["Location"]).to match("http://signon/oauth/authorize")
55
- end
56
-
57
- it "allows access for an authenticated user" do
58
- stub_signon_authenticated
59
-
60
- visit "/restricted"
61
- expect(page).to have_content("restricted kablooie")
62
- end
63
-
64
- it "restricts access if a user is authenticated but remotely signed out" do
65
- stub_signon_authenticated
66
- User.last.set_remotely_signed_out!
67
-
68
- visit "/restricted"
69
- expect(page.status_code).to eql(302)
70
- expect(page.response_headers["Location"]).to match("/auth/gds")
71
- end
72
-
73
- it "restricts access if a user is authenticated but session has expired" do
74
- stub_signon_authenticated
75
-
76
- Timecop.travel(Time.now.utc + GDS::SSO::Config.auth_valid_for + 5.minutes) do
77
- visit "/restricted"
78
- expect(page.status_code).to eql(302)
79
- expect(page.response_headers["Location"]).to match("/auth/gds")
80
- end
81
- end
82
-
83
- it "restricts access when the request doesn't match the api_request_matcher" do
84
- allow(GDS::SSO::Config)
85
- .to receive(:api_request_matcher)
86
- .and_return(->(_request) { false })
87
-
88
- visit "/restricted"
89
- expect(page.status_code).to eql(302)
90
- expect(page.response_headers["Location"]).to match("/auth/gds")
91
- end
92
-
93
- it "allows access when given a valid bearer token" do
94
- stub_signon_user_request
95
- page.driver.header("Authorization", "Bearer 123")
96
-
97
- visit "/restricted"
98
- expect(page).to have_content("restricted kablooie")
99
- end
100
-
101
- it "restricts access when given an invalid bearer token" do
102
- stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
103
- .to_return(status: 401)
104
- page.driver.header("Authorization", "Bearer 123")
105
-
106
- visit "/restricted"
107
- expect(page.status_code).to eq(401)
108
- expect(page.response_headers["WWW-Authenticate"]).to eq('Bearer error="invalid_token"')
109
- expect_json_response({ "message" => "Bearer token does not appear to be valid" })
110
- end
111
-
112
- it "returns a JSON 401 when a bearer token is missing and the app is api_only" do
113
- allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
114
-
115
- visit "/restricted"
116
- expect(page.status_code).to eq(401)
117
- expect(page.response_headers["WWW-Authenticate"]).to eq('Bearer error="invalid_request"')
118
- expect_json_response({ "message" => "No bearer token was provided" })
119
- end
120
-
121
- it "returns a JSON 401 when a bearer token is missing and the request matches the api_request_matcher" do
122
- allow(GDS::SSO::Config)
123
- .to receive(:api_request_matcher)
124
- .and_return(->(request) { request.path == "/restricted" })
125
-
126
- visit "/restricted"
127
- expect(page.status_code).to eq(401)
128
- expect(page.response_headers["WWW-Authenticate"]).to eq('Bearer error="invalid_request"')
129
- expect_json_response({ "message" => "No bearer token was provided" })
130
- end
131
- end
132
-
133
- context "when accessing a route that requires authentication with the mock strategies" do
134
- before do
135
- # Using allow_any_instance_of because it's hard to access the instance
136
- # of the class used within the Rails middleware
137
- allow_any_instance_of(Warden::Config).to receive(:[]).and_call_original
138
- allow_any_instance_of(Warden::Config)
139
- .to receive(:[])
140
- .with(:default_strategies)
141
- .and_return({ _all: %i[mock_gds_sso gds_bearer_token] })
142
-
143
- allow(Warden::OAuth2.config).to receive(:token_model).and_return(GDS::SSO::MockBearerToken)
144
- allow(GDS::SSO).to receive(:test_user).and_return(TestUser.new)
145
- end
146
-
147
- it "allows access without being logged in" do
148
- visit "/restricted"
149
- expect(page.status_code).to eq(200)
150
- expect(page.body).to have_content("restricted kablooie")
151
- end
152
-
153
- it "allows access to an API mock user" do
154
- allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
155
-
156
- visit "/restricted"
157
- expect(page.status_code).to eq(200)
158
- expect(page.body).to have_content("restricted kablooie")
159
- end
160
- end
161
-
162
- context "when accessing a route that requires a permission" do
163
- it "allows access when an authenticated user has the permission" do
164
- stub_signon_authenticated(permissions: %w[execute])
165
- visit "/this-requires-execute-permission"
166
- expect(page).to have_content("you have execute permission")
167
- end
168
-
169
- it "restricts access when an authenticated user lacks the permission" do
170
- stub_signon_authenticated
171
- visit "/this-requires-execute-permission"
172
- expect(page.status_code).to eq(403)
173
- expect(page).to have_content("Sorry, you don't seem to have the execute permission for this app.")
174
- end
175
-
176
- it "returns a JSON response when it's an API call" do
177
- allow(GDS::SSO::Config)
178
- .to receive(:api_request_matcher)
179
- .and_return(->(request) { request.path == "/this-requires-execute-permission" })
180
-
181
- stub_signon_user_request
182
- page.driver.header("Authorization", "Bearer 123")
183
-
184
- visit "/this-requires-execute-permission"
185
- expect(page.status_code).to eq(403)
186
- expect_json_response({ "message" => "Sorry, you don't seem to have the execute permission for this app." })
187
- end
188
- end
189
-
190
- context "when accessing a route that is restricted by the authorised user constraint" do
191
- it "allows access when an authenticated user has correct permissions" do
192
- stub_signon_authenticated(permissions: %w[execute])
193
- visit "/constraint-restricted"
194
- expect(page).to have_content("constraint restricted")
195
- end
196
-
197
- it "redirects an unauthenticated request to signon" do
198
- visit "/constraint-restricted"
199
- expect(page.response_headers["Location"]).to match("/auth/gds")
200
- visit page.response_headers["Location"]
201
- expect(page.response_headers["Location"]).to match("http://signon/oauth/authorize")
202
- end
203
-
204
- it "restricts access when an authenticated user does not have the correct permissions" do
205
- stub_signon_authenticated(permissions: %w[no-access])
206
- visit "/constraint-restricted"
207
- expect(page.status_code).to eq(403)
208
- end
209
- end
210
-
211
- def stub_signon_authenticated(permissions: [])
212
- # visit restricted page to trigger redirect URL to record state attribute
213
- visit "/auth/gds"
214
- state = CGI.parse(URI.parse(page.response_headers["Location"]).query)
215
- .then { |query| query["state"].first }
216
-
217
- stub_request(:post, "http://signon/oauth/access_token")
218
- .to_return(body: { access_token: "token" }.to_json,
219
- headers: { content_type: "application/json" })
220
-
221
- stub_signon_user_request(permissions:)
222
-
223
- visit "/auth/gds/callback?code=code&state=#{state}"
224
- end
225
-
226
- def stub_signon_user_request(permissions: [])
227
- stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
228
- .to_return(
229
- body: {
230
- user: {
231
- uid: "123",
232
- email: "test-user@example.com",
233
- name: "Test User",
234
- permissions:,
235
- },
236
- }.to_json,
237
- headers: { content_type: "application/json" },
238
- )
239
- end
240
-
241
- def expect_json_response(json_match)
242
- expect(page.response_headers["content-type"]).to match(/application\/json/)
243
- expect(JSON.parse(page.body)).to match(json_match)
244
- end
245
- end