14260: Added runtime_token to container record
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 11 Oct 2018 22:27:51 +0000 (18:27 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 11 Oct 2018 22:27:51 +0000 (18:27 -0400)
* runtime_token, runtime_user_uuid and runtime_auth_scopes are now
  part of container initialization and reuse decisions

* Determine runtime_user_uuid and runtime_auth_scopes as part of
  Container.resolve

* Use runtime_user_uuid to create container token (when runtime_token
  is not set)

* act_as runtime_user_uuid when resolving container request fields

* tokens used for runtime_token will be left untouched (remove expire_destroy)

* added/updated/fixed tests

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

12 files changed:
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/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/containers.yml
services/api/test/fixtures/links.yml
services/api/test/fixtures/users.yml
services/api/test/unit/container_request_test.rb
services/api/test/unit/container_test.rb

index 0a2793adedd108cffa5e06988c6a0d6ae180aca9..12ef8eb3eb5a2abede54c35919a8b72c815a357c 100644 (file)
@@ -92,27 +92,6 @@ class ApiClientAuthorization < ArvadosModel
        uuid_prefix+".arvadosapi.com")
   end
 
-  # Delete token, if remote, attempt to delete on the host as well.
-  def expire_destroy
-      uuid_prefix = self.uuid[0..4]
-      if uuid_prefix != Rails.configuration.uuid_prefix
-        # remote token
-        host = remote_host(uuid_prefix: uuid_prefix)
-        begin
-          clnt = HTTPClient.new
-          if Rails.configuration.sso_insecure
-            clnt.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
-          end
-          result = SafeJSON.load(
-            clnt.delete('https://' + host + '/arvados/v1/users/current',
-                        {'Authorization' => 'Bearer ' + token}))
-        rescue => e
-          Rails.logger.warn "deleting remote token #{self.uuid} failed: #{e}"
-        end
-      end
-      destroy
-  end
-
   def self.validate(token:, remote: nil)
     return nil if !token
     remote ||= Rails.configuration.uuid_prefix
index ba5c1c28caaa6f46e636e69047ccde248b63b81b..86201955aa6a4137f27835a65f5908e4765b5b63 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
@@ -91,15 +91,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 +146,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 +279,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.
@@ -415,7 +443,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
@@ -522,28 +551,30 @@ 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"
-    end
-    if cr.runtime_token.nil?
+    if self.runtime_token.nil?
+      if self.runtime_user_uuid.nil?
+        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(cr.modified_by_user_uuid).id,
-                            api_client_id: 0)
-      self.runtime_user_uuid = cr.modified_by_user_uuid
-      self.runtime_auth_scopes = self.auth.scopes
+                    create!(user_id: User.find_by_uuid(self.runtime_user_uuid).id,
+                            api_client_id: 0,
+                            scopes: self.runtime_auth_scopes)
     else
-      # using cr.runtime_token
-      self.auth = ApiClientAuthorization.validate(token: cr.runtime_token)
+      # using runtime_token
+      self.auth = ApiClientAuthorization.validate(token: self.runtime_token)
       if self.auth.nil?
         raise ArgumentError.new "Invalid runtime token"
       end
-      self.runtime_user_uuid = User.find_by_id(self.auth.user_id).uuid
-      self.runtime_auth_scopes = self.auth.scopes
     end
   end
 
@@ -569,12 +600,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
 
index b75775c87806239e58e28105818aeb3e1b746394..cd68517096cbabd40482a99539d91031f4bb91b7 100644 (file)
@@ -106,7 +106,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def skip_uuid_read_permission_check
-    # XXX temporary until permissions are sorted out.
+  # XXX temporary until permissions are sorted out.
     %w(modified_by_client_uuid container_uuid requesting_container_uuid)
   end
 
@@ -359,14 +359,7 @@ class ContainerRequest < ArvadosModel
   def scrub_secrets
     if self.state == Final
       self.secret_mounts = {}
-      if !self.runtime_token.nil?
-        _, uuid, secret = self.runtime_token.split('/')
-        tok = ApiClientAuthorization.find_by_uuid(uuid)
-        if !tok.nil?
-          tok.expire_destroy
-        end
-        self.runtime_token = nil
-      end
+      self.runtime_token = nil
     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 d1eb8d8d061f92bef23666d755f7e2edf35dccfb..636306f976e3b98a3368f0f1ed9f4dca4dd50287 100644 (file)
@@ -358,7 +358,8 @@ CREATE TABLE public.containers (
     secret_mounts_md5 character varying DEFAULT '99914b932bd37a50b983c5e7c90ae93b'::character varying,
     runtime_status jsonb DEFAULT '{}'::jsonb,
     runtime_user_uuid text,
-    runtime_auth_scopes jsonb
+    runtime_auth_scopes jsonb,
+    runtime_token text
 );
 
 
@@ -3176,3 +3177,5 @@ INSERT INTO schema_migrations (version) VALUES ('20180917205609');
 
 INSERT INTO schema_migrations (version) VALUES ('20181005192222');
 
+INSERT INTO schema_migrations (version) VALUES ('20181011184200');
+
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 9074c5ffc1d53c8e465d81f6ac415322c188ec07..d8ef63120bfc2a2eb6d938d0ed217cee2f5d7144 100644 (file)
@@ -345,6 +345,21 @@ foo_collection_sharing_token:
 container_runtime_token:
   uuid: zzzzz-gj3su-2nj68s291f50gd9
   api_client: untrusted
-  user: spectator
+  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 ce61c01eed054f7ccb53e284d9f5499f3b58e741..eefb0297c61650406a528e4c226d59f8634d99db 100644 (file)
@@ -271,6 +271,9 @@ runtime_token:
   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
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 00c341f74f3b9ce2ad656c68460697e7952195fb..14fa5796d0de06366b00a2e4525f98216f6a567f 100644 (file)
@@ -1076,8 +1076,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
   end
 
   test "using runtime_token" do
-    set_user_from_auth :active
-    spec = api_client_authorizations(:spectator)
+    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
@@ -1096,13 +1096,13 @@ class ContainerRequestTest < ActiveSupport::TestCase
     cr.reload
     c.reload
     assert_nil cr.runtime_token
-    assert_nil ApiClientAuthorization.find_by_uuid(spec.uuid)
+    assert_nil c.runtime_token
   end
 
   test "invalid runtime_token" do
     set_user_from_auth :active
     spec = api_client_authorizations(:spectator)
-    assert_raises(ActiveRecord::RecordInvalid) do
+    assert_raises(ArgumentError) do
       cr = create_minimal_req!(state: "Committed", runtime_token: "#{spec.token}xx")
       cr.save!
     end
index 11ae0bfe3b6b8d51e5c33555cbbbb8ed73128153..39fde79a331cd78131bfdc8b094125a74c281e8d 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)
@@ -596,10 +671,11 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container queued count" do
-    assert_equal 1, Container.readable_by(users(:active)).where(state: "Queued").count
+    assert_equal 2, Container.readable_by(users(:active)).where(state: "Queued").count
   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