11453: Authorize tokens issued by remote servers.
authorTom Clegg <tclegg@veritasgenetics.com>
Thu, 19 Oct 2017 18:10:45 +0000 (14:10 -0400)
committerTom Clegg <tclegg@veritasgenetics.com>
Thu, 19 Oct 2017 19:50:16 +0000 (15:50 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/app/middlewares/arvados_api_token.rb
services/api/app/models/api_client_authorization.rb
services/api/config/application.default.yml

index d5ba487d0f92e0506814585a44fcf98fd50ec31f..25736d31e7d017431b224a526e61671466f3097b 100644 (file)
@@ -17,7 +17,13 @@ class Arvados::V1::SchemaController < ApplicationController
 
   def index
     expires_in 24.hours, public: true
-    discovery = Rails.cache.fetch 'arvados_v1_rest_discovery' do
+    send_json discovery_doc
+  end
+
+  protected
+
+  def discovery_doc
+    Rails.cache.fetch 'arvados_v1_rest_discovery' do
       Rails.application.eager_load!
       discovery = {
         kind: "discovery#restDescription",
@@ -49,6 +55,8 @@ class Arvados::V1::SchemaController < ApplicationController
         crunchLogThrottleLines: Rails.application.config.crunch_log_throttle_lines,
         crunchLimitLogBytesPerJob: Rails.application.config.crunch_limit_log_bytes_per_job,
         crunchLogPartialLineThrottlePeriod: Rails.application.config.crunch_log_partial_line_throttle_period,
+        remoteHosts: Rails.configuration.remote_hosts,
+        remoteHostsViaDNS: Rails.configuration.remote_hosts_via_dns,
         websocketUrl: Rails.application.config.websocket_address,
         parameters: {
           alt: {
@@ -379,6 +387,5 @@ class Arvados::V1::SchemaController < ApplicationController
       end
       discovery
     end
-    send_json discovery
   end
 end
index dace94493059f63e0fc977172488281d34171258..be6bf0463c5168bd0efaa0e84aad078256e9732b 100644 (file)
@@ -28,18 +28,18 @@ class ArvadosApiToken
 
     auth = ApiClientAuthorization.
            validate(token: Thread.current[:supplied_token], remote: false)
-    if auth
-      auth.last_used_at = Time.now
-      auth.last_used_by_ip_address = remote_ip.to_s
-      auth.save validate: false
-    end
-
     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
     Thread.current[:api_client] = auth.andand.api_client
     Thread.current[:user] = auth.andand.user
 
+    if auth
+      auth.last_used_at = Time.now
+      auth.last_used_by_ip_address = remote_ip.to_s
+      auth.save validate: false
+    end
+
     @app.call env if @app
   end
 end
index eb9a8dc65012a466c258eed007adcc75a1989ec0..e7903d46ddc8465b0f04fceac66222193707bd06 100644 (file)
@@ -6,6 +6,7 @@ class ApiClientAuthorization < ArvadosModel
   include HasUuid
   include KindAndEtag
   include CommonApiTemplate
+  extend CurrentApiClient
 
   belongs_to :api_client
   belongs_to :user
@@ -82,6 +83,12 @@ class ApiClientAuthorization < ArvadosModel
     ["#{table_name}.id desc"]
   end
 
+  def self.remote_host(uuid:)
+    Rails.configuration.remote_hosts[uuid[0..4]] ||
+      (Rails.configuration.remote_hosts_via_dns &&
+       uuid[0..4]+".arvadosapi.com")
+  end
+
   def self.validate(token:, remote:)
     return nil if !token
     remote ||= Rails.configuration.uuid_prefix
@@ -97,6 +104,33 @@ 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)
+        remote_user = arv.user.current(remote_id: Rails.configuration.uuid_prefix)
+        if remote_user && remote_user[:uuid][0..4] == uuid[0..4]
+          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)
+            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
       end
     else
       auth = ApiClientAuthorization.
index 2f32556733b1a8a186bc3ad51a540518c485ac57..47b4bf1281511034f48b0842d7458d69cbf56562 100644 (file)
@@ -382,6 +382,19 @@ common:
   # original job reuse behavior, and is still the default).
   reuse_job_if_outputs_differ: false
 
+  ###
+  ### Federation support.
+  ###
+
+  # Map known prefixes to hosts. Example:
+  # remote_hosts:
+  #   zzzzz: zzzzz.example.com
+  remote_hosts: {}
+
+  # Use {prefix}.arvadosapi.com for any prefix not given in
+  # remote_hosts above.
+  remote_hosts_via_dns: true
+
   ###
   ### Remaining assorted configuration options.
   ###