1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
6 require 'webrick/https'
8 require 'helpers/users_test_helper'
10 class RemoteUsersTest < ActionDispatch::IntegrationTest
13 def salted_active_token(remote:)
14 salt_token(fixture: :active, remote: remote).sub('/zzzzz-', '/'+remote+'-')
18 token = salted_active_token(remote: remote)
19 {"HTTP_AUTHORIZATION" => "Bearer #{token}"}
22 # For remote authentication tests, we bring up a simple stub server
23 # (on a port chosen by webrick) and configure the SUT so the stub is
24 # responsible for clusters "zbbbb" (a well-behaved cluster) and
25 # "zbork" (a misbehaving cluster).
27 # Test cases can override the stub's default response to
28 # .../users/current by changing @stub_status and @stub_content.
31 clnt.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
32 HTTPClient.stubs(:new).returns clnt
34 @controller = Arvados::V1::UsersController.new
35 ready = Thread::Queue.new
40 ['zbbbb', 'zbork'].each do |clusterid|
41 srv = WEBrick::HTTPServer.new(
43 Logger: WEBrick::Log.new(
44 Rails.root.join("log", "webrick.log").to_s,
46 AccessLog: [[File.open(Rails.root.join(
47 "log", "webrick_access.log").to_s, 'a+'),
48 WEBrick::AccessLog::COMBINED_LOG_FORMAT]],
50 SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
51 SSLPrivateKey: OpenSSL::PKey::RSA.new(
52 File.open(Rails.root.join("tmp", "self-signed.key")).read),
53 SSLCertificate: OpenSSL::X509::Certificate.new(
54 File.open(Rails.root.join("tmp", "self-signed.pem")).read),
55 SSLCertName: [["CN", WEBrick::Utils::getservername]],
56 StartCallback: lambda { ready.push(true) })
57 srv.mount_proc '/discovery/v1/apis/arvados/v1/rest' do |req, res|
58 res.body = Arvados::V1::SchemaController.new.send(:discovery_doc).to_json
60 srv.mount_proc '/arvados/v1/users/current' do |req, res|
61 if clusterid == 'zbbbb' and req.header['authorization'][0][10..14] == 'zbork'
62 # asking zbbbb about zbork should yield an error, zbbbb doesn't trust zbork
66 res.status = @stub_status
67 res.body = @stub_content.is_a?(String) ? @stub_content : @stub_content.to_json
69 srv.mount_proc '/arvados/v1/api_client_authorizations/current' do |req, res|
70 if clusterid == 'zbbbb' and req.header['authorization'][0][10..14] == 'zbork'
71 # asking zbbbb about zbork should yield an error, zbbbb doesn't trust zbork
75 res.status = @stub_token_status
78 uuid: api_client_authorizations(:active).uuid.sub('zzzzz', clusterid),
79 owner_uuid: "#{clusterid}-tpzed-00000000000000z",
80 scopes: @stub_token_scopes,
82 if @stub_content.is_a?(Hash) and owner_uuid = @stub_content[:uuid]
83 body[:owner_uuid] = owner_uuid
85 res.body = body.to_json
93 @remote_host << "127.0.0.1:#{srv.config[:Port]}"
95 Rails.configuration.RemoteClusters = Rails.configuration.RemoteClusters.merge({zbbbb: ActiveSupport::InheritableOptions.new({Host: @remote_host[0]}),
96 zbork: ActiveSupport::InheritableOptions.new({Host: @remote_host[1]})})
97 Arvados::V1::SchemaController.any_instance.stubs(:root_url).returns "https://#{@remote_host[0]}"
100 uuid: 'zbbbb-tpzed-000000000000001',
101 email: 'foo@example.com',
107 @stub_token_status = 200
108 @stub_token_scopes = ["all"]
112 @remote_server.each do |srv|
117 def uncache_token(src)
118 if match = src.match(/\b(?:[a-z0-9]{5}-){2}[a-z0-9]{15}\b/)
119 tokens = ApiClientAuthorization.where(uuid: match[0])
121 tokens = ApiClientAuthorization.where("uuid like ?", "#{src}-%")
123 tokens.update_all(expires_at: "1995-05-15T01:02:03Z")
126 test 'authenticate with remote token that has limited scope' do
127 get '/arvados/v1/collections',
128 params: {format: 'json'},
129 headers: auth(remote: 'zbbbb')
130 assert_response :success
132 @stub_token_scopes = ["GET /arvados/v1/users/current"]
134 # re-authorize before cache expires
135 get '/arvados/v1/collections',
136 params: {format: 'json'},
137 headers: auth(remote: 'zbbbb')
138 assert_response :success
140 uncache_token('zbbbb')
141 # re-authorize after cache expires
142 get '/arvados/v1/collections',
143 params: {format: 'json'},
144 headers: auth(remote: 'zbbbb')
148 test "authenticate with remote token with limited initial scope" do
149 @stub_token_scopes = ["GET /arvados/v1/users/"]
150 get "/arvados/v1/users/#{@stub_content[:uuid]}",
151 params: {format: "json"},
152 headers: auth(remote: "zbbbb")
153 assert_response :success
156 test 'authenticate with remote token' do
157 get '/arvados/v1/users/current',
158 params: {format: 'json'},
159 headers: auth(remote: 'zbbbb')
160 assert_response :success
161 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
162 assert_equal false, json_response['is_admin']
163 assert_equal false, json_response['is_active']
164 assert_equal 'foo@example.com', json_response['email']
165 assert_equal 'barney', json_response['username']
167 # revoke original token
168 @stub_token_status = 401
170 # re-authorize before cache expires
171 get '/arvados/v1/users/current',
172 params: {format: 'json'},
173 headers: auth(remote: 'zbbbb')
174 assert_response :success
176 uncache_token('zbbbb')
177 # re-authorize after cache expires
178 get '/arvados/v1/users/current',
179 params: {format: 'json'},
180 headers: auth(remote: 'zbbbb')
183 # simulate cached token indicating wrong user (e.g., local user
184 # entry was migrated out of the way taking the cached token with
185 # it, or authorizing cluster reassigned auth to a different user)
186 ApiClientAuthorization.where(
187 uuid: salted_active_token(remote: 'zbbbb').split('/')[1]).
188 update_all(user_id: users(:active).id)
190 # revive original token and re-authorize
191 @stub_token_status = 200
192 @stub_content[:username] = 'blarney'
193 @stub_content[:email] = 'blarney@example.com'
194 get '/arvados/v1/users/current',
195 params: {format: 'json'},
196 headers: auth(remote: 'zbbbb')
197 assert_response :success
198 assert_equal 'barney', json_response['username'], 'local username should not change once assigned'
199 assert_equal 'blarney@example.com', json_response['email']
202 test 'remote user is deactivated' do
203 Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = true
204 get '/arvados/v1/users/current',
205 params: {format: 'json'},
206 headers: auth(remote: 'zbbbb')
207 assert_response :success
208 assert_equal true, json_response['is_active']
210 # revoke original token
211 @stub_content[:is_active] = false
212 @stub_content[:is_invited] = false
214 uncache_token('zbbbb')
215 # re-authorize after cache expires
216 get '/arvados/v1/users/current',
217 params: {format: 'json'},
218 headers: auth(remote: 'zbbbb')
219 assert_equal false, json_response['is_active']
223 test 'authenticate with remote token, remote username conflicts with local' do
224 @stub_content[:username] = 'active'
225 get '/arvados/v1/users/current',
226 params: {format: 'json'},
227 headers: auth(remote: 'zbbbb')
228 assert_response :success
229 assert_equal 'active2', json_response['username']
232 test 'authenticate with remote token, remote username is nil' do
233 @stub_content.delete :username
234 get '/arvados/v1/users/current',
235 params: {format: 'json'},
236 headers: auth(remote: 'zbbbb')
237 assert_response :success
238 assert_equal 'foo', json_response['username']
241 test 'authenticate with remote token from misbehaving remote cluster' do
242 get '/arvados/v1/users/current',
243 params: {format: 'json'},
244 headers: auth(remote: 'zbork')
248 test 'authenticate with remote token that fails validate' do
251 error: 'not authorized',
253 get '/arvados/v1/users/current',
254 params: {format: 'json'},
255 headers: auth(remote: 'zbbbb')
263 "v2/'; delete from users where 1=1; commit; select '/lol",
265 'v2/zzzzz-gj3su-077z32aux8dg2s1',
266 'v2/zzzzz-gj3su-077z32aux8dg2s1/',
267 'v2/3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi',
268 'v2/3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi/zzzzz-gj3su-077z32aux8dg2s1',
269 'v2//3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi',
270 'v8/zzzzz-gj3su-077z32aux8dg2s1/3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi',
271 '/zzzzz-gj3su-077z32aux8dg2s1/3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi',
272 '"v2/zzzzz-gj3su-077z32aux8dg2s1/3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi"',
277 test "authenticate with malformed remote token #{token}" do
278 get '/arvados/v1/users/current',
279 params: {format: 'json'},
280 headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"}
285 test "ignore extra fields in remote token" do
286 token = salted_active_token(remote: 'zbbbb') + '/foo/bar/baz/*'
287 get '/arvados/v1/users/current',
288 params: {format: 'json'},
289 headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"}
290 assert_response :success
293 test 'remote api server is not an api server' do
295 @stub_content = '<html>bad</html>'
296 get '/arvados/v1/users/current',
297 params: {format: 'json'},
298 headers: auth(remote: 'zbbbb')
302 ['zbbbb', 'z0000'].each do |token_valid_for|
303 test "validate #{token_valid_for}-salted token for remote cluster zbbbb" do
304 salted_token = salt_token(fixture: :active, remote: token_valid_for)
305 get '/arvados/v1/users/current',
306 params: {format: 'json', remote: 'zbbbb'},
307 headers: {"HTTP_AUTHORIZATION" => "Bearer #{salted_token}"}
308 if token_valid_for == 'zbbbb'
310 assert_equal(users(:active).uuid, json_response['uuid'])
317 test "list readable groups with salted token" do
318 Rails.configuration.Users.RoleGroupsVisibleToAll = false
319 salted_token = salt_token(fixture: :active, remote: 'zbbbb')
320 get '/arvados/v1/groups',
326 headers: {"HTTP_AUTHORIZATION" => "Bearer #{salted_token}"}
328 group_uuids = json_response['items'].collect { |i| i['uuid'] }
329 assert_includes(group_uuids, 'zzzzz-j7d0g-fffffffffffffff')
330 refute_includes(group_uuids, 'zzzzz-j7d0g-000000000000000')
331 assert_includes(group_uuids, groups(:aproject).uuid)
332 refute_includes(group_uuids, groups(:trashed_project).uuid)
333 refute_includes(group_uuids, groups(:testusergroup_admins).uuid)
336 test 'do not auto-activate user from untrusted cluster' do
337 Rails.configuration.RemoteClusters['zbbbb'].AutoSetupNewUsers = false
338 Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = false
339 get '/arvados/v1/users/current',
340 params: {format: 'json'},
341 headers: auth(remote: 'zbbbb')
342 assert_response :success
343 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
344 assert_equal false, json_response['is_admin']
345 assert_equal false, json_response['is_active']
346 assert_equal 'foo@example.com', json_response['email']
347 assert_equal 'barney', json_response['username']
348 post '/arvados/v1/users/zbbbb-tpzed-000000000000001/activate',
349 params: {format: 'json'},
350 headers: auth(remote: 'zbbbb')
354 test 'auto-activate user from trusted cluster' do
355 Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = true
356 get '/arvados/v1/users/current',
357 params: {format: 'json'},
358 headers: auth(remote: 'zbbbb')
359 assert_response :success
360 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
361 assert_equal false, json_response['is_admin']
362 assert_equal true, json_response['is_active']
363 assert_equal 'foo@example.com', json_response['email']
364 assert_equal 'barney', json_response['username']
367 test 'get user from Login cluster' do
368 Rails.configuration.Login.LoginCluster = 'zbbbb'
369 get '/arvados/v1/users/current',
370 params: {format: 'json'},
371 headers: auth(remote: 'zbbbb')
372 assert_response :success
373 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
374 assert_equal true, json_response['is_admin']
375 assert_equal true, json_response['is_active']
376 assert_equal 'foo@example.com', json_response['email']
377 assert_equal 'barney', json_response['username']
380 [true, false].each do |trusted|
381 [true, false].each do |logincluster|
382 [true, false].each do |admin|
383 [true, false].each do |active|
384 [true, false].each do |autosetup|
385 [true, false].each do |invited|
386 test "get invited=#{invited}, active=#{active}, admin=#{admin} user from #{if logincluster then "Login" else "peer" end} cluster when AutoSetupNewUsers=#{autosetup} ActivateUsers=#{trusted}" do
387 Rails.configuration.Login.LoginCluster = 'zbbbb' if logincluster
388 Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = trusted
389 Rails.configuration.Users.AutoSetupNewUsers = autosetup
391 uuid: 'zbbbb-tpzed-000000000000001',
392 email: 'foo@example.com',
398 get '/arvados/v1/users/current',
399 params: {format: 'json'},
400 headers: auth(remote: 'zbbbb')
401 assert_response :success
402 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
403 assert_equal (logincluster && admin && invited && active), json_response['is_admin']
404 assert_equal (invited and (logincluster || trusted || autosetup)), json_response['is_invited']
405 assert_equal (invited and (logincluster || trusted) and active), json_response['is_active']
406 assert_equal 'foo@example.com', json_response['email']
407 assert_equal 'barney', json_response['username']
416 test 'get active user from Login cluster when AutoSetupNewUsers is set' do
417 Rails.configuration.Login.LoginCluster = 'zbbbb'
418 Rails.configuration.Users.AutoSetupNewUsers = true
420 uuid: 'zbbbb-tpzed-000000000000001',
421 email: 'foo@example.com',
427 get '/arvados/v1/users/current',
428 params: {format: 'json'},
429 headers: auth(remote: 'zbbbb')
430 assert_response :success
431 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
432 assert_equal false, json_response['is_admin']
433 assert_equal true, json_response['is_active']
434 assert_equal true, json_response['is_invited']
435 assert_equal 'foo@example.com', json_response['email']
436 assert_equal 'barney', json_response['username']
439 uuid: 'zbbbb-tpzed-000000000000001',
440 email: 'foo@example.com',
447 # Use cached value. User will still be active because we haven't
448 # re-queried the upstream cluster.
449 get '/arvados/v1/users/current',
450 params: {format: 'json'},
451 headers: auth(remote: 'zbbbb')
452 assert_response :success
453 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
454 assert_equal false, json_response['is_admin']
455 assert_equal true, json_response['is_active']
456 assert_equal true, json_response['is_invited']
457 assert_equal 'foo@example.com', json_response['email']
458 assert_equal 'barney', json_response['username']
460 uncache_token('zbbbb')
461 # User should be inactive now.
462 get '/arvados/v1/users/current',
463 params: {format: 'json'},
464 headers: auth(remote: 'zbbbb')
465 assert_response :success
466 assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
467 assert_equal false, json_response['is_admin']
468 assert_equal false, json_response['is_active']
469 assert_equal false, json_response['is_invited']
470 assert_equal 'foo@example.com', json_response['email']
471 assert_equal 'barney', json_response['username']
475 test 'pre-activate remote user' do
477 uuid: 'zbbbb-tpzed-000000000001234',
478 email: 'foo@example.com',
485 post '/arvados/v1/users',
488 "uuid" => "zbbbb-tpzed-000000000001234",
489 "email" => 'foo@example.com',
490 "username" => 'barney',
495 headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{api_token(:admin)}"}
496 assert_response :success
498 get '/arvados/v1/users/current',
499 params: {format: 'json'},
500 headers: auth(remote: 'zbbbb')
501 assert_response :success
502 assert_equal 'zbbbb-tpzed-000000000001234', json_response['uuid']
503 assert_equal false, json_response['is_admin']
504 assert_equal true, json_response['is_active']
505 assert_equal 'foo@example.com', json_response['email']
506 assert_equal 'barney', json_response['username']
510 test 'remote user inactive without pre-activation' do
512 uuid: 'zbbbb-tpzed-000000000001234',
513 email: 'foo@example.com',
520 get '/arvados/v1/users/current',
521 params: {format: 'json'},
522 headers: auth(remote: 'zbbbb')
523 assert_response :success
524 assert_equal 'zbbbb-tpzed-000000000001234', json_response['uuid']
525 assert_equal false, json_response['is_admin']
526 assert_equal false, json_response['is_active']
527 assert_equal 'foo@example.com', json_response['email']
528 assert_equal 'barney', json_response['username']
531 test "validate unsalted v2 token for remote cluster zbbbb" do
532 auth = api_client_authorizations(:active)
533 token = "v2/#{auth.uuid}/#{auth.api_token}"
534 get '/arvados/v1/users/current',
535 params: {format: 'json', remote: 'zbbbb'},
536 headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"}
537 assert_response :success
538 assert_equal(users(:active).uuid, json_response['uuid'])
541 test 'container request with runtime_token' do
542 [["valid local", "v2/#{api_client_authorizations(:active).uuid}/#{api_client_authorizations(:active).api_token}"],
543 ["valid remote", "v2/zbbbb-gj3su-000000000000000/abc"],
544 ["invalid local", "v2/#{api_client_authorizations(:active).uuid}/fakefakefake"],
545 ["invalid remote", "v2/zbork-gj3su-000000000000000/abc"],
546 ].each do |label, runtime_token|
547 post '/arvados/v1/container_requests',
549 "container_request" => {
550 "command" => ["echo"],
551 "container_image" => "xyz",
552 "output_path" => "/",
554 "runtime_token" => runtime_token
557 headers: {"HTTP_AUTHORIZATION" => "Bearer #{api_client_authorizations(:active).api_token}"}
558 if label.include? "invalid"
561 assert_response :success
566 test 'authenticate with remote token, remote user is system user' do
567 @stub_content[:uuid] = 'zbbbb-tpzed-000000000000000'
568 get '/arvados/v1/users/current',
569 params: {format: 'json'},
570 headers: auth(remote: 'zbbbb')
571 assert_equal 'from cluster zbbbb', json_response['last_name']
574 test 'authenticate with remote token, remote user is anonymous user' do
575 @stub_content[:uuid] = 'zbbbb-tpzed-anonymouspublic'
576 get '/arvados/v1/users/current',
577 params: {format: 'json'},
578 headers: auth(remote: 'zbbbb')
579 assert_response :success
580 assert_equal 'zzzzz-tpzed-anonymouspublic', json_response['uuid']
583 [401, 403, 422, 500, 502, 503].each do |status|
584 test "propagate #{status} response from getting remote token" do
585 @stub_token_status = status
586 get "/arvados/v1/users/#{@stub_content[:uuid]}",
587 params: {format: "json"},
588 headers: auth(remote: "zbbbb")
589 assert_response status
592 test "propagate #{status} response from getting uncached user" do
593 @stub_status = status
594 get "/arvados/v1/users/#{@stub_content[:uuid]}",
595 params: {format: "json"},
596 headers: auth(remote: "zbbbb")
597 assert_response status
600 test "use cached user after getting #{status} response" do
601 url_path = "/arvados/v1/users/#{@stub_content[:uuid]}"
602 params = {format: "json"}
603 headers = auth(remote: "zbbbb")
605 get url_path, params: params, headers: headers
606 assert_response :success
608 uncache_token(headers["HTTP_AUTHORIZATION"])
609 expect_email = @stub_content[:email]
610 @stub_content[:email] = "new#{expect_email}"
611 @stub_status = status
612 get url_path, params: params, headers: headers
613 assert_response :success
614 user = User.find_by_uuid(@stub_content[:uuid])
616 assert_equal expect_email, user.email