Merge branch '13430-allow-storage-classes'
authorFuad Muhic <fmuhic@capeannenterprises.com>
Wed, 23 May 2018 13:07:39 +0000 (15:07 +0200)
committerFuad Muhic <fmuhic@capeannenterprises.com>
Wed, 23 May 2018 13:07:39 +0000 (15:07 +0200)
refs #13430

Arvados-DCO-1.1-Signed-off-by: Fuad Muhic <fmuhic@capeannenterprises.com>

24 files changed:
apps/workbench/app/controllers/users_controller.rb
apps/workbench/app/models/user.rb
apps/workbench/app/views/layouts/body.html.erb
apps/workbench/app/views/users/inactive.html.erb
apps/workbench/app/views/users/link_account.html.erb [new file with mode: 0644]
apps/workbench/config/routes.rb
apps/workbench/test/integration/link_account_test.rb [new file with mode: 0644]
build/build.list
build/run-build-packages.sh
doc/_config.yml
doc/admin/migrating-providers.html.textile.liquid [new file with mode: 0644]
doc/user/topics/link-accounts.html.textile.liquid [new file with mode: 0644]
sdk/R/R/RESTService.R
sdk/R/tests/testthat/fakes/FakeHttpRequest.R
sdk/cwl/arvados_cwl/arvtool.py
sdk/cwl/setup.py
sdk/cwl/tests/test_job.py
services/api/app/controllers/user_sessions_controller.rb
services/api/app/models/user.rb
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/users.yml
services/api/test/integration/user_sessions_test.rb
services/keep-balance/main.go
services/keep-balance/usage.go

index 2e3ced69a534485ca5d18df22b19ac53abeea793..8cfc2c10f1c29eee67a11074acfabe70da19aaba 100644 (file)
@@ -4,8 +4,8 @@
 
 class UsersController < ApplicationController
   skip_around_filter :require_thread_api_token, only: :welcome
-  skip_before_filter :check_user_agreements, only: [:welcome, :inactive]
-  skip_before_filter :check_user_profile, only: [:welcome, :inactive, :profile]
+  skip_before_filter :check_user_agreements, only: [:welcome, :inactive, :link_account, :merge]
+  skip_before_filter :check_user_profile, only: [:welcome, :inactive, :profile, :link_account, :merge]
   skip_before_filter :find_object_by_uuid, only: [:welcome, :activity, :storage]
   before_filter :ensure_current_user_is_admin, only: [:sudo, :unsetup, :setup]
 
@@ -317,6 +317,11 @@ class UsersController < ApplicationController
     RequestShellAccessReporter.send_request(current_user, params).deliver
   end
 
+  def merge
+    User.merge params[:new_user_token], params[:direction]
+    redirect_to "/"
+  end
+
   protected
 
   def find_current_links user
index 1f102dbf17acd3fb807110c34f4937686ebb9f2d..865ff6e9519cacf613b248df446fd4a1e0b24636 100644 (file)
@@ -14,6 +14,47 @@ class User < ArvadosBase
     arvados_api_client.unpack_api_response(res)
   end
 
+  def self.merge new_user_token, direction
+    # Merge user accounts.
+    #
+    # If the direction is "in", the current user is merged into the
+    # user represented by new_user_token
+    #
+    # If the direction is "out", the user represented by new_user_token
+    # is merged into the current user.
+
+    if direction == "in"
+      user_a = new_user_token
+      user_b = Thread.current[:arvados_api_token]
+      new_group_name = "Migrated from #{Thread.current[:user].email} (#{Thread.current[:user].uuid})"
+    elsif direction == "out"
+      user_a = Thread.current[:arvados_api_token]
+      user_b = new_user_token
+      res = arvados_api_client.api self, '/current', nil, {:arvados_api_token => user_b}, false
+      user_b_info = arvados_api_client.unpack_api_response(res)
+      new_group_name = "Migrated from #{user_b_info.email} (#{user_b_info.uuid})"
+    else
+      raise "Invalid merge direction, expected 'in' or 'out'"
+    end
+
+    # Create a project owned by user_a to accept everything owned by user_b
+    res = arvados_api_client.api Group, nil, {:group => {
+                                                :name => new_group_name,
+                                                :group_class => "project"},
+                                              :ensure_unique_name => true},
+                                 {:arvados_api_token => user_a}, false
+    target = arvados_api_client.unpack_api_response(res)
+
+    # The merge API merges the "current" user (user_b) into the user
+    # represented by "new_user_token" (user_a).
+    # After merging, the user_b redirects to user_a.
+    res = arvados_api_client.api self, '/merge', {:new_user_token => user_a,
+                                                  :new_owner_uuid => target[:uuid],
+                                                  :redirect_to_new_user => true},
+                                 {:arvados_api_token => user_b}, false
+    arvados_api_client.unpack_api_response(res)
+  end
+
   def self.system
     @@arvados_system_user ||= begin
                                 res = arvados_api_client.api self, '/system'
index f4be7cad63ab282f2332235e89cd848d6bfb1771..124a78577f3e5cac875569c8912217d65b8fc1ce 100644 (file)
@@ -93,7 +93,8 @@ SPDX-License-Identifier: AGPL-3.0 %>
                   <%= link_to ssh_keys_user_path(current_user), role: 'menu-item' do %>
                     <i class="fa fa-lg fa-key fa-fw"></i> SSH keys
                   <% end %>
-                </li>
+</li>
+                <li role="menuitem"><a href="/users/link_account" role="menuitem"><i class="fa fa-lg fa-link fa-fw"></i> Link account </a></li>
                 <% if Rails.configuration.user_profile_form_fields %>
                   <li role="menuitem"><a href="/users/<%=current_user.uuid%>/profile" role="menuitem"><i class="fa fa-lg fa-user fa-fw"></i> Manage profile</a></li>
                 <% end %>
index 389044f92fc2a3ab19337ae95a0f3815950c4314..f3cb3cf5cae7d25bde0ed590a0f81c45a37447f3 100644 (file)
@@ -25,6 +25,11 @@ SPDX-License-Identifier: AGPL-3.0 %>
         <%= link_to 'Retry', (params[:return_to] || '/'), class: 'btn btn-primary' %>
 
       </p>
+
+      <p>
+       Already have an account with a different login?  <a href="/users/link_account">Link this login to your existing account.</a>
+      </p>
+
     </div>
   </div>
 </div>
diff --git a/apps/workbench/app/views/users/link_account.html.erb b/apps/workbench/app/views/users/link_account.html.erb
new file mode 100644 (file)
index 0000000..61063b8
--- /dev/null
@@ -0,0 +1,108 @@
+<%= javascript_tag do %>
+  function update_visibility() {
+    if (sessionStorage.getItem('link_account_api_token') &&
+      sessionStorage.getItem('link_account_uuid') != '<%= Thread.current[:user].uuid %>')
+    {
+      $("#ready-to-link").css({"display": "inherit"});
+      $("#need-login").css({"display": "none"});
+
+      <% if params[:direction] == "in" %>
+      var user_a = "<b>"+sessionStorage.getItem('link_account_email')+"</b> ("+sessionStorage.getItem('link_account_username')+", "+sessionStorage.getItem('link_account_uuid')+")";
+      var user_b = "<b><%= Thread.current[:user].email %></b> (<%= Thread.current[:user].username%>, <%= Thread.current[:user].uuid%>)";
+      var user_a_is_active = (sessionStorage.getItem('link_account_is_active') == "true");
+      var user_a_is_admin = (sessionStorage.getItem('link_account_is_admin') == "true");
+      var user_b_is_admin = <%=if Thread.current[:user].is_admin then "true" else "false" end %>;
+      <% else %>
+      var user_a = "<b><%= Thread.current[:user].email %></b> (<%= Thread.current[:user].username%>, <%= Thread.current[:user].uuid%>)";
+      var user_b = "<b>"+sessionStorage.getItem('link_account_email')+"</b> ("+sessionStorage.getItem('link_account_username')+", "+sessionStorage.getItem('link_account_uuid')+")";
+      var user_a_is_active = <%= Thread.current[:user].is_active %>;
+      var user_a_is_admin = <%=if Thread.current[:user].is_admin then "true" else "false" end %>;
+      var user_b_is_admin = (sessionStorage.getItem('link_account_is_admin') == "true");
+      <% end %>
+
+      $("#new-user-token-input").val(sessionStorage.getItem('link_account_api_token'));
+
+      if (!user_a_is_active) {
+        $("#will-link-to").html("<p>Cannot link "+user_b+" to inactive account "+user_a+".</p>");
+        $("#link-account-submit").prop("disabled", true);
+      } else if (user_b_is_admin && !user_a_is_admin) {
+        $("#will-link-to").html("<p>Cannot link admin account "+user_b+" to non-admin account "+user_a+".</p>");
+        $("#link-account-submit").prop("disabled", true);
+      } else {
+        $("#will-link-to").html("<p>Clicking 'Link accounts' will link "+user_b+" created on <%=Thread.current[:user].created_at%> to "+
+          user_a+" created at <b>"+sessionStorage.getItem('link_account_created_at')+"</b>.</p>"+
+          "<p>After linking, logging in as "+user_b+" will log you into the same account as "+user_a+
+          ".</p>  <p>Any objects owned by "+user_b+" will be transferred to "+user_a+".</p>");
+      }
+    } else {
+      $("#ready-to-link").css({"display": "none"});
+      $("#need-login").css({"display": "inherit"});
+    }
+
+    sessionStorage.removeItem('link_account_api_token');
+    sessionStorage.removeItem('link_account_uuid');
+    sessionStorage.removeItem('link_account_email');
+    sessionStorage.removeItem('link_account_username');
+    sessionStorage.removeItem('link_account_created_at');
+    sessionStorage.removeItem('link_account_is_active');
+    sessionStorage.removeItem('link_account_is_admin');
+  };
+
+  $(window).on("load", function() {
+    update_visibility();
+  });
+
+  function do_login(dir) {
+    sessionStorage.setItem('link_account_api_token', '<%= Thread.current[:arvados_api_token] %>');
+    sessionStorage.setItem('link_account_email', '<%= Thread.current[:user].email %>');
+    sessionStorage.setItem('link_account_username', '<%= Thread.current[:user].username %>');
+    sessionStorage.setItem('link_account_uuid', '<%= Thread.current[:user].uuid %>');
+    sessionStorage.setItem('link_account_created_at', '<%= Thread.current[:user].created_at %>');
+    sessionStorage.setItem('link_account_is_active', <%= if Thread.current[:user].is_active then "true" else "false" end %>);
+    sessionStorage.setItem('link_account_is_admin', <%= if Thread.current[:user].is_admin then "true" else "false" end %>);
+    window.location.replace('<%=arvados_api_client.arvados_logout_url(return_to: arvados_api_client.arvados_login_url(return_to: "#{strip_token_from_path(request.url)}?direction="))%>'+dir);
+  }
+
+  $(document).on("click", "#link-account-in", function(e) { do_login("in"); });
+  $(document).on("click", "#link-account-out", function(e) { do_login("out"); });
+
+  $(document).on("click", "#cancel-link-accounts", function() {
+    window.location.replace('/users/link_account?api_token='+$("#new-user-token-input").val());
+  });
+<% end %>
+
+<div id="need-login" style="display: none">
+
+  <p>You are currently logged in as <b><%= Thread.current[:user].email %></b> (<%= Thread.current[:user].username%>, <%= Thread.current[:user].uuid %>) created at <b><%= Thread.current[:user].created_at%></b></p>
+
+<p>You can link Arvados accounts.  After linking, either login will take you to the same account.</p>
+
+  <p>
+    <% if Thread.current[:user].is_active %>
+  <button class="btn btn-primary" id="link-account-in" style="margin-right: 1em">
+    <i class="fa fa-fw fa-sign-in"></i> Add another login to this account
+  </button>
+  <% end %>
+  <button class="btn btn-primary" id="link-account-out" style="margin-right: 1em">
+    <i class="fa fa-fw fa-sign-in"></i> Use this login to access another account
+  </button>
+
+</p>
+</div>
+
+<div id="ready-to-link" style="display: none">
+
+  <div id="will-link-to"></div>
+
+  <%= button_tag "Cancel", class: "btn btn-cancel pull-left", id: "cancel-link-accounts", style: "margin-right: 1em" %>
+
+  <%= form_tag do |f| %>
+    <input type="hidden" id="new-user-token-input" name="new_user_token" value="" />
+    <input type="hidden" id="new-user-token-input" name="direction" value="<%=params[:direction]%>" />
+    <%= button_tag class: "btn btn-primary", id: "link-account-submit" do %>
+      <i class="fa fa-fw fa-link"></i> Link accounts
+  <% end %>
+<% end %>
+
+</div>
+</div>
index d969abd78c2b69d8de936e2a00df0c0d1f1ef0f1..718adfd2ed0583a99f8eebb221b5eae0c7d012c3 100644 (file)
@@ -65,6 +65,8 @@ ArvadosWorkbench::Application.routes.draw do
     get 'virtual_machines', :on => :member
     get 'repositories', :on => :member
     get 'ssh_keys', :on => :member
+    get 'link_account', :on => :collection
+    post 'link_account', :on => :collection, :action => :merge
   end
   get '/current_token' => 'users#current_token'
   get "/add_ssh_key_popup" => 'users#add_ssh_key_popup', :as => :add_ssh_key_popup
@@ -109,7 +111,7 @@ ArvadosWorkbench::Application.routes.draw do
     get 'tab_counts', on: :member
     get 'public', on: :collection
   end
-  
+
   resources :search do
     get 'choose', :on => :collection
   end
@@ -131,9 +133,9 @@ ArvadosWorkbench::Application.routes.draw do
   match '/_health/ping', to: 'healthcheck#ping', via: [:get]
 
   get '/tests/mithril', to: 'tests#mithril'
-  
+
   get '/status', to: 'status#status'
-  
+
   # Send unroutable requests to an arbitrary controller
   # (ends up at ApplicationController#render_not_found)
   match '*a', to: 'links#render_not_found', via: [:get, :post]
diff --git a/apps/workbench/test/integration/link_account_test.rb b/apps/workbench/test/integration/link_account_test.rb
new file mode 100644 (file)
index 0000000..9c22f5a
--- /dev/null
@@ -0,0 +1,172 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'integration_helper'
+require 'webrick'
+
+class LinkAccountTest < ActionDispatch::IntegrationTest
+  setup do
+    need_javascript
+  end
+
+  def start_sso_stub token
+    port = available_port('sso_stub')
+
+    s = WEBrick::HTTPServer.new(
+      :Port => port,
+      :BindAddress => 'localhost',
+      :Logger => WEBrick::Log.new('/dev/null', WEBrick::BasicLog::DEBUG),
+      :AccessLog => [nil,nil]
+    )
+
+    s.mount_proc("/login"){|req, res|
+      res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, req.query["return_to"] + "&api_token=#{token}")
+      s.shutdown
+    }
+
+    s.mount_proc("/logout"){|req, res|
+      res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, req.query["return_to"])
+    }
+
+    Thread.new do
+      s.start
+    end
+
+    "http://localhost:#{port}/"
+  end
+
+  test "Add another login to this account" do
+    visit page_with_token('active_trustedclient')
+    stub = start_sso_stub(api_fixture('api_client_authorizations')['project_viewer_trustedclient']['api_token'])
+    Rails.configuration.arvados_login_base = stub + "login"
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+
+    find("a", text: "Link account").click
+    find("button", text: "Add another login to this account").click
+
+    find("#notifications-menu").click
+    assert_text "project-viewer@arvados.local"
+
+    find("button", text: "Link accounts").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+  end
+
+  test "Use this login to access another account" do
+    visit page_with_token('project_viewer_trustedclient')
+    stub = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
+    Rails.configuration.arvados_login_base = stub + "login"
+
+    find("#notifications-menu").click
+    assert_text "project-viewer@arvados.local"
+
+    find("a", text: "Link account").click
+    find("button", text: "Use this login to access another account").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+
+    find("button", text: "Link accounts").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+  end
+
+  test "Link login of inactive user to this account" do
+    visit page_with_token('active_trustedclient')
+    stub = start_sso_stub(api_fixture('api_client_authorizations')['inactive_uninvited_trustedclient']['api_token'])
+    Rails.configuration.arvados_login_base = stub + "login"
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+
+    find("a", text: "Link account").click
+    find("button", text: "Add another login to this account").click
+
+    find("#notifications-menu").click
+    assert_text "inactive-uninvited-user@arvados.local"
+
+    find("button", text: "Link accounts").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+  end
+
+  test "Cannot link to inactive user" do
+    visit page_with_token('active_trustedclient')
+    stub = start_sso_stub(api_fixture('api_client_authorizations')['inactive_uninvited_trustedclient']['api_token'])
+    Rails.configuration.arvados_login_base = stub + "login"
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+
+    find("a", text: "Link account").click
+    find("button", text: "Use this login to access another account").click
+
+    find("#notifications-menu").click
+    assert_text "inactive-uninvited-user@arvados.local"
+
+    assert_text "Cannot link active-user@arvados.local"
+
+    assert find("#link-account-submit")['disabled']
+
+    find("button", text: "Cancel").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+  end
+
+  test "Inactive user can link to active account" do
+    visit page_with_token('inactive_uninvited_trustedclient')
+    stub = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
+    Rails.configuration.arvados_login_base = stub + "login"
+
+    find("#notifications-menu").click
+    assert_text "inactive-uninvited-user@arvados.local"
+
+    assert_text "Already have an account with a different login?"
+
+    find("a", text: "Link this login to your existing account").click
+
+    assert_no_text "Add another login to this account"
+
+    find("button", text: "Use this login to access another account").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+
+    find("button", text: "Link accounts").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+  end
+
+  test "Admin cannot link to non-admin" do
+    visit page_with_token('admin_trustedclient')
+    stub = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
+    Rails.configuration.arvados_login_base = stub + "login"
+
+    find("#notifications-menu").click
+    assert_text "admin@arvados.local"
+
+    find("a", text: "Link account").click
+    find("button", text: "Use this login to access another account").click
+
+    find("#notifications-menu").click
+    assert_text "active-user@arvados.local"
+
+    assert_text "Cannot link admin account admin@arvados.local"
+
+    assert find("#link-account-submit")['disabled']
+
+    find("button", text: "Cancel").click
+
+    find("#notifications-menu").click
+    assert_text "admin@arvados.local"
+  end
+
+end
index e994a2d669eeaed62438f57cfd68bcbb4458ee93..3d98fafb449b77f19bf074a7c8fa84ba88535d4f 100644 (file)
@@ -42,9 +42,9 @@ centos7|pbr|0.11.1|2|python|all
 centos7|pyparsing|2.1.10|2|python|all
 centos7|keepalive|0.5|2|python|all
 debian8,debian9,ubuntu1404,ubuntu1604,centos7|lockfile|0.12.2|2|python|all|--epoch 1
-debian8,debian9,ubuntu1404,ubuntu1604,centos7|subprocess32|3.5.0rc1|2|python|all
+debian8,debian9,ubuntu1404,ubuntu1604,centos7|subprocess32|3.5.1|2|python|all
 all|ruamel.yaml|0.14.12|2|python|amd64|--python-setup-py-arguments --single-version-externally-managed
-all|cwltest|1.0.20180416154033|4|python|all|--depends 'python-futures >= 3.0.5' --depends 'python-subprocess32'
+all|cwltest|1.0.20180518074130|4|python|all|--depends 'python-futures >= 3.0.5' --depends 'python-subprocess32 >= 3.5.0'
 all|junit-xml|1.8|3|python|all
 all|rdflib-jsonld|0.4.0|2|python|all
 all|futures|3.0.5|2|python|all
index fb37d53774982f7704c44ad71ce5de329b2fc64c..63f81832f0abecf4688cef2a65fa16bda31d691e 100755 (executable)
@@ -352,7 +352,7 @@ else
 fi
 test_package_presence ${PYTHON2_PKG_PREFIX}-arvados-cwl-runner "$arvados_cwl_runner_version" python "$arvados_cwl_runner_iteration"
 if [[ "$?" == "0" ]]; then
-  fpm_build $WORKSPACE/sdk/cwl "${PYTHON2_PKG_PREFIX}-arvados-cwl-runner" 'Curoverse, Inc.' 'python' "$arvados_cwl_runner_version" "--url=https://arvados.org" "--description=The Arvados CWL runner" --depends "${PYTHON2_PKG_PREFIX}-setuptools" --depends "${PYTHON2_PKG_PREFIX}-subprocess32 >= 3.5.0rc1" --depends "${PYTHON2_PKG_PREFIX}-pathlib2" --depends "${PYTHON2_PKG_PREFIX}-scandir" "${iterargs[@]}"
+  fpm_build $WORKSPACE/sdk/cwl "${PYTHON2_PKG_PREFIX}-arvados-cwl-runner" 'Curoverse, Inc.' 'python' "$arvados_cwl_runner_version" "--url=https://arvados.org" "--description=The Arvados CWL runner" --depends "${PYTHON2_PKG_PREFIX}-setuptools" --depends "${PYTHON2_PKG_PREFIX}-subprocess32 >= 3.5.0" --depends "${PYTHON2_PKG_PREFIX}-pathlib2" --depends "${PYTHON2_PKG_PREFIX}-scandir" "${iterargs[@]}"
 fi
 
 # schema_salad. This is a python dependency of arvados-cwl-runner,
index 78b11b769206fb49b5a1dbe50f2b282933a734af..aaa09e5f541e5e1b400d66b72e8549c55f1821e0 100644 (file)
@@ -50,6 +50,7 @@ navbar:
       - user/cwl/cwl-extensions.html.textile.liquid
       - user/topics/arv-docker.html.textile.liquid
     - Reference:
+      - user/topics/link-accounts.html.textile.liquid
       - user/reference/cookbook.html.textile.liquid
     - Arvados License:
       - user/copying/copying.html.textile.liquid
@@ -148,6 +149,7 @@ navbar:
       - admin/upgrading.html.textile.liquid
       - install/cheat_sheet.html.textile.liquid
       - user/topics/arvados-sync-groups.html.textile.liquid
+      - admin/migrating-providers.html.textile.liquid
       - admin/merge-remote-account.html.textile.liquid
       - install/migrate-docker19.html.textile.liquid
   installguide:
diff --git a/doc/admin/migrating-providers.html.textile.liquid b/doc/admin/migrating-providers.html.textile.liquid
new file mode 100644 (file)
index 0000000..9231dc2
--- /dev/null
@@ -0,0 +1,41 @@
+---
+layout: default
+navsection: admin
+title: "Migrating account providers"
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+This page describes how to enable users to use more than one provider to log into the same Arvados account.  This can be used to migrate account providers, for example, from LDAP to Google.  In order to do this, users must be able to log into both the "old" and "new" providers.
+
+h2. Configure multiple providers in SSO
+
+In @application.yml@ for the SSO server, enable both @google_oauth2@ and @ldap@ providers:
+
+<pre>
+production:
+  google_oauth2_client_id: abcd
+  google_oauth2_client_secret: abcd
+
+  use_ldap:
+    title: Example LDAP
+    host: ldap.example.com
+    port: 636
+    method: ssl
+    base: "ou=Users, dc=example, dc=com"
+    uid: uid
+    username: uid
+</pre>
+
+Restart the SSO server after changing the configuration.
+
+h2. Link accounts
+
+Instruct users to go through the process of "linking accounts":{{site.baseurl}}/user/topics/link-accounts.html
+
+After linking accounts, users can use the new provider to access their existing Arvados account.
+
+Once all users have migrated, the old account provider can be removed from the SSO configuration.
diff --git a/doc/user/topics/link-accounts.html.textile.liquid b/doc/user/topics/link-accounts.html.textile.liquid
new file mode 100644 (file)
index 0000000..3854bf6
--- /dev/null
@@ -0,0 +1,38 @@
+---
+layout: default
+navsection: userguide
+title: "Linking alternate login accounts"
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+This page describes how to link additional login accounts to the same Arvados account.  This can be used to migrate login accounts, for example, from one Google account to another.  It can also be used to migrate login providers, for example from LDAP to Google.  In order to do this, you must be able to log into both the "old" and "new" accounts.
+
+h2. Link accounts
+
+Follow this process to link the "new" login to the "old" login.
+
+# Log in using the "old" account
+# Under the users menu, choose *Link account*
+# On the link accounts page, press the button *Add another login to this account*
+# Follow login instructions from the login provider (eg Google)
+# You will be returned to the *Link accounts* confirmation page.
+# Press the *Link account* button to confirm.
+# After the accounts are linked, you will be returned to the dashboard.
+# Both the "old" and "new" logins will now log in to the same Arvados account.
+
+h2. Link accounts (alternate flow)
+
+You can also link accounts starting with logging into the "new" account first.
+
+# Log in using the "new" account
+# Under the users menu, choose *Link account* (if the user is inactive, there will be a link on the inactive user page)
+# On the link accounts page, press the button *Use this login to access another account*
+# Follow login instructions from the login provider (eg Google)
+# You will be returned to the *Link accounts* confirmation page.
+# Press the *Link account* button to confirm.
+# After the accounts are linked, you will be returned to the dashboard.
+# Both the "old" and "new" logins will now log in to the same Arvados account.
index dacf88a8c4bff2336e232fba4a86567a2cc5d7af..c215cf3595fc6dd211920f65ed5cb05c22b2346a 100644 (file)
@@ -37,8 +37,8 @@ RESTService <- R6::R6Class(
 
                 headers <- list(Authorization = paste("OAuth2", self$token))
 
-                serverResponse <- self$http$execute("GET", discoveryDocumentURL, headers,
-                                                    retryTimes = self$numRetries)
+                serverResponse <- self$http$exec("GET", discoveryDocumentURL, headers,
+                                                 retryTimes = self$numRetries)
 
                 discoveryDocument <- self$httpParser$parseJSONResponse(serverResponse)
                 private$webDavHostName <- discoveryDocument$keepWebServiceUrl
@@ -64,8 +64,8 @@ RESTService <- R6::R6Class(
                               uuid, "/", relativePath);
             headers <- list(Authorization = paste("OAuth2", self$token)) 
 
-            serverResponse <- self$http$execute("DELETE", fileURL, headers,
-                                                retryTimes = self$numRetries)
+            serverResponse <- self$http$exec("DELETE", fileURL, headers,
+                                             retryTimes = self$numRetries)
 
             if(serverResponse$status_code < 200 || serverResponse$status_code >= 300)
                 stop(paste("Server code:", serverResponse$status_code))
@@ -82,8 +82,8 @@ RESTService <- R6::R6Class(
             headers <- list("Authorization" = paste("OAuth2", self$token),
                            "Destination" = toURL)
 
-            serverResponse <- self$http$execute("MOVE", fromURL, headers,
-                                                retryTimes = self$numRetries)
+            serverResponse <- self$http$exec("MOVE", fromURL, headers,
+                                             retryTimes = self$numRetries)
 
             if(serverResponse$status_code < 200 || serverResponse$status_code >= 300)
                 stop(paste("Server code:", serverResponse$status_code))
@@ -98,8 +98,8 @@ RESTService <- R6::R6Class(
 
             headers <- list("Authorization" = paste("OAuth2", self$token))
 
-            response <- self$http$execute("PROPFIND", collectionURL, headers,
-                                          retryTimes = self$numRetries)
+            response <- self$http$exec("PROPFIND", collectionURL, headers,
+                                       retryTimes = self$numRetries)
 
             if(all(response == ""))
                 stop("Response is empty, request may be misconfigured")
@@ -119,8 +119,8 @@ RESTService <- R6::R6Class(
 
             headers <- list("Authorization" = paste("OAuth2", self$token))
 
-            response <- self$http$execute("PROPFIND", subcollectionURL, headers,
-                                          retryTimes = self$numRetries)
+            response <- self$http$exec("PROPFIND", subcollectionURL, headers,
+                                       retryTimes = self$numRetries)
 
             if(all(response == ""))
                 stop("Response is empty, request may be misconfigured")
@@ -156,8 +156,8 @@ RESTService <- R6::R6Class(
             if(!(contentType %in% self$httpParser$validContentTypes))
                 stop("Invalid contentType. Please use text or raw.")
 
-            serverResponse <- self$http$execute("GET", fileURL, headers,
-                                                retryTimes = self$numRetries)
+            serverResponse <- self$http$exec("GET", fileURL, headers,
+                                             retryTimes = self$numRetries)
 
             if(serverResponse$status_code < 200 || serverResponse$status_code >= 300)
                 stop(paste("Server code:", serverResponse$status_code))
@@ -173,8 +173,8 @@ RESTService <- R6::R6Class(
                             "Content-Type" = contentType)
             body <- content
 
-            serverResponse <- self$http$execute("PUT", fileURL, headers, body,
-                                                retryTimes = self$numRetries)
+            serverResponse <- self$http$exec("PUT", fileURL, headers, body,
+                                             retryTimes = self$numRetries)
 
             if(serverResponse$status_code < 200 || serverResponse$status_code >= 300)
                 stop(paste("Server code:", serverResponse$status_code))
@@ -210,8 +210,8 @@ RESTService <- R6::R6Class(
                             "Content-Type" = contentType)
             body <- NULL
 
-            serverResponse <- self$http$execute("PUT", fileURL, headers, body,
-                                                retryTimes = self$numRetries)
+            serverResponse <- self$http$exec("PUT", fileURL, headers, body,
+                                             retryTimes = self$numRetries)
 
             if(serverResponse$status_code < 200 || serverResponse$status_code >= 300)
                 stop(paste("Server code:", serverResponse$status_code))
index 533602886ab09e0d34a49e2829acaf73a9051baa..c4dbc669ce9f746f2051dff2c79b654e40797200 100644 (file)
@@ -56,8 +56,8 @@ FakeHttpRequest <- R6::R6Class(
             self$serverMaxElementsPerRequest <- 5
         },
 
-        execute = function(verb, url, headers = NULL, body = NULL, query = NULL,
-                           limit = NULL, offset = NULL, retryTimes = 0)
+        exec = function(verb, url, headers = NULL, body = NULL, query = NULL,
+                        limit = NULL, offset = NULL, retryTimes = 0)
         {
             private$validateURL(url)
             private$validateHeaders(headers)
index 8268300e75b66d6f82b999f649003ec3a0615bbf..fea6adfacc323539d7c2cd595f66d441859893b8 100644 (file)
@@ -39,6 +39,7 @@ class ArvadosCommandTool(CommandLineTool):
         # Workaround for #13365
         builderargs = kwargs.copy()
         builderargs["toplevel"] = True
+        builderargs["tmp_outdir_prefix"] = ""
         builder = self._init_job(joborder, **builderargs)
         joborder = builder.job
 
index 696837a366e51672eb8fbda1abf34c083da7f466..4df89ee75583f55a4c7d7cafb5b404dfe2c08467 100644 (file)
@@ -33,7 +33,7 @@ setup(name='arvados-cwl-runner',
       # Note that arvados/build/run-build-packages.sh looks at this
       # file to determine what version of cwltool and schema-salad to build.
       install_requires=[
-          'cwltool==1.0.20180508202931',
+          'cwltool==1.0.20180522135731',
           'schema-salad==2.7.20180501211602',
           'typing==3.5.3.0',
           'ruamel.yaml >=0.13.11, <0.15',
index 6d2598edaa8e4bdf2894c83a106c171d34fd1937..abf96947eb1b40f21207e0fc9171f66482a0bd47 100644 (file)
@@ -346,7 +346,7 @@ class TestWorkflow(unittest.TestCase):
                                               basedir="", make_fs_access=make_fs_access, loader=document_loader,
                                               makeTool=runner.arv_make_tool, metadata=metadata)
         arvtool.formatgraph = None
-        it = arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access)
+        it = arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access, tmp_outdir_prefix="")
         it.next().run()
         it.next().run()
 
@@ -432,7 +432,7 @@ class TestWorkflow(unittest.TestCase):
                                               basedir="", make_fs_access=make_fs_access, loader=document_loader,
                                               makeTool=runner.arv_make_tool, metadata=metadata)
         arvtool.formatgraph = None
-        it = arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access)
+        it = arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access, tmp_outdir_prefix="")
         it.next().run()
         it.next().run()
 
index 20633153e758c70f5b91d0b66466a06e6393b2da..b8fe2948923582ad9f40f3ec00c394cd6b2473ec 100644 (file)
@@ -70,7 +70,7 @@ class UserSessionsController < ApplicationController
       end
 
       while (uuid = user.redirect_to_user_uuid)
-        user = User.where(uuid: uuid).first
+        user = User.unscoped.where(uuid: uuid).first
         if !user
           raise Exception.new("identity_url #{omniauth['info']['identity_url']} redirects to nonexistent uuid #{uuid}")
         end
index 831036fd9d9cd722e7e84aa668bb75d5e111d6fc..9d4c20af9faaa1ff7076fdcd0bd8d0348324e4ef 100644 (file)
@@ -414,7 +414,7 @@ class User < ArvadosModel
     end
     if self.is_active_changed?
       if self.is_active != self.is_active_was
-        logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
+        logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_active_was} to #{self.is_active} for #{self.uuid}"
         self.is_active = self.is_active_was
       end
     end
index 8d9fc53c04d2c6bc4ccaea2009d72a178c6c11c7..92bd7cf872cfeca1c53d38c5ea05d7836e929f4f 100644 (file)
@@ -183,6 +183,13 @@ inactive_uninvited:
   api_token: 62mhllc0otp78v08e3rpa3nsmf8q8ogk47f7u5z4erp5gpj9al
   expires_at: 2038-01-01 00:00:00
 
+inactive_uninvited_trustedclient:
+  uuid: zzzzz-gj3su-228z32aux8dg2s1
+  api_client: trusted_workbench
+  user: inactive_uninvited
+  api_token: 7s29oj2hzmcmpq80hx9cta0rl5wuf3xfd6r7disusaptz7h9m0
+  expires_at: 2038-01-01 00:00:00
+
 inactive_but_signed_user_agreement:
   uuid: zzzzz-gj3su-247z32aux8dg2s1
   api_client: untrusted
index 8fb800c5f94f8a93bdc2f3990282ea76df7bb51b..8d2586921958570d97104b3fdd8bcefb8e51112f 100644 (file)
@@ -365,3 +365,37 @@ permission_perftest:
       organization: example.com
       role: IT
     getting_started_shown: 2015-03-26 12:34:56.789000000 Z
+
+redirects_to_active:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-1au3is3g3chtthd
+  email: redirects-to-active-user@arvados.local
+  first_name: Active2
+  last_name: User2
+  identity_url: https://redirects-to-active-user.openid.local
+  is_active: true
+  is_admin: false
+  username: redirect_active
+  redirect_to_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
+    getting_started_shown: 2015-03-26 12:34:56.789000000 Z
+
+double_redirects_to_active:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-oiusowoxoz0pk3p
+  email: double-redirects-to-active-user@arvados.local
+  first_name: Active3
+  last_name: User3
+  identity_url: https://double-redirects-to-active-user.openid.local
+  is_active: true
+  is_admin: false
+  username: double_redirect_active
+  redirect_to_user_uuid: zzzzz-tpzed-1au3is3g3chtthd
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
+    getting_started_shown: 2015-03-26 12:34:56.789000000 Z
index 6f9cf7edcbb6bb13b561cba11cc772c1ff7be097..0497c6a7d56294ae3d0841db5acd8ef9a441d809 100644 (file)
@@ -9,7 +9,7 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
     'https://wb.example.com'
   end
 
-  def mock_auth_with(email: nil, username: nil)
+  def mock_auth_with(email: nil, username: nil, identity_url: nil)
     mock = {
       'provider' => 'josh_id',
       'uid' => 'https://edward.example.com',
@@ -22,6 +22,7 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
     }
     mock['info']['email'] = email unless email.nil?
     mock['info']['username'] = username unless username.nil?
+    mock['info']['identity_url'] = identity_url unless identity_url.nil?
     post('/auth/josh_id/callback',
          {return_to: client_url},
          {'omniauth.auth' => mock})
@@ -40,6 +41,24 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
     assert_equal 'foo', u.username
   end
 
+  test 'existing user login' do
+    mock_auth_with(identity_url: "https://active-user.openid.local")
+    u = assigns(:user)
+    assert_equal 'zzzzz-tpzed-xurymjxw79nv3jz', u.uuid
+  end
+
+  test 'user redirect_to_user_uuid' do
+    mock_auth_with(identity_url: "https://redirects-to-active-user.openid.local")
+    u = assigns(:user)
+    assert_equal 'zzzzz-tpzed-xurymjxw79nv3jz', u.uuid
+  end
+
+  test 'user double redirect_to_user_uuid' do
+    mock_auth_with(identity_url: "https://double-redirects-to-active-user.openid.local")
+    u = assigns(:user)
+    assert_equal 'zzzzz-tpzed-xurymjxw79nv3jz', u.uuid
+  end
+
   test 'create new user during omniauth callback' do
     mock_auth_with(email: 'edward@example.com')
     assert_equal(0, @response.redirect_url.index(client_url),
index 947033564df01e479d05682617fc041417e5d54f..90235cbf3188d91bc274412ddd5522dc639fa812 100644 (file)
@@ -9,6 +9,7 @@ import (
        "flag"
        "fmt"
        "log"
+       "net/http"
        "os"
        "os/signal"
        "syscall"
@@ -45,6 +46,9 @@ type Config struct {
        // more memory, but can reduce store-and-forward latency when
        // fetching pages)
        CollectionBuffers int
+
+       // Timeout for outgoing http request/response cycle.
+       RequestTimeout arvados.Duration
 }
 
 // RunOptions controls runtime behavior. The flags/options that belong
@@ -107,6 +111,14 @@ func main() {
                log.Fatal(config.DumpAndExit(cfg))
        }
 
+       to := time.Duration(cfg.RequestTimeout)
+       if to == 0 {
+               to = 30 * time.Minute
+       }
+       arvados.DefaultSecureClient.Timeout = to
+       arvados.InsecureHTTPClient.Timeout = to
+       http.DefaultClient.Timeout = to
+
        log.Printf("keep-balance %s started", version)
 
        if *debugFlag {
index 0f4effe6f4e9b7c4e2590cfeb48ef5cd729ec5cd..4c7d5067182fe89783e104c56063fdaf86545c1b 100644 (file)
@@ -19,7 +19,8 @@ KeepServiceTypes:
     - disk
 RunPeriod: 600s
 CollectionBatchSize: 100000
-CollectionBuffers: 1000`)
+CollectionBuffers: 1000
+RequestTimeout: 30m`)
 
 func usage() {
        fmt.Fprintf(os.Stderr, `
@@ -86,6 +87,11 @@ Tuning resource usage:
     while the current page is still being processed. If this is zero
     or omitted, pages are processed serially.
 
+    RequestTimeout is the maximum time keep-balance will spend on a
+    single HTTP request (getting a page of collections, getting the
+    block index from a keepstore server, or sending a trash or pull
+    list to a keepstore server). Defaults to 30 minutes.
+
 Limitations:
 
     keep-balance does not attempt to discover whether committed pull