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
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
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
# 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
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.
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
# 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
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
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
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
--- /dev/null
+class AddRuntimeTokenToContainer < ActiveRecord::Migration
+ def change
+ add_column :containers, :runtime_token, :text, :null => true
+ end
+end
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
);
INSERT INTO schema_migrations (version) VALUES ('20181005192222');
+INSERT INTO schema_migrations (version) VALUES ('20181011184200');
+
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
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 /"]
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
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
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
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
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
"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
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}
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}))
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!
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
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
end
test "Lock and unlock" do
+ set_user_from_auth :active
c, cr = minimal_new priority: 0
set_user_from_auth :dispatch1
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)
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)
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)
end
test "Container running cancel" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
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
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)
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
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
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
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
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
{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