Merge branch 'master' into 14260-runtime-token
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 18 Oct 2018 15:58:57 +0000 (11:58 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 18 Oct 2018 15:58:57 +0000 (11:58 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

25 files changed:
apps/workbench/app/models/container_work_unit.rb
apps/workbench/app/views/work_units/_show_status.html.erb
doc/api/methods/container_requests.html.textile.liquid
doc/api/methods/containers.html.textile.liquid
services/api/app/controllers/arvados/v1/containers_controller.rb
services/api/app/models/api_client_authorization.rb
services/api/app/models/container.rb
services/api/app/models/container_request.rb
services/api/db/migrate/20181005192222_add_container_runtime_token.rb [new file with mode: 0644]
services/api/db/migrate/20181011184200_add_runtime_token_to_container.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/lib/sweep_trashed_objects.rb
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/container_requests.yml
services/api/test/fixtures/containers.yml
services/api/test/fixtures/links.yml
services/api/test/fixtures/users.yml
services/api/test/functional/arvados/v1/container_requests_controller_test.rb
services/api/test/functional/arvados/v1/containers_controller_test.rb
services/api/test/integration/container_auth_test.rb [new file with mode: 0644]
services/api/test/integration/remote_user_test.rb
services/api/test/unit/api_client_authorization_test.rb
services/api/test/unit/container_request_test.rb
services/api/test/unit/container_test.rb
services/crunch-run/crunchrun.go

index 964295619af4bf3b738b565ff401007fb203a72b..ef20a7f8f49cfd7ed72f7ff9c51774ca36ddaa74 100644 (file)
@@ -23,7 +23,7 @@ class ContainerWorkUnit < ProxyWorkUnit
     items = []
     container_uuid = if @proxied.is_a?(Container) then uuid else get(:container_uuid) end
     if container_uuid
-      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts)
+      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts runtime_token)
       my_children = @child_proxies || ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).results if !my_children
       my_child_containers = my_children.map(&:container_uuid).compact.uniq
       grandchildren = {}
index b726f3b6b1459b55bc54453ba3811eec464fa15d..003948584afae697bfef7822fdb60c65e5335674 100644 (file)
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <%
     container_uuid = if @object.is_a?(Container) then @object.uuid elsif @object.is_a?(ContainerRequest) then @object.container_uuid end
     if container_uuid
-      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts)
+      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts runtime_token)
       reqs = ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).results
       load_preloaded_objects(reqs)
 
index e1e006a86a1e63e669cb204975e3c64ab8849f3f..8703e927327dfb100a4dda73932e3740df2c6388 100644 (file)
@@ -35,6 +35,7 @@ table(table table-bordered table-condensed).
 |container_uuid|string|The uuid of the container that satisfies this container_request. The system may return a preexisting Container that matches the container request criteria. See "Container reuse":#container_reuse for more details.|Container reuse is the default behavior, but may be disabled with @use_existing: false@ to always create a new container.|
 |container_count_max|integer|Maximum number of containers to start, i.e., the maximum number of "attempts" to be made.||
 |mounts|hash|Objects to attach to the container's filesystem and stdin/stdout.|See "Mount types":#mount_types for more details.|
+|secret_mounts|hash|Objects to attach to the container's filesystem.  Only "json" or "text" mount types allowed.|Not returned in API responses. Reset to empty when state is "Complete" or "Cancelled".|
 |runtime_constraints|hash|Restrict the container's access to compute resources and the outside world.|Required when in "Committed" state. e.g.,<pre><code>{
   "ram":12000000000,
   "vcpus":2,
@@ -56,6 +57,9 @@ table(table table-bordered table-condensed).
 |log_uuid|string|Log collection containing log messages provided by the scheduler and crunch processes.|Null if the container has not yet completed.|
 |output_uuid|string|Output collection created when the container finished successfully.|Null if the container has failed or not yet completed.|
 |filters|string|Additional constraints for satisfying the container_request, given in the same form as the filters parameter accepted by the container_requests.list API.|
+|runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc.  |Not returned in API responses.  Reset to null when state is "Complete" or "Cancelled".|
+|runtime_user_uuid|string|The user permission that will be granted to this container.||
+|runtime_auth_scopes|array of string|The scopes associated with the auth token used to run this container.||
 
 h2(#priority). Priority
 
@@ -79,7 +83,7 @@ h2(#scheduling_parameters). {% include 'container_scheduling_parameters' %}
 
 h2(#container_reuse). Container reuse
 
-When a container request is "Committed", the system will try to find and reuse an existing Container with the same command, cwd, environment, output_path, container_image, mounts, and runtime_constraints being requested. (Hashes in the serialized fields environment, mounts and runtime_constraints are compared without regard to key order.)
+When a container request is "Committed", the system will try to find and reuse an existing Container with the same command, cwd, environment, output_path, container_image, mounts, secret_mounts, runtime_constraints, runtime_user_uuid, and runtime_auth_scopes being requested. (Hashes in the serialized fields environment, mounts and runtime_constraints use normalized key order.)
 
 In order of preference, the system will use:
 * The first matching container to have finished successfully (i.e., reached state "Complete" with an exit_code of 0) whose log and output collections are still available.
index 61e2715223493539eed2aa9a1b3ad355599354a8..f0ce8e362f40ee0c533829b3c04adf7afa6ccf88 100644 (file)
@@ -34,6 +34,7 @@ table(table table-bordered table-condensed).
 |command|array of strings|Command to execute.| Must be equal to a ContainerRequest's command in order to satisfy the ContainerRequest.|
 |output_path|string|Path to a directory or file inside the container that should be preserved as this container's output when it finishes.|Must be equal to a ContainerRequest's output_path in order to satisfy the ContainerRequest.|
 |mounts|hash|Must contain the same keys as the ContainerRequest being satisfied. Each value must be within the range of values described in the ContainerRequest at the time the Container is assigned to the ContainerRequest.|See "Mount types":#mount_types for more details.|
+|secret_mounts|hash|Must contain the same keys as the ContainerRequest being satisfied. Each value must be within the range of values described in the ContainerRequest at the time the Container is assigned to the ContainerRequest.|Not returned in API responses. Reset to empty when state is "Complete" or "Cancelled".|
 |runtime_constraints|hash|Compute resources, and access to the outside world, that are / were available to the container.
 Generally this will contain additional keys that are not present in any corresponding ContainerRequests: for example, even if no ContainerRequests specified constraints on the number of CPU cores, the number of cores actually used will be recorded here.|e.g.,
 <pre><code>{
@@ -53,8 +54,9 @@ Generally this will contain additional keys that are not present in any correspo
 |progress|number|A number between 0.0 and 1.0 describing the fraction of work done.||
 |priority|integer|Range 0-1000.  Indicate scheduling order preference.|Currently assigned by the system as the max() of the priorities of all associated ContainerRequests.  See "container request priority":container_requests.html#priority .|
 |exit_code|integer|Process exit code.|Null if state!="Complete"|
-|auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc.|Null if state∉{"Locked","Running"}|
+|auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc.  Automatically assigned.|Null if state∉{"Locked","Running"} or if @runtime_token@ was provided.|
 |locked_by_uuid|string|UUID of a token, indicating which dispatch process changed state to Locked. If null, any token can be used to lock. If not null, only the indicated token can modify this container.|Null if state∉{"Locked","Running"}|
+|runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc.|Not returned in API responses.  Reset to null when state is "Complete" or "Cancelled".|
 
 h2(#container_states). Container states
 
index 65d8385ad5f5b47619f1e158e674564333e09433..0403b8822f1423e148df1eb76bcc707b46c712d6 100644 (file)
@@ -17,7 +17,14 @@ class Arvados::V1::ContainersController < ApplicationController
     if @object.locked_by_uuid != Thread.current[:api_client_authorization].uuid
       raise ArvadosModel::PermissionDeniedError.new("Not locked by your token")
     end
-    @object = @object.auth
+    if @object.runtime_token.nil?
+      @object = @object.auth
+    else
+      @object = ApiClientAuthorization.validate(token: @object.runtime_token)
+      if @object.nil?
+        raise ArvadosModel::PermissionDeniedError.new("Invalid runtime_token")
+      end
+    end
     show
   end
 
@@ -51,20 +58,19 @@ class Arvados::V1::ContainersController < ApplicationController
     if Thread.current[:api_client_authorization].nil?
       send_error("Not logged in", status: 401)
     else
-      c = Container.where(auth_uuid: Thread.current[:api_client_authorization].uuid).first
-      if c.nil?
+      c = Container.for_current_token
+      if c.nil? or c.first.nil?
         send_error("Token is not associated with a container.", status: 404)
       else
-        @object = c
+        @object = c.first
         show
       end
     end
   end
 
   def secret_mounts
-    if @object &&
-       @object.auth_uuid &&
-       @object.auth_uuid == Thread.current[:api_client_authorization].uuid
+    c = Container.for_current_token
+    if @object && c && @object.uuid == c.first.uuid
       send_json({"secret_mounts" => @object.secret_mounts})
     else
       send_error("Token is not associated with this container.", status: 403)
index 12ef8eb3eb5a2abede54c35919a8b72c815a357c..53ae6af46426cadd55bf7ec4ae1cc94659ef1c0f 100644 (file)
@@ -98,11 +98,31 @@ class ApiClientAuthorization < ArvadosModel
 
     case token[0..2]
     when 'v2/'
-      _, uuid, secret = token.split('/')
+      _, uuid, secret, optional = token.split('/')
       unless uuid.andand.length == 27 && secret.andand.length.andand > 0
         return nil
       end
 
+      if !optional.nil?
+        # if "optional" is a container uuid, check that it
+        # matches expections.
+        c = Container.where(uuid: optional).first
+        if !c.nil?
+          if !c.auth_uuid.nil? and c.auth_uuid != uuid
+            # token doesn't match the container's token
+            return nil
+          end
+          if !c.runtime_token.nil? and "v2/#{uuid}/#{secret}" != c.runtime_token
+            # token doesn't match the container's token
+            return nil
+          end
+          if ![Container::Locked, Container::Running].include?(c.state)
+            # container isn't locked or running, token shouldn't be used
+            return nil
+          end
+        end
+      end
+
       auth = ApiClientAuthorization.
              includes(:user, :api_client).
              where('uuid=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', uuid).
index 079ac4c29980406982ee849046c4e23dccbe7e5f..527881ba6e7d07b27c8a0c2ee4b465a20e823a85 100644 (file)
@@ -37,7 +37,7 @@ class Container < ArvadosModel
   after_validation :assign_auth
   before_save :sort_serialized_attrs
   before_save :update_secret_mounts_md5
-  before_save :scrub_secret_mounts
+  before_save :scrub_secrets
   before_save :clear_runtime_status_when_queued
   after_save :update_cr_logs
   after_save :handle_completed
@@ -67,6 +67,8 @@ class Container < ArvadosModel
     t.add :state
     t.add :auth_uuid
     t.add :scheduling_parameters
+    t.add :runtime_user_uuid
+    t.add :runtime_auth_scopes
   end
 
   # Supported states for a container
@@ -91,15 +93,15 @@ class Container < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["secret_mounts", "secret_mounts_md5"]
+    super - ["secret_mounts", "secret_mounts_md5", "runtime_token"]
   end
 
   def self.searchable_columns *args
-    super - ["secret_mounts_md5"]
+    super - ["secret_mounts_md5", "runtime_token"]
   end
 
   def logged_attributes
-    super.except('secret_mounts')
+    super.except('secret_mounts', 'runtime_token')
   end
 
   def state_transitions
@@ -146,17 +148,37 @@ class Container < ArvadosModel
   # Create a new container (or find an existing one) to satisfy the
   # given container request.
   def self.resolve(req)
-    c_attrs = {
-      command: req.command,
-      cwd: req.cwd,
-      environment: req.environment,
-      output_path: req.output_path,
-      container_image: resolve_container_image(req.container_image),
-      mounts: resolve_mounts(req.mounts),
-      runtime_constraints: resolve_runtime_constraints(req.runtime_constraints),
-      scheduling_parameters: req.scheduling_parameters,
-      secret_mounts: req.secret_mounts,
-    }
+    if req.runtime_token.nil?
+      runtime_user = if req.modified_by_user_uuid.nil?
+                       current_user
+                     else
+                       User.find_by_uuid(req.modified_by_user_uuid)
+                     end
+      runtime_auth_scopes = ["all"]
+    else
+      auth = ApiClientAuthorization.validate(token: req.runtime_token)
+      if auth.nil?
+        raise ArgumentError.new "Invalid runtime token"
+      end
+      runtime_user = User.find_by_id(auth.user_id)
+      runtime_auth_scopes = auth.scopes
+    end
+    c_attrs = act_as_user runtime_user do
+      {
+        command: req.command,
+        cwd: req.cwd,
+        environment: req.environment,
+        output_path: req.output_path,
+        container_image: resolve_container_image(req.container_image),
+        mounts: resolve_mounts(req.mounts),
+        runtime_constraints: resolve_runtime_constraints(req.runtime_constraints),
+        scheduling_parameters: req.scheduling_parameters,
+        secret_mounts: req.secret_mounts,
+        runtime_token: req.runtime_token,
+        runtime_user_uuid: runtime_user.uuid,
+        runtime_auth_scopes: runtime_auth_scopes
+      }
+    end
     act_as_system_user do
       if req.use_existing && (reusable = find_reusable(c_attrs))
         reusable
@@ -259,6 +281,14 @@ class Container < ArvadosModel
     candidates = candidates.where_serialized(:runtime_constraints, resolve_runtime_constraints(attrs[:runtime_constraints]), md5: true)
     log_reuse_info(candidates) { "after filtering on runtime_constraints #{attrs[:runtime_constraints].inspect}" }
 
+    candidates = candidates.where('runtime_user_uuid = ? or (runtime_user_uuid is NULL and runtime_auth_scopes is NULL)',
+                                  attrs[:runtime_user_uuid])
+    log_reuse_info(candidates) { "after filtering on runtime_user_uuid #{attrs[:runtime_user_uuid].inspect}" }
+
+    candidates = candidates.where('runtime_auth_scopes = ? or (runtime_user_uuid is NULL and runtime_auth_scopes is NULL)',
+                                  SafeJSON.dump(attrs[:runtime_auth_scopes]))
+    log_reuse_info(candidates) { "after filtering on runtime_auth_scopes #{attrs[:runtime_auth_scopes].inspect}" }
+
     log_reuse_info { "checking for state=Complete with readable output and log..." }
 
     select_readable_pdh = Collection.
@@ -362,6 +392,18 @@ class Container < ArvadosModel
     [Complete, Cancelled].include?(self.state)
   end
 
+  def self.for_current_token
+    _, _, _, container_uuid = Thread.current[:token].split('/')
+    if container_uuid.nil?
+      Container.where(auth_uuid: current_api_client_authorization.uuid)
+    else
+      Container.where('auth_uuid=? or (uuid=? and runtime_token=?)',
+                      current_api_client_authorization.uuid,
+                      container_uuid,
+                      current_api_client_authorization.token)
+    end
+  end
+
   protected
 
   def fill_field_defaults
@@ -415,7 +457,8 @@ class Container < ArvadosModel
       permitted.push(:owner_uuid, :command, :container_image, :cwd,
                      :environment, :mounts, :output_path, :priority,
                      :runtime_constraints, :scheduling_parameters,
-                     :secret_mounts)
+                     :secret_mounts, :runtime_token,
+                     :runtime_user_uuid, :runtime_auth_scopes)
     end
 
     case self.state
@@ -511,7 +554,7 @@ class Container < ArvadosModel
 
   def assign_auth
     if self.auth_uuid_changed?
-      return errors.add :auth_uuid, 'is readonly'
+         return errors.add :auth_uuid, 'is readonly'
     end
     if not [Locked, Running].include? self.state
       # don't need one
@@ -522,16 +565,29 @@ class Container < ArvadosModel
       # already have one
       return
     end
-    cr = ContainerRequest.
-      where('container_uuid=? and priority>0', self.uuid).
-      order('priority desc').
-      first
-    if !cr
-      return errors.add :auth_uuid, "cannot be assigned because priority <= 0"
+    if self.runtime_token.nil?
+      if self.runtime_user_uuid.nil?
+        # legacy behavior, we don't have a runtime_user_uuid so get
+        # the user from the highest priority container request, needed
+        # when performing an upgrade and there are queued containers,
+        # and some tests.
+        cr = ContainerRequest.
+               where('container_uuid=? and priority>0', self.uuid).
+               order('priority desc').
+               first
+        if !cr
+          return errors.add :auth_uuid, "cannot be assigned because priority <= 0"
+        end
+        self.runtime_user_uuid = cr.modified_by_user_uuid
+        self.runtime_auth_scopes = ["all"]
+      end
+
+      # generate a new token
+      self.auth = ApiClientAuthorization.
+                    create!(user_id: User.find_by_uuid(self.runtime_user_uuid).id,
+                            api_client_id: 0,
+                            scopes: self.runtime_auth_scopes)
     end
-    self.auth = ApiClientAuthorization.
-      create!(user_id: User.find_by_uuid(cr.modified_by_user_uuid).id,
-              api_client_id: 0)
   end
 
   def sort_serialized_attrs
@@ -556,12 +612,13 @@ class Container < ArvadosModel
     end
   end
 
-  def scrub_secret_mounts
+  def scrub_secrets
     # this runs after update_secret_mounts_md5, so the
     # secret_mounts_md5 will still reflect the secrets that are being
     # scrubbed here.
     if self.state_changed? && self.final?
       self.secret_mounts = {}
+      self.runtime_token = nil
     end
   end
 
@@ -593,7 +650,11 @@ class Container < ArvadosModel
             container_image: self.container_image,
             mounts: self.mounts,
             runtime_constraints: self.runtime_constraints,
-            scheduling_parameters: self.scheduling_parameters
+            scheduling_parameters: self.scheduling_parameters,
+            secret_mounts: self.secret_mounts_was,
+            runtime_token: self.runtime_token_was,
+            runtime_user_uuid: self.runtime_user_uuid,
+            runtime_auth_scopes: self.runtime_auth_scopes
           }
           c = Container.create! c_attrs
           retryable_requests.each do |cr|
index bbec4210846ca95e918611fd23189e03f7433893..61b65b1ed89f3fdeb505720d2a5e8d43aef34f3d 100644 (file)
@@ -38,7 +38,8 @@ class ContainerRequest < ArvadosModel
   validate :validate_state_change
   validate :check_update_whitelist
   validate :secret_mounts_key_conflict
-  before_save :scrub_secret_mounts
+  validate :validate_runtime_token
+  before_save :scrub_secrets
   before_create :set_requesting_container_uuid
   before_destroy :set_priority_zero
   after_save :update_priority
@@ -88,7 +89,7 @@ class ContainerRequest < ArvadosModel
   AttrsPermittedAlways = [:owner_uuid, :state, :name, :description, :properties]
   AttrsPermittedBeforeCommit = [:command, :container_count_max,
   :container_image, :cwd, :environment, :filters, :mounts,
-  :output_path, :priority,
+  :output_path, :priority, :runtime_token,
   :runtime_constraints, :state, :container_uuid, :use_existing,
   :scheduling_parameters, :secret_mounts, :output_name, :output_ttl]
 
@@ -97,7 +98,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def logged_attributes
-    super.except('secret_mounts')
+    super.except('secret_mounts', 'runtime_token')
   end
 
   def state_transitions
@@ -105,8 +106,12 @@ class ContainerRequest < ArvadosModel
   end
 
   def skip_uuid_read_permission_check
-    # XXX temporary until permissions are sorted out.
-    %w(modified_by_client_uuid container_uuid requesting_container_uuid)
+    # The uuid_read_permission_check prevents users from making
+    # references to objects they can't view.  However, in this case we
+    # don't want to do that check since there's a circular dependency
+    # where user can't view the container until the user has
+    # constructed the container request that references the container.
+    %w(container_uuid)
   end
 
   def finalize_if_needed
@@ -165,7 +170,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["mounts", "secret_mounts", "secret_mounts_md5"]
+    super - ["mounts", "secret_mounts", "secret_mounts_md5", "runtime_token"]
   end
 
   protected
@@ -343,9 +348,22 @@ class ContainerRequest < ArvadosModel
     end
   end
 
-  def scrub_secret_mounts
+  def validate_runtime_token
+    if !self.runtime_token.nil? && self.runtime_token_changed?
+      if !runtime_token[0..2] == "v2/"
+        errors.add :runtime_token, "not a v2 token"
+        return
+      end
+      if ApiClientAuthorization.validate(token: runtime_token).nil?
+        errors.add :runtime_token, "failed validation"
+      end
+    end
+  end
+
+  def scrub_secrets
     if self.state == Final
       self.secret_mounts = {}
+      self.runtime_token = nil
     end
   end
 
@@ -375,8 +393,9 @@ class ContainerRequest < ArvadosModel
   def get_requesting_container
     return self.requesting_container_uuid if !self.requesting_container_uuid.nil?
     return if !current_api_client_authorization
-    if (c = Container.where('auth_uuid=?', current_api_client_authorization.uuid).select([:uuid, :priority]).first)
-      return c
+    c = Container.for_current_token
+    if !c.nil?
+      c.select([:uuid, :priority]).first
     end
   end
 end
diff --git a/services/api/db/migrate/20181005192222_add_container_runtime_token.rb b/services/api/db/migrate/20181005192222_add_container_runtime_token.rb
new file mode 100644 (file)
index 0000000..07151cd
--- /dev/null
@@ -0,0 +1,7 @@
+class AddContainerRuntimeToken < ActiveRecord::Migration
+  def change
+    add_column :container_requests, :runtime_token, :text, :null => true
+    add_column :containers, :runtime_user_uuid, :text, :null => true
+    add_column :containers, :runtime_auth_scopes, :jsonb, :null => true
+  end
+end
diff --git a/services/api/db/migrate/20181011184200_add_runtime_token_to_container.rb b/services/api/db/migrate/20181011184200_add_runtime_token_to_container.rb
new file mode 100644 (file)
index 0000000..09201f5
--- /dev/null
@@ -0,0 +1,5 @@
+class AddRuntimeTokenToContainer < ActiveRecord::Migration
+  def change
+    add_column :containers, :runtime_token, :text, :null => true
+  end
+end
index 5b579bd39e1e8760cd3243bf7dbe50b206d131f4..b9db1add053cf53a1295f187a1b8d9244895ec99 100644 (file)
@@ -302,7 +302,8 @@ CREATE TABLE public.container_requests (
     log_uuid character varying(255),
     output_name character varying(255) DEFAULT NULL::character varying,
     output_ttl integer DEFAULT 0 NOT NULL,
-    secret_mounts jsonb DEFAULT '{}'::jsonb
+    secret_mounts jsonb DEFAULT '{}'::jsonb,
+    runtime_token text
 );
 
 
@@ -358,7 +359,10 @@ CREATE TABLE public.containers (
     scheduling_parameters text,
     secret_mounts jsonb DEFAULT '{}'::jsonb,
     secret_mounts_md5 character varying DEFAULT '99914b932bd37a50b983c5e7c90ae93b'::character varying,
-    runtime_status jsonb DEFAULT '{}'::jsonb
+    runtime_status jsonb DEFAULT '{}'::jsonb,
+    runtime_user_uuid text,
+    runtime_auth_scopes jsonb,
+    runtime_token text
 );
 
 
@@ -3185,6 +3189,10 @@ INSERT INTO schema_migrations (version) VALUES ('20180915155335');
 
 INSERT INTO schema_migrations (version) VALUES ('20180917205609');
 
+INSERT INTO schema_migrations (version) VALUES ('20181005192222');
+
+INSERT INTO schema_migrations (version) VALUES ('20181011184200');
+
 INSERT INTO schema_migrations (version) VALUES ('20180919001158');
 
 INSERT INTO schema_migrations (version) VALUES ('20181001175023');
index 59008c0fc38067a3bf3ece9a885c0bfebfa2a438..162bebf5130cfa81973dc3244a06900a4d8ef6f5 100644 (file)
@@ -48,6 +48,11 @@ module SweepTrashedObjects
         where({group_class: 'project'}).
         where('is_trashed = false and trash_at < statement_timestamp()').
         update_all('is_trashed = true')
+
+      # Sweep expired tokens
+      ApiClientAuthorization.
+        where("expires_at <= statement_timestamp()").
+        destroy_all
     end
   end
 
index 2073d8b1bacccfaa0422643a34ddfe5ed0144461..d8ef63120bfc2a2eb6d938d0ed217cee2f5d7144 100644 (file)
@@ -341,3 +341,25 @@ foo_collection_sharing_token:
   - GET /arvados/v1/collections/zzzzz-4zz18-znfnqtbbv4spc3w
   - GET /arvados/v1/collections/zzzzz-4zz18-znfnqtbbv4spc3w/
   - GET /arvados/v1/keep_services/accessible
+
+container_runtime_token:
+  uuid: zzzzz-gj3su-2nj68s291f50gd9
+  api_client: untrusted
+  user: container_runtime_token_user
+  api_token: 2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+  expires_at: 2038-01-01 00:00:00
+
+crt_user:
+  uuid: zzzzz-gj3su-3r47qqy5ja5d54v
+  api_client: untrusted
+  user: container_runtime_token_user
+  api_token: 13z1tz9deoryml3twep0vsahi4862097pe5lsmesugnkgpgpwk
+  expires_at: 2038-01-01 00:00:00
+
+runtime_token_limited_scope:
+  uuid: zzzzz-gj3su-2fljvypjrr4yr9m
+  api_client: untrusted
+  user: container_runtime_token_user
+  api_token: 1fwc3be1m13qkypix2gd01i4bq5ju483zjfc0cf4babjseirbm
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /"]
index 5d3531eead8fb5a90c7ef4b7ef750a937da6ee90..dea98887e9843866b182a7ca054aa60628223fd7 100644 (file)
@@ -764,6 +764,26 @@ cr_in_trashed_project:
     vcpus: 1
     ram: 123
 
+runtime_token:
+  uuid: zzzzz-xvhdp-11eklkhy0n4dm86
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  name: queued
+  state: Committed
+  priority: 1
+  created_at: <%= 2.minute.ago.to_s(:db) %>
+  updated_at: <%= 1.minute.ago.to_s(:db) %>
+  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  container_uuid: zzzzz-dz642-20isqbkl8xwnsao
+  runtime_token: v2/zzzzz-gj3su-2nj68s291f50gd9/2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+  runtime_constraints:
+    vcpus: 1
+    ram: 123
+
 
 # Test Helper trims the rest of the file
 
index 757adcee1b979af4086d937cc928c1abb5042a1e..5c5d45f4bc0c5a880775d638bc133752992a454d 100644 (file)
@@ -259,3 +259,29 @@ running_to_be_deleted:
   auth_uuid: zzzzz-gj3su-ty6lvu9d7u7c2sq
   secret_mounts: {}
   secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
+
+runtime_token:
+  uuid: zzzzz-dz642-20isqbkl8xwnsao
+  owner_uuid: zzzzz-tpzed-000000000000000
+  state: Locked
+  locked_by_uuid: zzzzz-gj3su-jrriu629zljsnuf
+  priority: 1
+  created_at: 2016-01-11 11:11:11.111111111 Z
+  updated_at: 2016-01-11 11:11:11.111111111 Z
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  runtime_token: v2/zzzzz-gj3su-2nj68s291f50gd9/2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+  runtime_user_uuid: zzzzz-tpzed-l3skomkti0c4vg4
+  runtime_auth_scopes: ["all"]
+  runtime_constraints:
+    ram: 12000000000
+    vcpus: 4
+  mounts:
+    /tmp:
+      kind: tmp
+      capacity: 24000000000
+    /var/spool/cwl:
+      kind: tmp
+      capacity: 24000000000
index 8a33f696a958e56330aaa4cf6c9bbd0e19624ec4..2b247a960d989e962b373b726878828bc008d105 100644 (file)
@@ -597,6 +597,20 @@ active_user_permission_to_unlinked_docker_image_collection:
   head_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
   properties: {}
 
+crt_user_permission_to_unlinked_docker_image_collection:
+  uuid: zzzzz-o0j2j-20zvdi9b4odcfz3
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2014-01-24 20:42:26 -0800
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-000000000000000
+  modified_at: 2014-01-24 20:42:26 -0800
+  updated_at: 2014-01-24 20:42:26 -0800
+  tail_uuid: zzzzz-tpzed-l3skomkti0c4vg4
+  link_class: permission
+  name: can_read
+  head_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
+  properties: {}
+
 docker_image_collection_hash:
   uuid: zzzzz-o0j2j-dockercollhasha
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
index 8d2586921958570d97104b3fdd8bcefb8e51112f..7d6b1fc3aef2a6a7e5d4b95dffff1c63100d15bb 100644 (file)
@@ -165,6 +165,22 @@ spectator:
       role: Computational biologist
     getting_started_shown: 2015-03-26 12:34:56.789000000 Z
 
+container_runtime_token_user:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-l3skomkti0c4vg4
+  email: spectator@arvados.local
+  first_name: Spect
+  last_name: Ator
+  identity_url: https://spectator.openid.local
+  is_active: true
+  is_admin: false
+  username: containerruntimetokenuser
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
+    getting_started_shown: 2015-03-26 12:34:56.789000000 Z
+
 inactive_uninvited:
   owner_uuid: zzzzz-tpzed-000000000000000
   uuid: zzzzz-tpzed-rf2ec3ryh4vb5ma
index 282e09049e63beab2e591ac71f38b47e9484261d..a3252ad7b3fcdaa8fc78294bbb29b794a7107986 100644 (file)
@@ -81,4 +81,21 @@ class Arvados::V1::ContainerRequestsControllerTest < ActionController::TestCase
     req.reload
     assert_equal 'bar', req.secret_mounts['/foo']['content']
   end
+
+  test "runtime_token not in #create responses" do
+    authorize_with :active
+
+    post :create, {
+           container_request: minimal_cr.merge(
+             runtime_token: api_client_authorizations(:spectator).token)
+         }
+    assert_response :success
+
+    resp = JSON.parse(@response.body)
+    refute resp.has_key?('runtime_token')
+
+    req = ContainerRequest.where(uuid: resp['uuid']).first
+    assert_equal api_client_authorizations(:spectator).token, req.runtime_token
+  end
+
 end
index 8e2002c75919a68f27b64718e50279907339ce7d..452533b9e9a13e93b05f4c28be484e5c8ca23f98 100644 (file)
@@ -151,4 +151,14 @@ class Arvados::V1::ContainersControllerTest < ActionController::TestCase
       end
     end
   end
+
+  test 'get runtime_token auth' do
+    authorize_with :dispatch2
+    c = containers(:runtime_token)
+    get :auth, id: c.uuid
+    assert_response :success
+    assert_equal "v2/#{json_response['uuid']}/#{json_response['api_token']}", api_client_authorizations(:container_runtime_token).token
+    assert_equal 'arvados#apiClientAuthorization', json_response['kind']
+  end
+
 end
diff --git a/services/api/test/integration/container_auth_test.rb b/services/api/test/integration/container_auth_test.rb
new file mode 100644 (file)
index 0000000..552cce4
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'test_helper'
+
+class ContainerAuthTest < ActionDispatch::IntegrationTest
+  fixtures :all
+
+  test "container token validate, Running, regular auth" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:running_container_auth).token}/#{containers(:running).uuid}"}
+    # Container is Running, token can be used
+    assert_response :success
+    assert_equal containers(:running).uuid, json_response['uuid']
+  end
+
+  test "container token validate, Locked, runtime_token" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:runtime_token).uuid}"}
+    # Container is Running, token can be used
+    assert_response :success
+    assert_equal containers(:runtime_token).uuid, json_response['uuid']
+  end
+
+  test "container token validate, Cancelled, runtime_token" do
+    put "/arvados/v1/containers/#{containers(:runtime_token).uuid}", {
+          :format => :json,
+          :container => {:state => "Cancelled"}
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:dispatch1).token}"}
+    assert_response :success
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:runtime_token).uuid}"}
+    # Container is Queued, token cannot be used
+    assert_response 401
+  end
+
+  test "container token validate, Running, without optional portion" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:running_container_auth).token}"}
+    # Container is Running, token can be used
+    assert_response :success
+    assert_equal containers(:running).uuid, json_response['uuid']
+  end
+
+  test "container token validate, Locked, runtime_token, without optional portion" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}"}
+    # runtime_token without container uuid won't return 'current'
+    assert_response 404
+  end
+
+  test "container token validate, wrong container uuid" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:running).uuid}"}
+    # Container uuid mismatch, token can't be used
+    assert_response 401
+  end
+end
index c38c230b2276609c6ce21ccf581f4e710854167d..0e61db7bcd9d5cc0cb185c4766a2e597c6d6ed4a 100644 (file)
@@ -251,4 +251,37 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     assert_equal 'barney', json_response['username']
   end
 
+  test "validate unsalted v2 token for remote cluster zbbbb" do
+    auth = api_client_authorizations(:active)
+    token = "v2/#{auth.uuid}/#{auth.api_token}"
+    get '/arvados/v1/users/current', {format: 'json', remote: 'zbbbb'}, {
+          "HTTP_AUTHORIZATION" => "Bearer #{token}"
+        }
+    assert_response :success
+    assert_equal(users(:active).uuid, json_response['uuid'])
+  end
+
+  test 'container request with runtime_token' do
+    [["valid local", "v2/#{api_client_authorizations(:active).uuid}/#{api_client_authorizations(:active).api_token}"],
+     ["valid remote", "v2/zbbbb-gj3su-000000000000000/abc"],
+     ["invalid local", "v2/#{api_client_authorizations(:active).uuid}/fakefakefake"],
+     ["invalid remote", "v2/zbork-gj3su-000000000000000/abc"],
+    ].each do |label, runtime_token|
+      post '/arvados/v1/container_requests', {
+             "container_request" => {
+               "command" => ["echo"],
+               "container_image" => "xyz",
+               "output_path" => "/",
+               "cwd" => "/",
+               "runtime_token" => runtime_token
+             }
+           }, {"HTTP_AUTHORIZATION" => "Bearer #{api_client_authorizations(:active).api_token}"}
+      if label.include? "invalid"
+        assert_response 422
+      else
+        assert_response :success
+      end
+    end
+  end
+
 end
index 51a6ff3ba857821bacf831b6a08cf3804b4dc316..c390a02c04ef1ce705fa23f7a26aa2a42a93b51b 100644 (file)
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'test_helper'
+require 'sweep_trashed_objects'
 
 class ApiClientAuthorizationTest < ActiveSupport::TestCase
   include CurrentApiClient
@@ -18,4 +19,11 @@ class ApiClientAuthorizationTest < ActiveSupport::TestCase
       assert_empty ApiClientAuthorization.where(api_token: newtoken), "Destroyed ApiClientAuth is still in database"
     end
   end
+
+  test "delete expired in SweepTrashedObjects" do
+    assert_not_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+    SweepTrashedObjects.sweep_now
+    assert_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+  end
+
 end
index 81b49ff4fcce525b5e7fba88ff0c6f78087e7686..8ff216e28caf8a598c5b6fbbf46a9d342e4a7c35 100644 (file)
@@ -380,7 +380,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
   [
     ['running_container_auth', 'zzzzz-dz642-runningcontainr', 501],
-    ['active_no_prefs', nil, 0],
+    ['active_no_prefs', nil, 0]
   ].each do |token, expected, expected_priority|
     test "create as #{token} and expect requesting_container_uuid to be #{expected}" do
       set_user_from_auth token
@@ -391,6 +391,15 @@ class ContainerRequestTest < ActiveSupport::TestCase
     end
   end
 
+  test "create as container_runtime_token and expect requesting_container_uuid to be zzzzz-dz642-20isqbkl8xwnsao" do
+    set_user_from_auth :container_runtime_token
+    Thread.current[:token] = "#{Thread.current[:token]}/zzzzz-dz642-20isqbkl8xwnsao"
+    cr = ContainerRequest.create(container_image: "img", output_path: "/tmp", command: ["echo", "foo"])
+    assert_not_nil cr.uuid, 'uuid should be set for newly created container_request'
+    assert_equal 'zzzzz-dz642-20isqbkl8xwnsao', cr.requesting_container_uuid
+    assert_equal 1, cr.priority
+  end
+
   [[{"vcpus" => [2, nil]},
     lambda { |resolved| resolved["vcpus"] == 2 }],
    [{"vcpus" => [3, 7]},
@@ -668,6 +677,49 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_not_equal cr2.container_uuid, cr.container_uuid
   end
 
+  test "Retry on container cancelled with runtime_token" do
+    set_user_from_auth :spectator
+    spec = api_client_authorizations(:active)
+    cr = create_minimal_req!(priority: 1, state: "Committed",
+                             runtime_token: spec.token,
+                             container_count_max: 2)
+    prev_container_uuid = cr.container_uuid
+
+    c = act_as_system_user do
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal spec.token, c.runtime_token
+      c.update_attributes!(state: Container::Locked)
+      c.update_attributes!(state: Container::Running)
+      c
+    end
+
+    cr.reload
+    assert_equal "Committed", cr.state
+    assert_equal prev_container_uuid, cr.container_uuid
+    prev_container_uuid = cr.container_uuid
+
+    act_as_system_user do
+      c.update_attributes!(state: Container::Cancelled)
+    end
+
+    cr.reload
+    assert_equal "Committed", cr.state
+    assert_not_equal prev_container_uuid, cr.container_uuid
+    prev_container_uuid = cr.container_uuid
+
+    c = act_as_system_user do
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal spec.token, c.runtime_token
+      c.update_attributes!(state: Container::Cancelled)
+      c
+    end
+
+    cr.reload
+    assert_equal "Final", cr.state
+    assert_equal prev_container_uuid, cr.container_uuid
+
+  end
+
   test "Output collection name setting using output_name with name collision resolution" do
     set_user_from_auth :active
     output_name = 'unimaginative name'
@@ -1074,4 +1126,38 @@ class ContainerRequestTest < ActiveSupport::TestCase
                                              secret_mounts: sm)
     assert_equal [:secret_mounts], cr.errors.messages.keys
   end
+
+  test "using runtime_token" do
+    set_user_from_auth :spectator
+    spec = api_client_authorizations(:active)
+    cr = create_minimal_req!(state: "Committed", runtime_token: spec.token, priority: 1)
+    cr.save!
+    c = Container.find_by_uuid cr.container_uuid
+    lock_and_run c
+    assert_nil c.auth_uuid
+    assert_equal c.runtime_token, spec.token
+
+    assert_not_nil ApiClientAuthorization.find_by_uuid(spec.uuid)
+
+    act_as_system_user do
+      c.update_attributes!(state: Container::Complete,
+                           exit_code: 0,
+                           output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
+                           log: 'fa7aeb5140e2848d39b416daeef4ffc5+45')
+    end
+
+    cr.reload
+    c.reload
+    assert_nil cr.runtime_token
+    assert_nil c.runtime_token
+  end
+
+  test "invalid runtime_token" do
+    set_user_from_auth :active
+    spec = api_client_authorizations(:spectator)
+    assert_raises(ArgumentError) do
+      cr = create_minimal_req!(state: "Committed", runtime_token: "#{spec.token}xx")
+      cr.save!
+    end
+  end
 end
index 11ae0bfe3b6b8d51e5c33555cbbbb8ed73128153..491022ad8d5a9cd6e47e1cf7727a5cba92d54ce4 100644 (file)
@@ -33,14 +33,18 @@ class ContainerTest < ActiveSupport::TestCase
       "var" => "val",
     },
     secret_mounts: {},
+    runtime_user_uuid: "zzzzz-tpzed-xurymjxw79nv3jz",
+    runtime_auth_scopes: ["all"]
   }
 
+  def request_only attrs
+    attrs.reject {|k| [:runtime_user_uuid, :runtime_auth_scopes].include? k}
+  end
+
   def minimal_new attrs={}
-    cr = ContainerRequest.new DEFAULT_ATTRS.merge(attrs)
+    cr = ContainerRequest.new request_only(DEFAULT_ATTRS.merge(attrs))
     cr.state = ContainerRequest::Committed
-    act_as_user users(:active) do
-      cr.save!
-    end
+    cr.save!
     c = Container.find_by_uuid cr.container_uuid
     assert_not_nil c
     return c, cr
@@ -220,6 +224,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container serialized hash attributes sorted before save" do
+    set_user_from_auth :active
     env = {"C" => "3", "B" => "2", "A" => "1"}
     m = {"F" => {"kind" => "3"}, "E" => {"kind" => "2"}, "D" => {"kind" => "1"}}
     rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1}
@@ -236,6 +241,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "find_reusable method should select higher priority queued container" do
+        Rails.configuration.log_reuse_decisions = true
     set_user_from_auth :active
     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment:{"var" => "queued"}})
     c_low_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:1}))
@@ -285,13 +291,13 @@ class ContainerTest < ActiveSupport::TestCase
       log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
     }
 
-    cr = ContainerRequest.new common_attrs
+    cr = ContainerRequest.new request_only(common_attrs)
     cr.use_existing = false
     cr.state = ContainerRequest::Committed
     cr.save!
     c_output1 = Container.where(uuid: cr.container_uuid).first
 
-    cr = ContainerRequest.new common_attrs
+    cr = ContainerRequest.new request_only(common_attrs)
     cr.use_existing = false
     cr.state = ContainerRequest::Committed
     cr.save!
@@ -312,7 +318,8 @@ class ContainerTest < ActiveSupport::TestCase
     c_output2.update_attributes!({state: Container::Running})
     c_output2.update_attributes!(completed_attrs.merge({log: log1, output: out2}))
 
-    reused = Container.resolve(ContainerRequest.new(common_attrs))
+    set_user_from_auth :active
+    reused = Container.resolve(ContainerRequest.new(request_only(common_attrs)))
     assert_equal c_output1.uuid, reused.uuid
   end
 
@@ -507,7 +514,73 @@ class ContainerTest < ActiveSupport::TestCase
     Container.find_reusable(REUSABLE_COMMON_ATTRS)
   end
 
+  def runtime_token_attr tok
+    auth = api_client_authorizations(tok)
+    {runtime_user_uuid: User.find_by_id(auth.user_id).uuid,
+     runtime_auth_scopes: auth.scopes,
+     runtime_token: auth.token}
+  end
+
+  test "find_reusable method with same runtime_token" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:container_runtime_token).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_not_nil reused
+    assert_equal reused.uuid, c1.uuid
+  end
+
+  test "find_reusable method with different runtime_token, same user" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:crt_user).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_not_nil reused
+    assert_equal reused.uuid, c1.uuid
+  end
+
+  test "find_reusable method with nil runtime_token, then runtime_token with same user" do
+    set_user_from_auth :crt_user
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs)
+    assert_equal Container::Queued, c1.state
+    assert_equal users(:container_runtime_token_user).uuid, c1.runtime_user_uuid
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_not_nil reused
+    assert_equal reused.uuid, c1.uuid
+  end
+
+  test "find_reusable method with different runtime_token, different user" do
+    set_user_from_auth :crt_user
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:active).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_nil reused
+  end
+
+  test "find_reusable method with nil runtime_token, then runtime_token with different user" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: nil}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_nil reused
+  end
+
+  test "find_reusable method with different runtime_token, different scope, same user" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:runtime_token_limited_scope).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_nil reused
+  end
+
   test "Container running" do
+    set_user_from_auth :active
     c, _ = minimal_new priority: 1
 
     set_user_from_auth :dispatch1
@@ -527,6 +600,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Lock and unlock" do
+    set_user_from_auth :active
     c, cr = minimal_new priority: 0
 
     set_user_from_auth :dispatch1
@@ -587,6 +661,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container queued cancel" do
+    set_user_from_auth :active
     c, cr = minimal_new({container_count_max: 1})
     set_user_from_auth :dispatch1
     assert c.update_attributes(state: Container::Cancelled), show_errors(c)
@@ -600,6 +675,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container locked cancel" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     assert c.lock, show_errors(c)
@@ -608,6 +684,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container locked cancel with log" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     assert c.lock, show_errors(c)
@@ -619,6 +696,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container running cancel" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -641,6 +719,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container only set exit code on complete" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -653,6 +732,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "locked_by_uuid can update log when locked/running, and output when running" do
+    set_user_from_auth :active
     logcoll = collections(:real_log_collection)
     c, cr1 = minimal_new
     cr2 = ContainerRequest.new(DEFAULT_ATTRS)
@@ -698,6 +778,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "auth_uuid can set output, progress, runtime_status, state on running container -- but not log" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -718,6 +799,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "not allowed to set output that is not readable by current user" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -732,6 +814,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "other token cannot set output on running container" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -742,6 +825,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "can set trashed output on running container" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -755,6 +839,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "not allowed to set trashed output that is not readable by current user" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -774,20 +859,24 @@ class ContainerTest < ActiveSupport::TestCase
     {state: Container::Complete, exit_code: 0, output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'},
     {state: Container::Cancelled},
   ].each do |final_attrs|
-    test "secret_mounts is null after container is #{final_attrs[:state]}" do
+    test "secret_mounts and runtime_token are null after container is #{final_attrs[:state]}" do
+      set_user_from_auth :active
       c, cr = minimal_new(secret_mounts: {'/secret' => {'kind' => 'text', 'content' => 'foo'}},
-                          container_count_max: 1)
+                          container_count_max: 1, runtime_token: api_client_authorizations(:active).token)
       set_user_from_auth :dispatch1
       c.lock
       c.update_attributes!(state: Container::Running)
       c.reload
       assert c.secret_mounts.has_key?('/secret')
+      assert_equal api_client_authorizations(:active).token, c.runtime_token
 
       c.update_attributes!(final_attrs)
       c.reload
       assert_equal({}, c.secret_mounts)
+      assert_nil c.runtime_token
       cr.reload
       assert_equal({}, cr.secret_mounts)
+      assert_nil cr.runtime_token
       assert_no_secrets_logged
     end
   end
index be98a3ee113b7c30879f3039474534ea295f8b17..44560b80a0973e78488b2bf2dbb898877d98891f 100644 (file)
@@ -1432,7 +1432,7 @@ func (runner *ContainerRunner) ContainerToken() (string, error) {
        if err != nil {
                return "", err
        }
-       runner.token = auth.APIToken
+       runner.token = fmt.Sprintf("v2/%s/%s/%s", auth.UUID, auth.APIToken, runner.Container.UUID)
        return runner.token, nil
 }