11453: Move remote token validation to middleware. Bypass Ruby SDK.
authorTom Clegg <tclegg@veritasgenetics.com>
Tue, 7 Nov 2017 18:09:50 +0000 (13:09 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Tue, 7 Nov 2017 18:09:50 +0000 (13:09 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

build/run-tests.sh
services/api/Gemfile
services/api/Gemfile.lock
services/api/app/controllers/application_controller.rb
services/api/app/middlewares/arvados_api_token.rb
services/api/app/models/api_client_authorization.rb
services/api/test/functional/arvados/v1/users_controller_test.rb
services/api/test/integration/remote_user_test.rb [moved from services/api/test/functional/remote_user_account_test.rb with 69% similarity]
services/api/test/integration/users_test.rb

index e1e83edbc8c85f9e0a8f84dd0ccc6a613d5d8d8a..e25bd3790d8b2d43315fb581029da5222034108e 100755 (executable)
@@ -780,6 +780,12 @@ install_apiserver() {
 
     mkdir -p "$WORKSPACE/services/api/tmp/pids"
 
+    cert="$WORKSPACE/services/api/tmp/self-signed"
+    if ! [[ -e "$cert.key" ]]; then
+        dir="$WORKSPACE/services/api/tmp"
+        openssl req -new -x509 -nodes -out "$cert.pem" -keyout "$cert.key" -days 3650 -subj /CN=0.0.0.0 -extfile <(printf 'subjectAltName=DNS:127.0.0.1,DNS:localhost,DNS:::1')
+    fi
+
     cd "$WORKSPACE/services/api" \
         && RAILS_ENV=test bundle exec rake db:drop \
         && RAILS_ENV=test bundle exec rake db:setup \
index c06a9c5a9799d65dee6bf5d18d198b32d3395767..34e88a85e625792cae8ad74cdf7b85d68b2abff3 100644 (file)
@@ -57,6 +57,7 @@ gem 'themes_for_rails', git: 'https://github.com/curoverse/themes_for_rails'
 
 gem 'arvados', '>= 0.1.20150615153458'
 gem 'arvados-cli', '>= 0.1.20161017193526'
+gem 'httpclient'
 
 gem 'puma', '~> 2.0'
 gem 'sshkey'
index ebb594cb1ef31c8b89bd79a0dd01d2b14c158e56..85e90e38475122f716720a04ff031f9aa6d43191 100644 (file)
@@ -127,6 +127,7 @@ GEM
     hashie (3.5.5)
     highline (1.7.8)
     hike (1.2.3)
+    httpclient (2.8.3)
     i18n (0.9.0)
       concurrent-ruby (~> 1.0)
     jquery-rails (4.2.2)
@@ -296,6 +297,7 @@ DEPENDENCIES
   database_cleaner
   factory_girl_rails
   faye-websocket
+  httpclient
   jquery-rails
   lograge
   logstash-event
index a627bf4a46da64896482e5026f1e62c0aa567806..3c5ed60d1875714497e27a22dd000bed4de5811b 100644 (file)
@@ -345,20 +345,6 @@ class ApplicationController < ActionController::Base
         .all
     end
     @read_auths.select! { |auth| auth.scopes_allow_request? request }
-
-    # Use a salted token as a reader token for /groups/ and /users/current
-    if params[:remote] && (
-         request.path.start_with?('/arvados/v1/groups') ||
-         request.path.start_with?('/arvados/v1/users/current'))
-      auth = ApiClientAuthorization.
-             validate(token: Thread.current[:supplied_token],
-                      remote: params[:remote])
-      if auth && auth.user
-        Thread.current[:user] = auth.user
-        @read_auths << auth
-      end
-    end
-
     @read_users = @read_auths.map(&:user).uniq
   end
 
index fa8d9871fe80e329e81c136cab979f802aa3f9cd..3d680cbfb93578d28dd7e3dbc3706d769e1e7742 100644 (file)
@@ -26,8 +26,20 @@ class ArvadosApiToken
       env["HTTP_AUTHORIZATION"].andand.
         match(/(OAuth2|Bearer) ([-\/a-zA-Z0-9]+)/).andand[2]
 
+    if params[:remote] && (
+         request.path.start_with?('/arvados/v1/groups') ||
+         request.path.start_with?('/arvados/v1/users/current'))
+      # Request from a remote API server, asking to validate a salted
+      # token.
+      remote = params[:remote]
+    else
+      # Normal request.
+      remote = false
+    end
     auth = ApiClientAuthorization.
-           validate(token: Thread.current[:supplied_token], remote: false)
+           validate(token: Thread.current[:supplied_token],
+                    remote: remote)
+
     Thread.current[:api_client_ip_address] = remote_ip
     Thread.current[:api_client_authorization] = auth
     Thread.current[:api_client_uuid] = auth.andand.api_client.andand.uuid
index 6ab8abd3bb496931b531b48c2c7922d79dd3168c..43442c98731a8089a5da10f17ea154fe51bc3ec5 100644 (file)
@@ -83,10 +83,10 @@ class ApiClientAuthorization < ArvadosModel
     ["#{table_name}.id desc"]
   end
 
-  def self.remote_host(uuid:)
-    Rails.configuration.remote_hosts[uuid[0..4]] ||
+  def self.remote_host(uuid_prefix:)
+    Rails.configuration.remote_hosts[uuid_prefix] ||
       (Rails.configuration.remote_hosts_via_dns &&
-       uuid[0..4]+".arvadosapi.com")
+       uuid_prefix+".arvadosapi.com")
   end
 
   def self.validate(token:, remote:)
@@ -104,41 +104,62 @@ class ApiClientAuthorization < ArvadosModel
          (secret == auth.api_token ||
           secret == OpenSSL::HMAC.hexdigest('sha1', auth.api_token, remote))
         return auth
-      elsif uuid[0..4] != Rails.configuration.uuid_prefix
-        # Token was issued by a different cluster. If it's expired or
-        # missing in our database, ask the originating cluster to
-        # [re]validate it.
-        arv = Arvados.new(api_host: remote_host(uuid: uuid),
-                          api_token: token)
-        begin
-          remote_user = arv.user.current(remote: Rails.configuration.uuid_prefix)
-        rescue => e
-          logger.warn "remote authentication with token #{token.inspect} failed: #{e}"
-          return nil
-        end
-        if !remote_user.is_a?(Hash) || !remote_user[:uuid].is_a?(String) || remote_user[:uuid][0..4] != uuid[0..4]
-          logger.warn "remote authentication rejected: remote_user=#{remote_user.inspect}"
-          return nil
-        end
-        act_as_system_user do
-          # Add/update user and token in our database so we can
-          # validate subsequent requests faster.
-          user = User.find_or_create_by(uuid: remote_user[:uuid])
-          user.update_attributes!(remote_user.merge(is_admin: false))
-          auth = ApiClientAuthorization.
-                 includes(:user).
-                 find_or_create_by(uuid: uuid,
-                                   api_token: token,
-                                   user: user,
-                                   api_client_id: 0)
-          # Accept this token (and don't reload the user record) for
-          # 5 minutes. TODO: Request the actual api_client_auth
-          # record from the remote server in case it wants the token
-          # to expire sooner.
-          auth.update_attributes!(expires_at: Time.now + 5.minutes)
-        end
-        return auth
       end
+
+      uuid_prefix = uuid[0..4]
+      if uuid_prefix == Rails.configuration.uuid_prefix
+        # If the token were valid, we would have validated it above
+        return nil
+      elsif uuid_prefix.length != 5
+        # malformed
+        return nil
+      end
+
+      host = remote_host(uuid_prefix: uuid_prefix)
+      if !host
+        Rails.logger.warn "remote authentication rejected: no host for #{uuid_prefix.inspect}"
+        return nil
+      end
+
+      # Token was issued by a different cluster. If it's expired or
+      # missing in our database, ask the originating cluster to
+      # [re]validate it.
+      arv = Arvados.new(api_host: host,
+                        api_token: token,
+                        suppress_ssl_warnings: Rails.env == 'test')
+      begin
+        clnt = HTTPClient.new
+        remote_user = SafeJSON.load(
+          clnt.get_content('https://' + host + '/arvados/v1/users/current',
+                           {'remote' => Rails.configuration.uuid_prefix},
+                           {'Authorization' => 'Bearer ' + token}))
+      rescue => e
+        logger.warn "remote authentication with token #{token.inspect} failed: #{e}"
+        STDERR.puts e.backtrace
+        return nil
+      end
+      if !remote_user.is_a?(Hash) || !remote_user[:uuid].is_a?(String) || remote_user[:uuid][0..4] != uuid[0..4]
+        logger.warn "remote authentication rejected: remote_user=#{remote_user.inspect}"
+        return nil
+      end
+      act_as_system_user do
+        # Add/update user and token in our database so we can
+        # validate subsequent requests faster.
+        user = User.find_or_create_by(uuid: remote_user[:uuid])
+        user.update_attributes!(remote_user.merge(is_admin: false))
+        auth = ApiClientAuthorization.
+               includes(:user).
+               find_or_create_by(uuid: uuid,
+                                 api_token: token,
+                                 user: user,
+                                 api_client_id: 0)
+        # Accept this token (and don't reload the user record) for
+        # 5 minutes. TODO: Request the actual api_client_auth
+        # record from the remote server in case it wants the token
+        # to expire sooner.
+        auth.update_attributes!(expires_at: Time.now + 5.minutes)
+      end
+      return auth
     else
       auth = ApiClientAuthorization.
              includes(:user, :api_client).
index b89145b3f24335c7ef1756007764a7f99a47186e..b75479ff8d145f0b11712273df0970f3f3ff6dc6 100644 (file)
@@ -870,19 +870,4 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     }
     return return_obj
   end
-
-  ['zbbbb', 'z0000'].each do |token_valid_for|
-    test "validate #{token_valid_for}-salted token for remote cluster zbbbb" do
-      salted_token = salt_token(fixture: :active, remote: token_valid_for)
-      ArvadosApiToken.new.call("rack.input" => "",
-                               "HTTP_AUTHORIZATION" => "Bearer #{salted_token}")
-      get :current, {remote: 'zbbbb'}
-      if token_valid_for == 'zbbbb'
-        assert_equal(users(:active).uuid, json_response['uuid'])
-        assert_response 200
-      else
-        assert_response 401
-      end
-    end
-  end
 end
similarity index 69%
rename from services/api/test/functional/remote_user_account_test.rb
rename to services/api/test/integration/remote_user_test.rb
index 8b07be5f71be175e5781ddd4b5fbdf3241f0ed55..a7a789907739fbdd2bec21badd31842b93247502 100644 (file)
@@ -5,13 +5,13 @@
 require 'webrick'
 require 'webrick/https'
 require 'test_helper'
+require 'helpers/users_test_helper'
 
-class RemoteUserAccountTest < ActionController::TestCase
+class RemoteUsersTest < ActionDispatch::IntegrationTest
   def auth(remote:)
     token = salt_token(fixture: :active, remote: remote)
     token.sub!('/zzzzz-', '/'+remote+'-')
-    ArvadosApiToken.new.call("rack.input" => "",
-                             "HTTP_AUTHORIZATION" => "Bearer #{token}")
+    {"HTTP_AUTHORIZATION" => "Bearer #{token}"}
   end
 
   setup do
@@ -28,9 +28,9 @@ class RemoteUserAccountTest < ActionController::TestCase
       SSLEnable: true,
       SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
       SSLPrivateKey: OpenSSL::PKey::RSA.new(
-        File.open("self-signed.key").read),
+        File.open(Rails.root.join("tmp", "self-signed.key")).read),
       SSLCertificate: OpenSSL::X509::Certificate.new(
-        File.open("self-signed.pem").read),
+        File.open(Rails.root.join("tmp", "self-signed.pem")).read),
       SSLCertName: [["CN", WEBrick::Utils::getservername]],
       StartCallback: lambda { ready.push(true) })
     srv.mount_proc '/discovery/v1/apis/arvados/v1/rest' do |req, res|
@@ -59,12 +59,11 @@ class RemoteUserAccountTest < ActionController::TestCase
   end
 
   teardown do
-    @remote_server.stop
+    @remote_server.andand.stop
   end
 
   test 'authenticate with remote token' do
-    auth(remote: 'zbbbb')
-    get :current
+    get '/arvados/v1/users/current', {}, auth(remote: 'zbbbb')
     assert_response :success
     assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
     assert_equal false, json_response['is_admin']
@@ -72,8 +71,7 @@ class RemoteUserAccountTest < ActionController::TestCase
 
   test 'authenticate with remote token from wrong site' do
     @stub_content[:uuid] = 'zcccc-tpzed-000000000000000'
-    auth(remote: 'zbbbb')
-    get :current
+    get '/arvados/v1/users/current', {}, auth(remote: 'zbbbb')
     assert_response 401
   end
 
@@ -82,16 +80,29 @@ class RemoteUserAccountTest < ActionController::TestCase
     @stub_content = {
       error: 'not authorized',
     }
-    auth(remote: 'zbbbb')
-    get :current
+    get '/arvados/v1/users/current', {}, auth(remote: 'zbbbb')
     assert_response 401
   end
 
   test 'remote api server is not an api server' do
-    @stub_status = 401
+    @stub_status = 200
     @stub_content = '<html>bad</html>'
-    auth(remote: 'zbbbb')
-    get :current
+    get '/arvados/v1/users/current', {}, auth(remote: 'zbbbb')
     assert_response 401
   end
+
+  ['zbbbb', 'z0000'].each do |token_valid_for|
+    test "validate #{token_valid_for}-salted token for remote cluster zbbbb" do
+      salted_token = salt_token(fixture: :active, remote: token_valid_for)
+      get '/arvados/v1/users/current', {format: 'json', remote: 'zbbbb'}, {
+            "HTTP_AUTHORIZATION" => "Bearer #{salted_token}"
+          }
+      if token_valid_for == 'zbbbb'
+        assert_response 200
+        assert_equal(users(:active).uuid, json_response['uuid'])
+      else
+        assert_response 401
+      end
+    end
+  end
 end
index 0288e887f94e83361e59f60a7beba6a7cf62a493..8ddab3fee1eb6963dff5c34b3f2788fa09bcef1e 100644 (file)
@@ -216,5 +216,4 @@ class UsersTest < ActionDispatch::IntegrationTest
     end
     nil
   end
-
 end