[ARVADOS] created: 1.3.0-853-g05bfa9a17

Git user git at public.curoverse.com
Thu May 9 19:31:15 UTC 2019


        at  05bfa9a17e8d46d9a388fea130b7df33b7aa15c6 (commit)


commit 05bfa9a17e8d46d9a388fea130b7df33b7aa15c6
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Thu May 9 15:25:23 2019 -0400

    15061: Add documentation for using arv-federation-migrate
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/doc/admin/merge-remote-account.html.textile.liquid b/doc/admin/merge-remote-account.html.textile.liquid
index b69730c93..14899f37f 100644
--- a/doc/admin/merge-remote-account.html.textile.liquid
+++ b/doc/admin/merge-remote-account.html.textile.liquid
@@ -1,7 +1,7 @@
 ---
 layout: default
 navsection: admin
-title: "Migrating a user to a federated account"
+title: "Migrating users to federated accounts"
 ...
 {% comment %}
 Copyright (C) The Arvados Authors. All rights reserved.
@@ -9,39 +9,72 @@ Copyright (C) The Arvados Authors. All rights reserved.
 SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
-When you use federation capabilities to connect two or more clusters that were already operating, some users might already have accounts on multiple clusters. Typically, they will want to choose a single account on one of the clusters and abandon the rest, transferring all data or permissions from their old “remote” accounts to a single “home” account.
+When using multiple Arvados clusters, prior to federation capabilities described here, a user would have to create a separate account on each cluster.  Unfortunately, because each account represents a separate "identity", in this system permissions granted to a user on one cluster do not transfer to another cluster, even if the accounts are associated with the same user.
 
-This effect can be achieved by changing the UUIDs of the user records on the remote clusters. This should be done before the user has ever used federation features to access cluster B with cluster A credentials. Otherwise, see "managing conflicting accounts" below.
+To address this, Arvados supports "federated user accounts".  A federated user account is associated with a specific "home" cluster, and can be used access other clusters in the federation that trust the home cluster.  When a user arrives at another cluster's Workbench, they select and log in to their home cluster, and then are returned to the starting cluster logged in with the federated user account.
 
-For example, a user might have:
-* an account A on cluster A with uuid @aaaaa-tpzed-abcdefghijklmno@, and
-* an account B on cluster B with uuid @bbbbb-tpzed-lmnopqrstuvwxyz@
+When setting up federation capabilities on existing clusters, some users might already have accounts on multiple clusters.  In order to have a single federated identity, users should be assigned a "home" cluster, and accounts associated with that user on the other (non-home) clusters should be migrated to the new federated user account.  The @arv-federation-migrate@ tool assists with this.
 
-An administrator at cluster B can merge the two accounts by renaming account B to account A.
+h2. arv-federation-migrate
 
-<notextile>
-<pre><code>#!/usr/bin/env python
-import arvados
-arvados.api('v1').users().update_uuid(
-    uuid="<span class="userinput">bbbbb-tpzed-lmnopqrstuvwxyz</span>",
-    new_uuid="<span class="userinput">aaaaa-tpzed-abcdefghijklmno</span>").execute()
-</code></pre></notextile>
+The tool @arv-federation-migrate@ is part of the @arvados-python-client@ package.
 
-This should be done when the user is idle, i.e., not logged in and not running any jobs or containers.
+This tool is designed to help an administrator who has access to all clusters in a federation to migrate users who have multiple accounts to a single federated account.
 
-h2. Managing conflicting accounts
+As part of migrating a user, any data or permissions associated with old user accounts will be reassigned to the federated account.
 
-If the user has already used federation capabilities to access cluster B using account A before the above migration has been done, this will have already created a database entry for account A on cluster B, and the above program will error out. To fix this, the same "update_uuid API call":../api/methods/users.html#update_uuid can be used to move the conflicting account out of the way first.
+h2. Get user report
 
-<notextile>
-<pre><code>#!/usr/bin/env python
-import arvados
-import random
-import string
-random_chars = ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(15))
-arvados.api('v1').users().update_uuid(
-    uuid="<span class="userinput">aaaaa-tpzed-abcdefghijklmno</span>",
-    new_uuid="bbbbb-tpzed-"+random_chars).execute()
-</code></pre></notextile>
+The first step is to create @tokens.csv@ and list each cluster and API token to access the cluster.  API tokens must be trusted tokens with administrator access.  This is a simple comma separated value file and can be created in a text editor.  Example:
 
-After this is done and the migration is complete, the affected user should wait 5 minutes for the authorization cache to expire before using the remote cluster.
+_tokens.csv_
+
+<pre>
+x3982.arvadosapi.com,v2/x3982-gj3su-sb6meh2jf145s7x/98d40d70d8862e33d7398213435d1a71a96cf870
+x6b1s.arvadosapi.com,v2/x6b1s-gj3su-dxc87btfv5kg91z/5575d980d3ff6231bb0c692281c42a7541c59417
+</pre>
+
+Next, run @arv-federation-migrate@ with the @--tokens@ and @--report@ flags:
+
+<pre>
+$ arv-federation-migrate --tokens tokens.csv --report users.csv
+Reading tokens.csv
+Getting user list from x6b1s
+Getting user list from x3982
+Wrote users.csv
+</pre>
+
+This will produce a report of users across all clusters listed in @tokens.csv@, sorted by email address.  This file can be loaded into a text editor or spreadsheet program for ease of viewing and editing.
+
+_users.csv_
+
+<pre>
+email,user uuid,primary cluster/user
+person_a at example.com,x6b1s-tpzed-hb5n7doogwhk6cf,x6b1s
+person_b at example.com,x3982-tpzed-1vl3k7knf7qihbe,
+person_b at example.com,x6b1s-tpzed-w4nhkx2rmrhlr54,
+</pre>
+
+The third column describes that user's home cluster.  If a user only has one account (identified by email address), the column will be filled in and there is nothing to do.  If the column is blank, that means there is more than one Arvados account associated with the user.  Edit the file and provide the desired home cluster for each user.  In this example, @person_b at example.com@ is assigned the home cluster @x3982 at .
+
+_users.csv_
+
+<pre>
+email,user uuid,primary cluster/user
+person_a at example.com,x6b1s-tpzed-hb5n7doogwhk6cf,x6b1s
+person_b at example.com,x3982-tpzed-1vl3k7knf7qihbe,x3982
+person_b at example.com,x6b1s-tpzed-w4nhkx2rmrhlr54,x3982
+</pre>
+
+h2. Migrate users
+
+To avoid disruption, advise users to log out and avoid running workflows while performing the migration.
+
+After updating @users.csv@, use the @--migrate@ option:
+
+<pre>
+$ arv-federation-migrate --tokens tokens.csv --migrate users.csv
+(person_b at example.com) Migrating x6b1s-tpzed-w4nhkx2rmrhlr54 to x3982-tpzed-1vl3k7knf7qihbe
+</pre>
+
+After migration, users should select their home cluster when logging into Arvados Workbench.  If a user attempts to log into a migrated user account, they will be redirected to log in with their home cluster.
diff --git a/sdk/python/arvados/commands/federation_migrate.py b/sdk/python/arvados/commands/federation_migrate.py
index b789a5ee5..e04c0f7f9 100755
--- a/sdk/python/arvados/commands/federation_migrate.py
+++ b/sdk/python/arvados/commands/federation_migrate.py
@@ -12,15 +12,16 @@ import hmac
 
 def main():
 
-    parser = argparse.ArgumentParser(description='Migrate users to federated identity')
-    parser.add_argument('--tokens', type=str)
-    group = parser.add_mutually_exclusive_group()
+    parser = argparse.ArgumentParser(description='Migrate users to federated identity, see https://doc.arvados.org/admin/???')
+    parser.add_argument('--tokens', type=str, required=True)
+    group = parser.add_mutually_exclusive_group(required=True)
     group.add_argument('--report', type=str)
     group.add_argument('--migrate', type=str)
     args = parser.parse_args()
 
     clusters = {}
 
+    print("Reading %s" % args.tokens)
     with open(args.tokens, "rt") as f:
         for r in csv.reader(f):
             host = r[0]
@@ -34,6 +35,7 @@ def main():
     if args.report:
         users = []
         for c, arv in clusters.items():
+            print("Getting user list from %s" % c)
             ul = arvados.util.list_all(arv.users().list)
             for l in ul:
                 if l["uuid"].startswith(c):
@@ -75,6 +77,8 @@ def main():
         for a in accum:
             out.writerow((a["email"], a["uuid"], homeuuid[0:5]))
 
+        print("Wrote %s" % args.report)
+
     if args.migrate:
         rows = []
         by_email = {}
@@ -101,7 +105,7 @@ def main():
                 print("(%s) Multiple users listed to migrate %s to %s, use full uuid" % (r[0], r[1], r[2]))
                 continue
             new_user_uuid = candidates[0][1]
-            print("(%s) Will migrate %s to %s" % (r[0], r[1], new_user_uuid))
+            print("(%s) Migrating %s to %s" % (r[0], r[1], new_user_uuid))
             oldcluster = r[1][0:5]
             newhomecluster = r[2][0:5]
             homearv = clusters[newhomecluster]

commit 2d6908242ec0726e76878e0316d6334fe91c2555
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Thu May 9 10:59:56 2019 -0400

    15061: Move federation migrate script into sdk
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/tools/federation-migrate/federation-migrate.py b/sdk/python/arvados/commands/federation_migrate.py
similarity index 99%
rename from tools/federation-migrate/federation-migrate.py
rename to sdk/python/arvados/commands/federation_migrate.py
index 9f612b24b..b789a5ee5 100755
--- a/tools/federation-migrate/federation-migrate.py
+++ b/sdk/python/arvados/commands/federation_migrate.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # Copyright (C) The Arvados Authors. All rights reserved.
 #
-# SPDX-License-Identifier: AGPL-3.0
+# SPDX-License-Identifier: Apache-2.0
 
 import arvados
 import arvados.util
diff --git a/sdk/python/bin/arv-federation-migrate b/sdk/python/bin/arv-federation-migrate
new file mode 100755
index 000000000..a4c097473
--- /dev/null
+++ b/sdk/python/bin/arv-federation-migrate
@@ -0,0 +1,7 @@
+#!/usr/bin/env python
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from arvados.commands.federation_migrate import main
+main()
diff --git a/sdk/python/setup.py b/sdk/python/setup.py
index ffca23495..0fc2dde31 100644
--- a/sdk/python/setup.py
+++ b/sdk/python/setup.py
@@ -37,6 +37,7 @@ setup(name='arvados-python-client',
           'bin/arv-keepdocker',
           'bin/arv-ls',
           'bin/arv-migrate-docker19',
+          'bin/arv-federation-migrate',
           'bin/arv-normalize',
           'bin/arv-put',
           'bin/arv-run',

commit a53b268f3f3d30b35c1641d01626203f2572b4d1
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Wed May 8 17:25:06 2019 -0400

    15061: add federation-migrate script
    
    Produces a report, users can edit the report to choose which cluster
    each user should belong to, then run the report back through to
    perform the migration.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/tools/federation-migrate/federation-migrate.py b/tools/federation-migrate/federation-migrate.py
new file mode 100755
index 000000000..9f612b24b
--- /dev/null
+++ b/tools/federation-migrate/federation-migrate.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+import arvados
+import arvados.util
+import csv
+import sys
+import argparse
+import hmac
+
+def main():
+
+    parser = argparse.ArgumentParser(description='Migrate users to federated identity')
+    parser.add_argument('--tokens', type=str)
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument('--report', type=str)
+    group.add_argument('--migrate', type=str)
+    args = parser.parse_args()
+
+    clusters = {}
+
+    with open(args.tokens, "rt") as f:
+        for r in csv.reader(f):
+            host = r[0]
+            token = r[1]
+            arv = arvados.api(host=host, token=token)
+            clusters[arv._rootDesc["uuidPrefix"]] = arv
+            cur = arv.users().current().execute()
+            if not cur["is_admin"]:
+                raise Exception("Not admin of %s" % host)
+
+    if args.report:
+        users = []
+        for c, arv in clusters.items():
+            ul = arvados.util.list_all(arv.users().list)
+            for l in ul:
+                if l["uuid"].startswith(c):
+                    users.append(l)
+
+        out = csv.writer(open(args.report, "wt"))
+
+        out.writerow(("email", "user uuid", "primary cluster/user"))
+
+        users = sorted(users, key=lambda u: u["email"]+"::"+u["uuid"])
+
+        accum = []
+        lastemail = None
+        for u in users:
+            if u["uuid"].endswith("-anonymouspublic") or u["uuid"].endswith("-000000000000000"):
+                continue
+            if lastemail == None:
+                lastemail = u["email"]
+            if u["email"] == lastemail:
+                accum.append(u)
+            else:
+                homeuuid = None
+                for a in accum:
+                    if homeuuid is None:
+                        homeuuid = a["uuid"]
+                    if a["uuid"] != homeuuid:
+                        homeuuid = ""
+                for a in accum:
+                    out.writerow((a["email"], a["uuid"], homeuuid[0:5]))
+                lastemail = u["email"]
+                accum = [u]
+
+        homeuuid = None
+        for a in accum:
+            if homeuuid is None:
+                homeuuid = a["uuid"]
+            if a["uuid"] != homeuuid:
+                homeuuid = ""
+        for a in accum:
+            out.writerow((a["email"], a["uuid"], homeuuid[0:5]))
+
+    if args.migrate:
+        rows = []
+        by_email = {}
+        with open(args.migrate, "rt") as f:
+            for r in csv.reader(f):
+                if r[0] == "email":
+                    continue
+                by_email.setdefault(r[0], [])
+                by_email[r[0]].append(r)
+                rows.append(r)
+        for r in rows:
+            if r[2] == "":
+                print("(%s) Skipping %s, no home cluster specified" % (r[0], r[1]))
+            if r[1].startswith(r[2]):
+                continue
+            candidates = []
+            for b in by_email[r[0]]:
+                if b[1].startswith(r[2]):
+                    candidates.append(b)
+            if len(candidates) == 0:
+                print("(%s) No user listed to migrate %s to %s" % (r[0], r[1], r[2]))
+                continue
+            if len(candidates) > 1:
+                print("(%s) Multiple users listed to migrate %s to %s, use full uuid" % (r[0], r[1], r[2]))
+                continue
+            new_user_uuid = candidates[0][1]
+            print("(%s) Will migrate %s to %s" % (r[0], r[1], new_user_uuid))
+            oldcluster = r[1][0:5]
+            newhomecluster = r[2][0:5]
+            homearv = clusters[newhomecluster]
+            # create a token
+            newtok = homearv.api_client_authorizations().create(body={"api_client_authorization": {'owner_uuid': new_user_uuid}}).execute()
+            salted = 'v2/' + newtok["uuid"] + '/' + hmac.new(newtok["api_token"].encode(), msg=oldcluster.encode(), digestmod='sha1').hexdigest()
+            arvados.api(host=arv._rootDesc["rootUrl"][8:-1], token=salted).users().current().execute()
+
+            # now migrate from local user to remote user.
+            arv = clusters[oldcluster]
+
+            grp = arv.groups().create(body={
+                "owner_uuid": new_user_uuid,
+                "name": "Migrated from %s (%s)" % (r[0], r[1]),
+                "group_class": "project"
+            }, ensure_unique_name=True).execute()
+            arv.users().merge(old_user_uuid=r[1],
+                              new_user_uuid=new_user_uuid,
+                              new_owner_uuid=grp["uuid"],
+                              redirect_to_new_user=True).execute()
+
+if __name__ == "__main__":
+    main()

commit e2a30b016300357dcfc8ae0ea4bfd7111f7e9862
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Thu May 9 15:27:07 2019 -0400

    15061: Add missing file
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/services/api/app/views/user_sessions/create.html.erb b/services/api/app/views/user_sessions/create.html.erb
new file mode 100644
index 000000000..2cb694818
--- /dev/null
+++ b/services/api/app/views/user_sessions/create.html.erb
@@ -0,0 +1,7 @@
+<div style="width:30em; margin:2em auto 0 auto">
+  <h1>Login redirect</h1>
+  <p>This login is linked to federated user <b><%= @user.uuid %></b> on cluster <b><%= @user.uuid[0..4] %></b>.</p>
+  <p><a href="<%=@remotehomeurl%>">Click here log in on cluster <%= @user.uuid[0..4] %>.</a></p>
+  <p>After logging in, you will be returned to this cluster (<%=Rails.configuration.ClusterID%>).</p>
+  <p>To avoid seeing this page, choose <b><%= @user.uuid[0..4] %></b> as the cluster that hosts your user account on the Workbench login page.</p>
+</div>

commit 6731fb679926da11149c5c287072a6c139046f10
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Tue May 7 18:00:57 2019 -0400

    15061: Implement admin variant of user merge by uuid
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/services/api/app/controllers/arvados/v1/users_controller.rb b/services/api/app/controllers/arvados/v1/users_controller.rb
index de18144c5..4a345f363 100644
--- a/services/api/app/controllers/arvados/v1/users_controller.rb
+++ b/services/api/app/controllers/arvados/v1/users_controller.rb
@@ -126,40 +126,65 @@ class Arvados::V1::UsersController < ApplicationController
   end
 
   def merge
-    if !Thread.current[:api_client].andand.is_trusted
-      return send_error("supplied API token is not from a trusted client", status: 403)
-    elsif Thread.current[:api_client_authorization].scopes != ['all']
-      return send_error("cannot merge with a scoped token", status: 403)
-    end
+    if (params[:old_user_uuid] || params[:new_user_uuid])
+      if !current_user.andand.is_admin
+        return send_error("Must be admin to use old_user_uuid/new_user_uuid", status: 403)
+      end
+      if !params[:old_user_uuid] || !params[:new_user_uuid]
+        return send_error("Must supply both old_user_uuid and new_user_uuid", status: 422)
+      end
+      new_user = User.find_by_uuid(params[:new_user_uuid])
+      if !new_user
+        return send_error("User in new_user_uuid not found", status: 422)
+      end
+      @object = User.find_by_uuid(params[:old_user_uuid])
+      if !@object
+        return send_error("User in old_user_uuid not found", status: 422)
+      end
+    else
+      if !Thread.current[:api_client].andand.is_trusted
+        return send_error("supplied API token is not from a trusted client", status: 403)
+      elsif Thread.current[:api_client_authorization].scopes != ['all']
+        return send_error("cannot merge with a scoped token", status: 403)
+      end
 
-    new_auth = ApiClientAuthorization.validate(token: params[:new_user_token])
-    if !new_auth
-      return send_error("invalid new_user_token", status: 401)
-    end
+      new_auth = ApiClientAuthorization.validate(token: params[:new_user_token])
+      if !new_auth
+        return send_error("invalid new_user_token", status: 401)
+      end
 
-    if new_auth.user.uuid[0..4] == Rails.configuration.ClusterID
-      if !new_auth.api_client.andand.is_trusted
-        return send_error("supplied new_user_token is not from a trusted client", status: 403)
-      elsif new_auth.scopes != ['all']
-        return send_error("supplied new_user_token has restricted scope", status: 403)
+      if new_auth.user.uuid[0..4] == Rails.configuration.ClusterID
+        if !new_auth.api_client.andand.is_trusted
+          return send_error("supplied new_user_token is not from a trusted client", status: 403)
+        elsif new_auth.scopes != ['all']
+          return send_error("supplied new_user_token has restricted scope", status: 403)
+        end
       end
+      new_user = new_auth.user
+      @object = current_user
     end
-    new_user = new_auth.user
 
-    if current_user.uuid == new_user.uuid
+    if @object.uuid == new_user.uuid
       return send_error("cannot merge user to self", status: 422)
     end
 
+    if !params[:new_owner_uuid]
+      return send_error("missing new_owner_uuid", status: 422)
+    end
+
     if !new_user.can?(write: params[:new_owner_uuid])
       return send_error("cannot move objects into supplied new_owner_uuid: new user does not have write permission", status: 403)
     end
 
     redirect = params[:redirect_to_new_user]
+    if @object.uuid[0..4] != Rails.configuration.ClusterID && redirect
+      return send_error("cannot merge remote user to other with redirect_to_new_user=true", status: 422)
+    end
+
     if !redirect
       return send_error("merge with redirect_to_new_user=false is not yet supported", status: 422)
     end
 
-    @object = current_user
     act_as_system_user do
       @object.merge(new_owner_uuid: params[:new_owner_uuid], redirect_to_user_uuid: redirect && new_user.uuid)
     end
@@ -174,11 +199,17 @@ class Arvados::V1::UsersController < ApplicationController
         type: 'string', required: true,
       },
       new_user_token: {
-        type: 'string', required: true,
+        type: 'string', required: false,
       },
       redirect_to_new_user: {
         type: 'boolean', required: false,
       },
+      old_user_uuid: {
+        type: 'string', required: false,
+      },
+      new_user_uuid: {
+        type: 'string', required: false,
+      }
     }
   end
 
diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index 1e4befb05..ef0f88686 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -85,10 +85,6 @@ class UserSessionsController < ApplicationController
       # Send them to their home cluster's login
       rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
       remote, return_to_url = params[:return_to].split(',', 2)
-      if remote !~ /^[0-9a-z]{5}$/ && remote != ""
-        return send_error 'Invalid remote cluster id', status: 400
-      end
-      remote = nil if remote == ''
       @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
       render
       return
diff --git a/services/api/test/functional/arvados/v1/users_controller_test.rb b/services/api/test/functional/arvados/v1/users_controller_test.rb
index 0501da167..60696b98a 100644
--- a/services/api/test/functional/arvados/v1/users_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/users_controller_test.rb
@@ -927,7 +927,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
            redirect_to_new_user: true,
          })
     assert_response(:success)
-    assert_equal(users(:project_viewer).redirect_to_user_uuid, users(:active).uuid)
+    assert_equal(users(:active).uuid, User.unscoped.find_by_uuid(users(:project_viewer).uuid).redirect_to_user_uuid)
 
     auth = ApiClientAuthorization.validate(token: api_client_authorizations(:project_viewer).api_token)
     assert_not_nil(auth)
@@ -935,6 +935,82 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_equal(users(:active).uuid, auth.user.uuid)
   end
 
+
+  test "merge 'project_viewer' account into 'active' account using uuids" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(:success)
+    assert_equal(users(:active).uuid, User.unscoped.find_by_uuid(users(:project_viewer).uuid).redirect_to_user_uuid)
+
+    auth = ApiClientAuthorization.validate(token: api_client_authorizations(:project_viewer).api_token)
+    assert_not_nil(auth)
+    assert_not_nil(auth.user)
+    assert_equal(users(:active).uuid, auth.user.uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied for non-admin" do
+    authorize_with(:active)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(403)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied missing old_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied missing new_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied bogus old_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: "zzzzz-tpzed-bogusbogusbogus",
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied bogus new_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_user_uuid: "zzzzz-tpzed-bogusbogusbogus",
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
   NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
                          "last_name", "username"].sort
 

commit ce6416c4bfaf33ac17815f070ae5693fe1b7f1ae
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Tue May 7 16:31:21 2019 -0400

    15061: API support for linking local account to remote user
    
    Adds page to login process to redirect user to log in to home cluster.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/services/api/Gemfile b/services/api/Gemfile
index 6d4295694..804d2a479 100644
--- a/services/api/Gemfile
+++ b/services/api/Gemfile
@@ -71,6 +71,8 @@ gem 'rails-observers'
 gem 'rails-perftest'
 gem 'rails-controller-testing'
 
+gem 'sass-rails'
+
 # Install any plugin gems
 Dir.glob(File.join(File.dirname(__FILE__), 'lib', '**', "Gemfile")) do |f|
     eval(IO.read(f), binding)
diff --git a/services/api/Gemfile.lock b/services/api/Gemfile.lock
index 13f7564be..078b2b7f4 100644
--- a/services/api/Gemfile.lock
+++ b/services/api/Gemfile.lock
@@ -110,6 +110,7 @@ GEM
     faye-websocket (0.10.7)
       eventmachine (>= 0.12.0)
       websocket-driver (>= 0.5.1)
+    ffi (1.9.25)
     globalid (0.4.2)
       activesupport (>= 4.2.0)
     googleauth (0.8.0)
@@ -220,6 +221,9 @@ GEM
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
     rake (12.3.2)
+    rb-fsevent (0.10.3)
+    rb-inotify (0.9.10)
+      ffi (>= 0.5.0, < 2)
     ref (2.0.0)
     request_store (1.4.1)
       rack (>= 1.4)
@@ -231,6 +235,17 @@ GEM
     rvm-capistrano (1.5.6)
       capistrano (~> 2.15.4)
     safe_yaml (1.0.5)
+    sass (3.5.5)
+      sass-listen (~> 4.0.0)
+    sass-listen (4.0.0)
+      rb-fsevent (~> 0.9, >= 0.9.4)
+      rb-inotify (~> 0.9, >= 0.9.7)
+    sass-rails (5.0.7)
+      railties (>= 4.0.0, < 6)
+      sass (~> 3.1)
+      sprockets (>= 2.8, < 4.0)
+      sprockets-rails (>= 2.0, < 4.0)
+      tilt (>= 1.1, < 3)
     signet (0.11.0)
       addressable (~> 2.3)
       faraday (~> 0.9)
@@ -257,6 +272,7 @@ GEM
       ref
     thor (0.20.3)
     thread_safe (0.3.6)
+    tilt (2.0.8)
     tzinfo (1.2.5)
       thread_safe (~> 0.1)
     uglifier (2.7.2)
@@ -299,6 +315,7 @@ DEPENDENCIES
   ruby-prof (~> 0.15.0)
   rvm-capistrano
   safe_yaml
+  sass-rails
   simplecov (~> 0.7.1)
   simplecov-rcov
   sshkey
diff --git a/services/api/app/assets/images/logo.png b/services/api/app/assets/images/logo.png
index 4db96efab..c511f0ec5 100644
Binary files a/services/api/app/assets/images/logo.png and b/services/api/app/assets/images/logo.png differ
diff --git a/services/api/app/assets/stylesheets/application.css b/services/api/app/assets/stylesheets/application.css
index 742a575a9..721ff801c 100644
--- a/services/api/app/assets/stylesheets/application.css
+++ b/services/api/app/assets/stylesheets/application.css
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0 */
  * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
  * the top of the compiled file, but it's generally better to create a new file per style scope.
  *= require_self
- *= require_tree . 
+ *= require_tree .
 */
 
 .contain-align-left {
@@ -63,8 +63,7 @@ div#header span.beta > span {
     font-size: 0.8em;
 }
 img.curoverse-logo {
-    width: 221px;
-    height: 44px;
+    height: 66px;
 }
 #intropage {
     font-family: Verdana,Arial,sans-serif;
@@ -180,4 +179,3 @@ div#header a.sudo-logout {
     color: #000;
     font-weight: bold;
 }
-
diff --git a/services/api/app/controllers/arvados/v1/users_controller.rb b/services/api/app/controllers/arvados/v1/users_controller.rb
index 18b6b46d2..de18144c5 100644
--- a/services/api/app/controllers/arvados/v1/users_controller.rb
+++ b/services/api/app/controllers/arvados/v1/users_controller.rb
@@ -136,10 +136,13 @@ class Arvados::V1::UsersController < ApplicationController
     if !new_auth
       return send_error("invalid new_user_token", status: 401)
     end
-    if !new_auth.api_client.andand.is_trusted
-      return send_error("supplied new_user_token is not from a trusted client", status: 403)
-    elsif new_auth.scopes != ['all']
-      return send_error("supplied new_user_token has restricted scope", status: 403)
+
+    if new_auth.user.uuid[0..4] == Rails.configuration.ClusterID
+      if !new_auth.api_client.andand.is_trusted
+        return send_error("supplied new_user_token is not from a trusted client", status: 403)
+      elsif new_auth.scopes != ['all']
+        return send_error("supplied new_user_token has restricted scope", status: 403)
+      end
     end
     new_user = new_auth.user
 
diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index 6e18cdd46..1e4befb05 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -80,6 +80,20 @@ class UserSessionsController < ApplicationController
     # For the benefit of functional and integration tests:
     @user = user
 
+    if user.uuid[0..4] != Rails.configuration.ClusterID
+      # Actually a remote user
+      # Send them to their home cluster's login
+      rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
+      remote, return_to_url = params[:return_to].split(',', 2)
+      if remote !~ /^[0-9a-z]{5}$/ && remote != ""
+        return send_error 'Invalid remote cluster id', status: 400
+      end
+      remote = nil if remote == ''
+      @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
+      render
+      return
+    end
+
     # prevent ArvadosModel#before_create and _update from throwing
     # "unauthorized":
     Thread.current[:user] = user
diff --git a/services/api/app/views/layouts/application.html.erb b/services/api/app/views/layouts/application.html.erb
index 302859543..a99b6f165 100644
--- a/services/api/app/views/layouts/application.html.erb
+++ b/services/api/app/views/layouts/application.html.erb
@@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <!DOCTYPE html>
 <html>
 <head>
-  <title>Server</title>
+  <title>Arvados API Server (<%= Rails.configuration.ClusterID %>)</title>
   <%= stylesheet_link_tag    "application" %>
   <%= javascript_include_tag "application" %>
   <%= csrf_meta_tags %>
 </head>
 <body>
 <div id="header">
-  <div class="apptitle">ARVADOS <span class="beta"><span>BETA</span></span></div>
+  <div class="apptitle">ARVADOS</div>
+  <div>(<%= Rails.configuration.ClusterID %>)</div>
   <div style="float:right">
     <% if current_user %>
     <%= current_user.full_name %>
@@ -23,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
      • 
     <a class="logout" href="/logout">Log out</a>
     <% else %>
-    <a class="logout" href="/auth/joshid">Log in</a>
+      <!--<a class="logout" href="/auth/joshid">Log in</a>-->
     <% end %>
 
     <% if current_user and session[:real_uid] and session[:switch_back_to] and User.find(session[:real_uid].to_i).verify_userswitch_cookie(session[:switch_back_to]) %>
diff --git a/services/api/app/views/static/login_failure.html.erb b/services/api/app/views/static/login_failure.html.erb
index 0f3141e0e..b3c6e70d9 100644
--- a/services/api/app/views/static/login_failure.html.erb
+++ b/services/api/app/views/static/login_failure.html.erb
@@ -17,9 +17,9 @@ $(function(){
 
     <p>Sorry, something went wrong logging you in. Please try again.</p>
 
-    <p style="float:right;margin-top:1em">
-      <a href="/auth/joshid">Log in here.</a>
-    </p>
+    <!--<p style="float:right;margin-top:1em">
+      <a href="/login">Log in here.</a>
+    </p>-->
 
     <div style="clear:both;height:8em"></div>
   </div>

commit 3d7e21293962aac93f195ab02bf29d60039387bb
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Mon May 6 15:35:42 2019 -0400

    15061: Correctly filter out "*" from RemoteClusters
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/services/api/app/controllers/arvados/v1/schema_controller.rb b/services/api/app/controllers/arvados/v1/schema_controller.rb
index 13e47f76c..eb3ff2388 100644
--- a/services/api/app/controllers/arvados/v1/schema_controller.rb
+++ b/services/api/app/controllers/arvados/v1/schema_controller.rb
@@ -26,7 +26,7 @@ class Arvados::V1::SchemaController < ApplicationController
     Rails.cache.fetch 'arvados_v1_rest_discovery' do
       Rails.application.eager_load!
       remoteHosts = {}
-      Rails.configuration.RemoteClusters.each {|k,v| if k != "*" then remoteHosts[k] = v["Host"] end }
+      Rails.configuration.RemoteClusters.each {|k,v| if k != :"*" then remoteHosts[k] = v["Host"] end }
       discovery = {
         kind: "discovery#restDescription",
         discoveryVersion: "v1",

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list