Merge branch '15795-sys-root-token'
authorTom Clegg <tclegg@veritasgenetics.com>
Thu, 28 Nov 2019 15:19:46 +0000 (10:19 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Thu, 28 Nov 2019 15:19:46 +0000 (10:19 -0500)
refs #15795

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

58 files changed:
apps/workbench/Gemfile.lock
apps/workbench/app/controllers/users_controller.rb
apps/workbench/app/views/users/_setup_popup.html.erb
apps/workbench/app/views/users/_show_admin.html.erb
apps/workbench/test/integration/users_test.rb
apps/workbench/test/test_helper.rb
build/package-build-dockerfiles/Makefile
build/package-build-dockerfiles/centos7/Dockerfile
build/package-build-dockerfiles/debian10/Dockerfile
build/package-build-dockerfiles/debian9/Dockerfile
build/package-build-dockerfiles/ubuntu1604/Dockerfile
build/package-build-dockerfiles/ubuntu1804/Dockerfile
doc/_includes/_vocabulary_migrate_py.liquid [new symlink]
doc/admin/management-token.html.textile.liquid
doc/admin/workbench2-vocabulary.html.textile.liquid
doc/install/install-manual-prerequisites.html.textile.liquid
lib/config/cmd.go
lib/config/cmd_test.go
lib/controller/federation/conn.go
lib/controller/federation/federation_test.go [new file with mode: 0644]
lib/controller/federation/generate.go
lib/controller/federation/generated.go
lib/controller/federation/list.go
lib/controller/federation/list_test.go
lib/controller/federation/login_test.go
lib/controller/federation/user_test.go [new file with mode: 0644]
lib/controller/handler.go
lib/controller/router/request.go
lib/controller/router/request_test.go
lib/controller/router/router.go
lib/controller/router/router_test.go
lib/controller/rpc/conn.go
sdk/go/arvados/api.go
sdk/go/arvados/user.go
sdk/go/arvadostest/api.go
sdk/go/arvadostest/fixtures.go
sdk/python/tests/run_test_server.py
services/api/Gemfile.lock
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/models/api_client_authorization.rb
services/api/app/models/user.rb
services/api/config/routes.rb
services/api/test/functional/arvados/v1/repositories_controller_test.rb
services/api/test/functional/arvados/v1/users_controller_test.rb
services/api/test/helpers/users_test_helper.rb
services/api/test/integration/remote_user_test.rb
services/api/test/integration/user_sessions_test.rb
services/api/test/integration/users_test.rb
services/api/test/unit/user_test.rb
services/keep-balance/integration_test.go
services/keepproxy/keepproxy_test.go
services/keepstore/handler_test.go
services/keepstore/mounts_test.go
services/keepstore/proxy_remote_test.go
services/login-sync/Gemfile.lock
tools/arvbox/lib/arvbox/docker/service/workbench2/run-service
tools/keep-rsync/keep-rsync_test.go
tools/vocabulary-migrate/vocabulary-migrate.py [new file with mode: 0644]

index f16f298bac7f610ea4ce546bac5f405be7107fae..ac3d4f8b62bab2933097ff6fe0eac9eeba4f52b4 100644 (file)
@@ -375,4 +375,4 @@ DEPENDENCIES
   uglifier (~> 2.0)
 
 BUNDLED WITH
-   2.0.2
+   1.17.3
index febd6e3a1d22ba561b458a9b8bbe86f92466a9eb..27fc12bf4c9fc7d3239131f96e93d114588bad31 100644 (file)
@@ -124,7 +124,7 @@ class UsersController < ApplicationController
 
   def show_pane_list
     if current_user.andand.is_admin
-      super | %w(Admin)
+      %w(Admin) | super
     else
       super
     end
index d6f25136c438a39c6eb9659c198effec62afbd69..4c3a95e87e96080b487875e1e519e30d6da350ff 100644 (file)
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
     <div class="modal-header">
       <button type="button" class="close" onClick="reset_form()" data-dismiss="modal" aria-hidden="true">&times;</button>
       <div>
-        <div class="col-sm-6"> <h4 class="modal-title">Setup Shell Account</h4> </div>
+        <div class="col-sm-6"> <h4 class="modal-title">Setup Account</h4> </div>
         <div class="spinner spinner-32px spinner-h-center col-sm-1" hidden="true"></div>
       </div>
       <br/>
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
         <% end %>
       </div>
       <div class="form-group">
-        <label for="vm_uuid">Virtual Machine</label>
+        <label for="vm_uuid">Virtual Machine (optional)</label>
         <select class="form-control" name="vm_uuid">
           <option value="" <%= 'selected' unless selected_vm %>>
             Choose One:
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
         </select>
       </div>
       <div class="groups-group">
-        <label for="groups">Groups for virtual machine (comma separated list)</label>
+        <label for="groups">Groups for virtual machine (comma separated list) (optional)</label>
         <input class="form-control" id="groups" maxlength="250" name="groups" type="text" value="<%=groups%>">
       </div>
     </div>
index ddff79be01c3b02c4a6ff6c7c95f28f129fb1711..1da22d438fabe1609cf09857d17ec0b6bd3c9a52 100644 (file)
@@ -4,35 +4,36 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
 <div class="row">
   <div class="col-md-6">
+
     <p>
-      As an admin, you can log in as this user. When you&rsquo;ve
-      finished, you will need to log out and log in again with your
-      own account.
+      This page enables you to <a href="https://doc.arvados.org/master/admin/user-management.html">manage users</a>.
     </p>
 
-    <blockquote>
-      <%= button_to "Log in as #{@object.full_name}", sudo_user_url(id: @object.uuid), class: 'btn btn-primary' %>
-    </blockquote>
-
     <p>
-      As an admin, you can setup a shell account for this user.
-      The login name is automatically generated from the user's e-mail address.
+      This button sets up a user.  After setup, they will be able use
+      Arvados.  This dialog box also allows you to optionally set up a
+      shell account for this user.  The login name is automatically
+      generated from the user's e-mail address.
     </p>
 
-    <blockquote>
-      <%= link_to "Setup shell account #{'for ' if @object.full_name.present?} #{@object.full_name}", setup_popup_user_url(id: @object.uuid),  {class: 'btn btn-primary', :remote => true, 'data-toggle' =>  "modal", 'data-target' => '#user-setup-modal-window'}  %>
-    </blockquote>
+    <%= link_to "Setup account #{'for ' if @object.full_name.present?} #{@object.full_name}", setup_popup_user_url(id: @object.uuid),  {class: 'btn btn-primary', :remote => true, 'data-toggle' =>  "modal", 'data-target' => '#user-setup-modal-window'}  %>
 
-    <p>
+    <p style="margin-top: 3em">
       As an admin, you can deactivate and reset this user. This will
       remove all repository/VM permissions for the user. If you
       "setup" the user again, the user will have to sign the user
-      agreement again.
+      agreement again.  You may also want to <a href="https://doc.arvados.org/master/admin/reassign-ownership.html">reassign data ownership</a>.
+    </p>
+
+    <%= button_to "Deactivate #{@object.full_name}", unsetup_user_url(id: @object.uuid), class: 'btn btn-primary', data: {confirm: "Are you sure you want to deactivate #{@object.full_name}?"} %>
+
+    <p style="margin-top: 3em">
+      As an admin, you can log in as this user. When you&rsquo;ve
+      finished, you will need to log out and log in again with your
+      own account.
     </p>
 
-    <blockquote>
-      <%= button_to "Deactivate #{@object.full_name}", unsetup_user_url(id: @object.uuid), class: 'btn btn-primary', data: {confirm: "Are you sure you want to deactivate #{@object.full_name}?"} %>
-    </blockquote>
+    <%= button_to "Log in as #{@object.full_name}", sudo_user_url(id: @object.uuid), class: 'btn btn-primary' %>
   </div>
   <div class="col-md-6">
     <div class="panel panel-default">
index bad01a1c6273067fb01f2e0ac7a4cdafbd723124..57be9d370dcb30d8415e5f59bfeb05d6cfefdc78 100644 (file)
@@ -77,6 +77,8 @@ class UsersTest < ActionDispatch::IntegrationTest
       find('a', text: 'Show').
       click
 
+    click_link 'Attributes'
+
     assert page.has_text? 'modified_by_user_uuid'
     page.within(:xpath, '//span[@data-name="is_active"]') do
       assert_equal "false", text, "Expected new user's is_active to be false"
@@ -84,7 +86,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     click_link 'Advanced'
     click_link 'Metadata'
-    assert page.has_text? 'can_login' # make sure page is rendered / ready
+    assert page.has_text? 'can_read' # make sure page is rendered / ready
     assert page.has_no_text? 'VirtualMachine:'
   end
 
@@ -103,9 +105,9 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     # Setup user
     click_link 'Admin'
-    assert page.has_text? 'As an admin, you can setup'
+    assert page.has_text? 'This button sets up a user'
 
-    click_link 'Setup shell account for Active User'
+    click_link 'Setup account for Active User'
 
     within '.modal-content' do
       find 'label', text: 'Virtual Machine'
@@ -113,6 +115,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     end
 
     visit user_url
+    click_link 'Attributes'
     assert page.has_text? 'modified_by_client_uuid'
 
     click_link 'Advanced'
@@ -123,7 +126,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     # Click on Setup button again and this time also choose a VM
     click_link 'Admin'
-    click_link 'Setup shell account for Active User'
+    click_link 'Setup account for Active User'
 
     within '.modal-content' do
       select("testvm.shell", :from => 'vm_uuid')
@@ -132,12 +135,15 @@ class UsersTest < ActionDispatch::IntegrationTest
     end
 
     visit user_url
+    click_link 'Attributes'
     find '#Attributes', text: 'modified_by_client_uuid'
 
     click_link 'Advanced'
     click_link 'Metadata'
     assert page.has_text? 'VirtualMachine: testvm.shell'
     assert page.has_text? '["test group one", "test-group-two"]'
+    vm_links = all("a", text: "VirtualMachine:")
+    assert_equal(2, vm_links.size)
   end
 
   test "unsetup active user" do
@@ -155,7 +161,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     user_url = page.current_url
 
     # Verify that is_active is set
-    find('a,button', text: 'Attributes').click
+    click_link 'Attributes'
     assert page.has_text? 'modified_by_user_uuid'
     page.within(:xpath, '//span[@data-name="is_active"]') do
       assert_equal "true", text, "Expected user's is_active to be true"
@@ -176,6 +182,8 @@ class UsersTest < ActionDispatch::IntegrationTest
       # poltergeist returns true for confirm(), so we don't need to accept.
     end
 
+    click_link 'Attributes'
+
     # Should now be back in the Attributes tab for the user
     assert page.has_text? 'modified_by_user_uuid'
     page.within(:xpath, '//span[@data-name="is_active"]') do
@@ -188,7 +196,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     # setup user again and verify links present
     click_link 'Admin'
-    click_link 'Setup shell account for Active User'
+    click_link 'Setup account for Active User'
 
     within '.modal-content' do
       select("testvm.shell", :from => 'vm_uuid')
@@ -196,6 +204,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     end
 
     visit user_url
+    click_link 'Attributes'
     assert page.has_text? 'modified_by_client_uuid'
 
     click_link 'Advanced'
@@ -211,7 +220,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     # Setup user
     click_link 'Admin'
-    assert page.has_text? 'As an admin, you can setup'
+    assert page.has_text? 'This button sets up a user'
 
     click_link 'Add new group'
 
index 28a7fa51375bfd15acd9fc8b178eac7541376842..84728b8c6882082bbaf015edf2dcb2450674d61f 100644 (file)
@@ -362,6 +362,7 @@ module Minitest
           n += 1
           raise if n > 2 || e.is_a?(Skip)
           STDERR.puts "Test failed, retrying (##{n})"
+          ActiveSupport::TestCase.reset_api_fixtures_now
           retry
         end
       rescue *PASSTHROUGH_EXCEPTIONS
index db53ab096dd8c4c6752b2aa42a2fc0ebc7eef0ad..818f2575254f91ab81cacdb04f9db055ad68e1b8 100644 (file)
@@ -25,7 +25,7 @@ ubuntu1804/generated: common-generated-all
        test -d ubuntu1804/generated || mkdir ubuntu1804/generated
        cp -rlt ubuntu1804/generated common-generated/*
 
-GOTARBALL=go1.12.7.linux-amd64.tar.gz
+GOTARBALL=go1.13.4.linux-amd64.tar.gz
 NODETARBALL=node-v6.11.2-linux-x64.tar.xz
 RVMKEY1=mpapis.asc
 RVMKEY2=pkuczynski.asc
index 916c4abbb0037562c34e2dd1ecc46a6cb0ed1f77..e187df63914fec6d4305b383638c8e8cb725c4dc 100644 (file)
@@ -16,6 +16,7 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.5 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.5 && \
+    /usr/local/rvm/bin/rvm-exec default gem install bundler --version 2.0.2 && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install Bash 4.4.12 // see https://dev.arvados.org/issues/15612
@@ -30,7 +31,7 @@ RUN cd /usr/local/src \
 && ln -sf /usr/local/src/bash-4.4.12/bash /bin/bash
 
 # Install golang binary
-ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.13.4.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
index bab447f5388aae4c5cd9ce8f8705ab9208e78e79..50ffa69d8bb086167af90e3789079af132ee0779 100644 (file)
@@ -22,10 +22,11 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.5 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.5 && \
+    /usr/local/rvm/bin/rvm-exec default gem install bundler --version 2.0.2 && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.13.4.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
index c403d79bcc859a2925ce50752124d9aa1fad4068..b3dcba40d451dbe966d5f572cf1e3ce1546f78a3 100644 (file)
@@ -22,10 +22,11 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.5 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.5 && \
+    /usr/local/rvm/bin/rvm-exec default gem install bundler --version 2.0.2 && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.13.4.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
index 90f340e66cb65155be2737b3a0dd9c6de174d30c..33198fab12305d0ca7004b5d5730345467d4099f 100644 (file)
@@ -21,10 +21,11 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.5 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.5 && \
+    /usr/local/rvm/bin/rvm-exec default gem install bundler --version 2.0.2 && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.13.4.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
index 1adff74000ea208fe8ac6b66915b1f30f0f117e2..143d0fb4fe11d4b4924252cbb96f38a908ae4275 100644 (file)
@@ -21,10 +21,11 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.5 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.5 && \
+    /usr/local/rvm/bin/rvm-exec default gem install bundler --version 2.0.2 && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.13.4.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
diff --git a/doc/_includes/_vocabulary_migrate_py.liquid b/doc/_includes/_vocabulary_migrate_py.liquid
new file mode 120000 (symlink)
index 0000000..9cd3629
--- /dev/null
@@ -0,0 +1 @@
+../../tools/vocabulary-migrate/vocabulary-migrate.py
\ No newline at end of file
index 5380f38f9c40711722744e1281d6ee6dfff6d858..cf3e273ceba13f9b54c5be7f1bfc5aa5c1a921c7 100644 (file)
@@ -16,17 +16,6 @@ Services must have ManagementToken configured.  This is used to authorize access
 
 To access a monitoring endpoint, the requester must provide the HTTP header @Authorization: Bearer (ManagementToken)@.
 
-h2. API server
-
-Set @ManagementToken@ in the appropriate section of @application.yml@
-
-<pre>
-production:
-  # Token to be included in all healthcheck requests. Disabled by default.
-  # Server expects request header of the format "Authorization: Bearer xxx"
-  ManagementToken: xxx
-</pre>
-
 h2. Node Manager
 
 Set @port@ (the listen port) and @ManagementToken@ in the @Manage@ section of @node-manager.ini@.
@@ -45,12 +34,26 @@ Set @port@ (the listen port) and @ManagementToken@ in the @Manage@ section of @n
 ManagementToken = xxx
 </pre>
 
-h2. Other services
+h2. API server and other services
 
-The following services also support monitoring.  Set @ManagementToken@ in the respective yaml config file for each service.
+The following services also support monitoring.
 
+* API server
+* arv-git-httpd
+* controller
+* keep-balance
+* keepproxy
 * keepstore
 * keep-web
-* keepproxy
-* arv-git-httpd
 * websockets
+
+Set @ManagementToken@ in the appropriate section of @/etc/arvados/config.yml@.
+
+<notextile>
+<pre><code>Clusters:
+  <span class="userinput">uuid_prefix</span>:
+    # Token to be included in all healthcheck requests. Disabled by default.
+    # Server expects request header of the format "Authorization: Bearer xxx"
+    ManagementToken: xxx
+</code></pre>
+</notextile>
index 82c384c282f7ad06bca1c001df8e8c9db16dfaca..e259f78625711c6462e4be412528e0d9b45b0d1e 100644 (file)
@@ -48,4 +48,20 @@ The @values@ member is optional and is used to define valid key/label pairs when
 
 When any key or value has more than one label option, Workbench2's user interface will allow the user to select any of the options. But because only the IDs are saved in the system, when the property is displayed in the user interface, the label shown will be the first of each group defined in the vocabulary file. For example, the user could select the property key @Species@ and @Homo sapiens@ as its value, but the user interface will display it as @Animal: Human@ because those labels are the first in the vocabulary definition.
 
-Internally, Workbench2 uses the IDs to do property based searches, so if the user searches by @Animal: Human@ or @Species: Homo sapiens@, both will return the same results.
\ No newline at end of file
+Internally, Workbench2 uses the IDs to do property based searches, so if the user searches by @Animal: Human@ or @Species: Homo sapiens@, both will return the same results.
+
+h2. Properties migration
+
+After installing the new vocabulary definition, it may be necessary to migrate preexisting properties that were set up using literal strings. This can be a big task depending on the number of properties on the vocabulary and the amount of collections and projects on the cluster.
+
+To help with this task we provide below a migration example script that accepts the new vocabulary definition file as an input, and uses the @ARVADOS_API_TOKEN@ and @ARVADOS_API_HOST@ environment variables to connect to the cluster, search for every collection and group that has properties with labels defined on the vocabulary file, and migrates them to the corresponding identifiers.
+
+This script will not run if the vocabulary file has duplicated labels for different keys or for different values inside a key, this is a failsafe mechanism to avoid migration errors.
+
+Please take into account that this script requires admin credentials. It also offers a @--dry-run@ flag that will report what changes are required without applying them, so it can be reviewed by an administrator.
+
+Also, take into consideration that this example script does case-sensitive matching on labels.
+
+{% codeblock as python %}
+{% include 'vocabulary_migrate_py' %}
+{% endcodeblock %}
\ No newline at end of file
index 9aeb6e88888e7bed6119ff482664473a650faf74..7e72990bfc64429f79d28668435ed17b57507638 100644 (file)
@@ -35,9 +35,10 @@ h2. Supported GNU/Linux distributions
 table(table table-bordered table-condensed).
 |_. Distribution|_. State|_. Last supported version|
 |CentOS 7|Supported|Latest|
+|Debian 10 ("buster")|Supported|Latest|
 |Debian 9 ("stretch")|Supported|Latest|
-|Ubuntu 16.04 ("xenial")|Supported|Latest|
 |Ubuntu 18.04 ("bionic")|Supported|Latest|
+|Ubuntu 16.04 ("xenial")|Supported|Latest|
 |Ubuntu 14.04 ("trusty")|EOL|5f943cd451acfbdcddd84e791738c3aa5926bfed (2019-07-10)|
 |Debian 8 ("jessie")|EOL|5f943cd451acfbdcddd84e791738c3aa5926bfed (2019-07-10)|
 |Ubuntu 12.04 ("precise")|EOL|8ed7b6dd5d4df93a3f37096afe6d6f81c2a7ef6e (2017-05-03)|
index e9ceaca8642af7dbb7e993fa0b52de6e81566d63..1ca278391a829f63963930538416f496e2497a1b 100644 (file)
@@ -12,6 +12,7 @@ import (
        "os"
        "os/exec"
 
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "github.com/ghodss/yaml"
        "github.com/sirupsen/logrus"
@@ -124,6 +125,10 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
        if err != nil {
                return 1
        }
+       problems := false
+       if warnAboutProblems(logger, withDepr) {
+               problems = true
+       }
        cmd := exec.Command("diff", "-u", "--label", "without-deprecated-configs", "--label", "relying-on-deprecated-configs", "/dev/fd/3", "/dev/fd/4")
        for _, obj := range []interface{}{withoutDepr, withDepr} {
                y, _ := yaml.Marshal(obj)
@@ -153,7 +158,27 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
        if logbuf.Len() > 0 {
                return 1
        }
-       return 0
+
+       if problems {
+               return 1
+       } else {
+               return 0
+       }
+}
+
+func warnAboutProblems(logger logrus.FieldLogger, cfg *arvados.Config) bool {
+       warned := false
+       for id, cc := range cfg.Clusters {
+               if cc.SystemRootToken == "" {
+                       logger.Warnf("Clusters.%s.SystemRootToken is empty; see https://doc.arvados.org/master/install/install-keepstore.html", id)
+                       warned = true
+               }
+               if cc.ManagementToken == "" {
+                       logger.Warnf("Clusters.%s.ManagementToken is empty; see https://doc.arvados.org/admin/management-token.html", id)
+                       warned = true
+               }
+       }
+       return warned
 }
 
 var DumpDefaultsCommand defaultsCommand
index fb1cba38b4857d4b253933158f3f8a64a002cb48..c275e4c35b37903e9d1f20e6e5c232e026f0d4ee 100644 (file)
@@ -30,25 +30,27 @@ func (s *CommandSuite) SetUpSuite(c *check.C) {
        os.Unsetenv("ARVADOS_API_TOKEN")
 }
 
-func (s *CommandSuite) TestBadArg(c *check.C) {
+func (s *CommandSuite) TestDump_BadArg(c *check.C) {
        var stderr bytes.Buffer
        code := DumpCommand.RunCommand("arvados config-dump", []string{"-badarg"}, bytes.NewBuffer(nil), bytes.NewBuffer(nil), &stderr)
        c.Check(code, check.Equals, 2)
        c.Check(stderr.String(), check.Matches, `(?ms)flag provided but not defined: -badarg\nUsage:\n.*`)
 }
 
-func (s *CommandSuite) TestEmptyInput(c *check.C) {
+func (s *CommandSuite) TestDump_EmptyInput(c *check.C) {
        var stdout, stderr bytes.Buffer
        code := DumpCommand.RunCommand("arvados config-dump", []string{"-config", "-"}, &bytes.Buffer{}, &stdout, &stderr)
        c.Check(code, check.Equals, 1)
        c.Check(stderr.String(), check.Matches, `config does not define any clusters\n`)
 }
 
-func (s *CommandSuite) TestCheckNoDeprecatedKeys(c *check.C) {
+func (s *CommandSuite) TestCheck_NoWarnings(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
 Clusters:
  z1234:
+  ManagementToken: xyzzy
+  SystemRootToken: xyzzy
   API:
     MaxItemsPerResponse: 1234
   PostgreSQL:
@@ -73,7 +75,7 @@ Clusters:
        c.Check(stderr.String(), check.Equals, "")
 }
 
-func (s *CommandSuite) TestCheckDeprecatedKeys(c *check.C) {
+func (s *CommandSuite) TestCheck_DeprecatedKeys(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
 Clusters:
@@ -86,7 +88,7 @@ Clusters:
        c.Check(stdout.String(), check.Matches, `(?ms).*\n\- +.*MaxItemsPerResponse: 1000\n\+ +MaxItemsPerResponse: 1234\n.*`)
 }
 
-func (s *CommandSuite) TestCheckOldKeepstoreConfigFile(c *check.C) {
+func (s *CommandSuite) TestCheck_OldKeepstoreConfigFile(c *check.C) {
        f, err := ioutil.TempFile("", "")
        c.Assert(err, check.IsNil)
        defer os.Remove(f.Name())
@@ -106,7 +108,7 @@ Clusters:
        c.Check(stderr.String(), check.Matches, `(?ms).*you should remove the legacy keepstore config file.*\n`)
 }
 
-func (s *CommandSuite) TestCheckUnknownKey(c *check.C) {
+func (s *CommandSuite) TestCheck_UnknownKey(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
 Clusters:
@@ -130,7 +132,7 @@ Clusters:
        c.Check(stderr.String(), check.Matches, `(?ms).*unexpected object in config entry: Clusters.z1234.PostgreSQL.ConnectionPool"\n.*`)
 }
 
-func (s *CommandSuite) TestDumpFormatting(c *check.C) {
+func (s *CommandSuite) TestDump_Formatting(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
 Clusters:
@@ -149,7 +151,7 @@ Clusters:
        c.Check(stdout.String(), check.Matches, `(?ms).*http://localhost:12345: {}\n.*`)
 }
 
-func (s *CommandSuite) TestDumpUnknownKey(c *check.C) {
+func (s *CommandSuite) TestDump_UnknownKey(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
 Clusters:
index 3a439eb7d4c2d9339cbdc615a52a06d0d8dce4cd..887102f8e58f4d659d3ed4c52b95d92a8e460003 100644 (file)
@@ -15,6 +15,7 @@ import (
        "net/url"
        "regexp"
        "strings"
+       "time"
 
        "git.curoverse.com/arvados.git/lib/config"
        "git.curoverse.com/arvados.git/lib/controller/localdb"
@@ -254,6 +255,10 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions)
        }
 }
 
+func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+       return conn.generated_CollectionList(ctx, options)
+}
+
 func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
        return conn.chooseBackend(options.UUID).CollectionProvenance(ctx, options)
 }
@@ -274,6 +279,10 @@ func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.Untrash
        return conn.chooseBackend(options.UUID).CollectionUntrash(ctx, options)
 }
 
+func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+       return conn.generated_ContainerList(ctx, options)
+}
+
 func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
        return conn.chooseBackend(options.ClusterID).ContainerCreate(ctx, options)
 }
@@ -298,6 +307,10 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
        return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
 }
 
+func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+       return conn.generated_SpecimenList(ctx, options)
+}
+
 func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
        return conn.chooseBackend(options.ClusterID).SpecimenCreate(ctx, options)
 }
@@ -314,6 +327,139 @@ func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOpti
        return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options)
 }
 
+var userAttrsCachedFromLoginCluster = map[string]bool{
+       "created_at":              true,
+       "email":                   true,
+       "first_name":              true,
+       "is_active":               true,
+       "is_admin":                true,
+       "last_name":               true,
+       "modified_at":             true,
+       "modified_by_client_uuid": true,
+       "modified_by_user_uuid":   true,
+       "prefs":                   true,
+       "username":                true,
+
+       "full_name":    false,
+       "identity_url": false,
+       "is_invited":   false,
+       "owner_uuid":   false,
+       "uuid":         false,
+}
+
+func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
+       logger := ctxlog.FromContext(ctx)
+       if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID {
+               resp, err := conn.chooseBackend(id).UserList(ctx, options)
+               if err != nil {
+                       return resp, err
+               }
+               batchOpts := arvados.UserBatchUpdateOptions{Updates: map[string]map[string]interface{}{}}
+               for _, user := range resp.Items {
+                       if !strings.HasPrefix(user.UUID, id) {
+                               continue
+                       }
+                       logger.Debugf("cache user info for uuid %q", user.UUID)
+
+                       // If the remote cluster has null timestamps
+                       // (e.g., test server with incomplete
+                       // fixtures) use dummy timestamps (instead of
+                       // the zero time, which causes a Rails API
+                       // error "year too big to marshal: 1 UTC").
+                       if user.ModifiedAt.IsZero() {
+                               user.ModifiedAt = time.Now()
+                       }
+                       if user.CreatedAt.IsZero() {
+                               user.CreatedAt = time.Now()
+                       }
+
+                       var allFields map[string]interface{}
+                       buf, err := json.Marshal(user)
+                       if err != nil {
+                               return arvados.UserList{}, fmt.Errorf("error encoding user record from remote response: %s", err)
+                       }
+                       err = json.Unmarshal(buf, &allFields)
+                       if err != nil {
+                               return arvados.UserList{}, fmt.Errorf("error transcoding user record from remote response: %s", err)
+                       }
+                       updates := allFields
+                       if len(options.Select) > 0 {
+                               updates = map[string]interface{}{}
+                               for _, k := range options.Select {
+                                       if v, ok := allFields[k]; ok && userAttrsCachedFromLoginCluster[k] {
+                                               updates[k] = v
+                                       }
+                               }
+                       } else {
+                               for k := range updates {
+                                       if !userAttrsCachedFromLoginCluster[k] {
+                                               delete(updates, k)
+                                       }
+                               }
+                       }
+                       batchOpts.Updates[user.UUID] = updates
+               }
+               if len(batchOpts.Updates) > 0 {
+                       ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
+                       _, err = conn.local.UserBatchUpdate(ctxRoot, batchOpts)
+                       if err != nil {
+                               return arvados.UserList{}, fmt.Errorf("error updating local user records: %s", err)
+                       }
+               }
+               return resp, nil
+       } else {
+               return conn.generated_UserList(ctx, options)
+       }
+}
+
+func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.ClusterID).UserCreate(ctx, options)
+}
+
+func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserUpdate(ctx, options)
+}
+
+func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserUpdateUUID(ctx, options)
+}
+
+func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.OldUserUUID).UserMerge(ctx, options)
+}
+
+func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserActivate(ctx, options)
+}
+
+func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
+       return conn.chooseBackend(options.UUID).UserSetup(ctx, options)
+}
+
+func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserUnsetup(ctx, options)
+}
+
+func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserGet(ctx, options)
+}
+
+func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserGetCurrent(ctx, options)
+}
+
+func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserGetSystem(ctx, options)
+}
+
+func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
+       return conn.chooseBackend(options.UUID).UserDelete(ctx, options)
+}
+
+func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
+       return conn.local.UserBatchUpdate(ctx, options)
+}
+
 func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
        return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
 }
diff --git a/lib/controller/federation/federation_test.go b/lib/controller/federation/federation_test.go
new file mode 100644 (file)
index 0000000..60164b4
--- /dev/null
@@ -0,0 +1,75 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+       "context"
+       "net/url"
+       "os"
+       "testing"
+
+       "git.curoverse.com/arvados.git/lib/controller/router"
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "git.curoverse.com/arvados.git/sdk/go/httpserver"
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+// FederationSuite does some generic setup/teardown. Don't add Test*
+// methods to FederationSuite itself.
+type FederationSuite struct {
+       cluster *arvados.Cluster
+       ctx     context.Context
+       fed     *Conn
+}
+
+func (s *FederationSuite) SetUpTest(c *check.C) {
+       s.cluster = &arvados.Cluster{
+               ClusterID:       "aaaaa",
+               SystemRootToken: arvadostest.SystemRootToken,
+               RemoteClusters: map[string]arvados.RemoteCluster{
+                       "aaaaa": arvados.RemoteCluster{
+                               Host: os.Getenv("ARVADOS_API_HOST"),
+                       },
+               },
+       }
+       arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
+       s.cluster.TLS.Insecure = true
+       s.cluster.API.MaxItemsPerResponse = 3
+
+       ctx := context.Background()
+       ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
+       ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+       s.ctx = ctx
+
+       s.fed = New(s.cluster)
+}
+
+func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend) {
+       s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
+               Host: "in-process.local",
+       }
+       s.fed.remotes[id] = backend
+}
+
+func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
+       srv := httpserver.Server{Addr: ":"}
+       srv.Handler = router.New(backend)
+       c.Check(srv.Start(), check.IsNil)
+       s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
+               Scheme: "http",
+               Host:   srv.Addr,
+               Proxy:  true,
+       }
+       s.fed.remotes[id] = rpc.NewConn(id, &url.URL{Scheme: "http", Host: srv.Addr}, true, saltedTokenProvider(s.fed.local, id))
+}
index 11f021e518e4b5cdb0e0ff3d7aad946b27f87750..ab5d9966a4409479ec2bd1725e14a629c1770f12 100644 (file)
@@ -31,7 +31,7 @@ func main() {
        if err != nil {
                panic(err)
        }
-       orig := regexp.MustCompile(`(?ms)\nfunc [^\n]*CollectionList\(.*?\n}\n`).Find(buf)
+       orig := regexp.MustCompile(`(?ms)\nfunc [^\n]*generated_CollectionList\(.*?\n}\n`).Find(buf)
        if len(orig) == 0 {
                panic("can't find CollectionList func")
        }
@@ -52,7 +52,7 @@ func main() {
                defer out.Close()
                out.Write(regexp.MustCompile(`(?ms)^.*package .*?import.*?\n\)\n`).Find(buf))
                io.WriteString(out, "//\n// -- this file is auto-generated -- do not edit -- edit list.go and run \"go generate\" instead --\n//\n\n")
-               for _, t := range []string{"Container", "Specimen"} {
+               for _, t := range []string{"Container", "Specimen", "User"} {
                        _, err := out.Write(bytes.ReplaceAll(orig, []byte("Collection"), []byte(t)))
                        if err != nil {
                                panic(err)
index fb91a84960547d6dc6099e2e7e3ca69fb162afd8..56a55d137bfa2070696b61289de34693d96f5416 100755 (executable)
@@ -17,7 +17,7 @@ import (
 // -- this file is auto-generated -- do not edit -- edit list.go and run "go generate" instead --
 //
 
-func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+func (conn *Conn) generated_ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
        var mtx sync.Mutex
        var merged arvados.ContainerList
        var needSort atomic.Value
@@ -57,7 +57,7 @@ func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions
        return merged, err
 }
 
-func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+func (conn *Conn) generated_SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
        var mtx sync.Mutex
        var merged arvados.SpecimenList
        var needSort atomic.Value
@@ -96,3 +96,43 @@ func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions)
        }
        return merged, err
 }
+
+func (conn *Conn) generated_UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
+       var mtx sync.Mutex
+       var merged arvados.UserList
+       var needSort atomic.Value
+       needSort.Store(false)
+       err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
+               cl, err := backend.UserList(ctx, options)
+               if err != nil {
+                       return nil, err
+               }
+               mtx.Lock()
+               defer mtx.Unlock()
+               if len(merged.Items) == 0 {
+                       merged = cl
+               } else if len(cl.Items) > 0 {
+                       merged.Items = append(merged.Items, cl.Items...)
+                       needSort.Store(true)
+               }
+               uuids := make([]string, 0, len(cl.Items))
+               for _, item := range cl.Items {
+                       uuids = append(uuids, item.UUID)
+               }
+               return uuids, nil
+       })
+       if needSort.Load().(bool) {
+               // Apply the default/implied order, "modified_at desc"
+               sort.Slice(merged.Items, func(i, j int) bool {
+                       mi, mj := merged.Items[i].ModifiedAt, merged.Items[j].ModifiedAt
+                       return mj.Before(mi)
+               })
+       }
+       if merged.Items == nil {
+               // Return empty results as [], not null
+               // (https://github.com/golang/go/issues/27589 might be
+               // a better solution in the future)
+               merged.Items = []arvados.User{}
+       }
+       return merged, err
+}
index 54f59812a0bceaf3706dc35bdf599d8d58a8f7be..26b6b254e8e9fbdbb59638ca441412ade5575abb 100644 (file)
@@ -21,7 +21,7 @@ import (
 // CollectionList is used as a template to auto-generate List()
 // methods for other types; see generate.go.
 
-func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+func (conn *Conn) generated_CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
        var mtx sync.Mutex
        var merged arvados.CollectionList
        var needSort atomic.Value
index 35d201028b96e3a2a0c3033e4a5d06fe68f532ff..a9c4f588f12a38b017d1c89b2995263a8c12d3d6 100644 (file)
@@ -8,75 +8,14 @@ import (
        "context"
        "fmt"
        "net/http"
-       "net/url"
-       "os"
        "sort"
-       "testing"
 
-       "git.curoverse.com/arvados.git/lib/controller/router"
-       "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
-       "git.curoverse.com/arvados.git/sdk/go/auth"
-       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
-       "git.curoverse.com/arvados.git/sdk/go/httpserver"
        check "gopkg.in/check.v1"
 )
 
-// Gocheck boilerplate
-func Test(t *testing.T) {
-       check.TestingT(t)
-}
-
-var (
-       _ = check.Suite(&FederationSuite{})
-       _ = check.Suite(&CollectionListSuite{})
-)
-
-type FederationSuite struct {
-       cluster *arvados.Cluster
-       ctx     context.Context
-       fed     *Conn
-}
-
-func (s *FederationSuite) SetUpTest(c *check.C) {
-       s.cluster = &arvados.Cluster{
-               ClusterID: "aaaaa",
-               RemoteClusters: map[string]arvados.RemoteCluster{
-                       "aaaaa": arvados.RemoteCluster{
-                               Host: os.Getenv("ARVADOS_API_HOST"),
-                       },
-               },
-       }
-       arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
-       s.cluster.TLS.Insecure = true
-       s.cluster.API.MaxItemsPerResponse = 3
-
-       ctx := context.Background()
-       ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
-       ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
-       s.ctx = ctx
-
-       s.fed = New(s.cluster)
-}
-
-func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend) {
-       s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
-               Host: "in-process.local",
-       }
-       s.fed.remotes[id] = backend
-}
-
-func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
-       srv := httpserver.Server{Addr: ":"}
-       srv.Handler = router.New(backend)
-       c.Check(srv.Start(), check.IsNil)
-       s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
-               Host:  srv.Addr,
-               Proxy: true,
-       }
-       s.fed.remotes[id] = rpc.NewConn(id, &url.URL{Scheme: "http", Host: srv.Addr}, true, saltedTokenProvider(s.fed.local, id))
-}
+var _ = check.Suite(&CollectionListSuite{})
 
 type collectionLister struct {
        arvadostest.APIStub
index e294df7d89f5e39d2467544ee49623e4ecda5fcc..8ec2bd5a4910db98d04d5d042453371d78455870 100644 (file)
@@ -13,7 +13,13 @@ import (
        check "gopkg.in/check.v1"
 )
 
-func (s *FederationSuite) TestDeferToLoginCluster(c *check.C) {
+var _ = check.Suite(&LoginSuite{})
+
+type LoginSuite struct {
+       FederationSuite
+}
+
+func (s *LoginSuite) TestDeferToLoginCluster(c *check.C) {
        s.addHTTPRemote(c, "zhome", &arvadostest.APIStub{})
        s.cluster.Login.LoginCluster = "zhome"
 
diff --git a/lib/controller/federation/user_test.go b/lib/controller/federation/user_test.go
new file mode 100644 (file)
index 0000000..8202a66
--- /dev/null
@@ -0,0 +1,124 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+       "encoding/json"
+       "errors"
+       "net/url"
+       "os"
+       "strings"
+
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&UserSuite{})
+
+type UserSuite struct {
+       FederationSuite
+}
+
+func (s *UserSuite) TestLoginClusterUserList(c *check.C) {
+       s.cluster.ClusterID = "local"
+       s.cluster.Login.LoginCluster = "zzzzz"
+       s.fed = New(s.cluster)
+       s.addDirectRemote(c, "zzzzz", rpc.NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")}, true, rpc.PassthroughTokenProvider))
+
+       for _, updateFail := range []bool{false, true} {
+               for _, opts := range []arvados.ListOptions{
+                       {Offset: 0, Limit: -1, Select: nil},
+                       {Offset: 1, Limit: 1, Select: nil},
+                       {Offset: 0, Limit: 2, Select: []string{"uuid"}},
+                       {Offset: 0, Limit: 2, Select: []string{"uuid", "email"}},
+               } {
+                       c.Logf("updateFail %v, opts %#v", updateFail, opts)
+                       spy := arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
+                       stub := &arvadostest.APIStub{Error: errors.New("local cluster failure")}
+                       if updateFail {
+                               s.fed.local = stub
+                       } else {
+                               s.fed.local = rpc.NewConn(s.cluster.ClusterID, spy.URL, true, rpc.PassthroughTokenProvider)
+                       }
+                       userlist, err := s.fed.UserList(s.ctx, opts)
+                       if updateFail && err == nil {
+                               // All local updates fail, so the only
+                               // cases expected to succeed are the
+                               // ones with 0 results.
+                               c.Check(userlist.Items, check.HasLen, 0)
+                               c.Check(stub.Calls(nil), check.HasLen, 0)
+                       } else if updateFail {
+                               c.Logf("... err %#v", err)
+                               calls := stub.Calls(stub.UserBatchUpdate)
+                               if c.Check(calls, check.HasLen, 1) {
+                                       c.Logf("... stub.UserUpdate called with options: %#v", calls[0].Options)
+                                       shouldUpdate := map[string]bool{
+                                               "uuid":       false,
+                                               "email":      true,
+                                               "first_name": true,
+                                               "last_name":  true,
+                                               "is_admin":   true,
+                                               "is_active":  true,
+                                               "prefs":      true,
+                                               // can't safely update locally
+                                               "owner_uuid":   false,
+                                               "identity_url": false,
+                                               // virtual attrs
+                                               "full_name":  false,
+                                               "is_invited": false,
+                                       }
+                                       if opts.Select != nil {
+                                               // Only the selected
+                                               // fields (minus uuid)
+                                               // should be updated.
+                                               for k := range shouldUpdate {
+                                                       shouldUpdate[k] = false
+                                               }
+                                               for _, k := range opts.Select {
+                                                       if k != "uuid" {
+                                                               shouldUpdate[k] = true
+                                                       }
+                                               }
+                                       }
+                                       var uuid string
+                                       for uuid = range calls[0].Options.(arvados.UserBatchUpdateOptions).Updates {
+                                       }
+                                       for k, shouldFind := range shouldUpdate {
+                                               _, found := calls[0].Options.(arvados.UserBatchUpdateOptions).Updates[uuid][k]
+                                               c.Check(found, check.Equals, shouldFind, check.Commentf("offending attr: %s", k))
+                                       }
+                               }
+                       } else {
+                               updates := 0
+                               for _, d := range spy.RequestDumps {
+                                       d := string(d)
+                                       if strings.Contains(d, "PATCH /arvados/v1/users/batch") {
+                                               c.Check(d, check.Matches, `(?ms).*Authorization: Bearer `+arvadostest.SystemRootToken+`.*`)
+                                               updates++
+                                       }
+                               }
+                               c.Check(err, check.IsNil)
+                               c.Check(updates, check.Equals, 1)
+                               c.Logf("... response items %#v", userlist.Items)
+                       }
+               }
+       }
+}
+
+// userAttrsCachedFromLoginCluster must have an entry for every field
+// in the User struct.
+func (s *UserSuite) TestUserAttrsUpdateWhitelist(c *check.C) {
+       buf, err := json.Marshal(&arvados.User{})
+       c.Assert(err, check.IsNil)
+       var allFields map[string]interface{}
+       err = json.Unmarshal(buf, &allFields)
+       c.Assert(err, check.IsNil)
+       for k := range allFields {
+               _, ok := userAttrsCachedFromLoginCluster[k]
+               c.Check(ok, check.Equals, true, check.Commentf("field name %q missing from userAttrsCachedFromLoginCluster", k))
+       }
+}
index f925233ba36ddfdddab0b26b3ea16db772e2877a..a0c2450096e948c998c1476f4e15f24ae9dadfc4 100644 (file)
@@ -83,6 +83,8 @@ func (h *Handler) setup() {
        if h.Cluster.EnableBetaController14287 {
                mux.Handle("/arvados/v1/collections", rtr)
                mux.Handle("/arvados/v1/collections/", rtr)
+               mux.Handle("/arvados/v1/users", rtr)
+               mux.Handle("/arvados/v1/users/", rtr)
                mux.Handle("/login", rtr)
        }
 
index 377f7243c009bef591fffb4b3b53acaf39c0d359..cc6379486893742ff1f9464fcc8c62b1b5f8add7 100644 (file)
@@ -13,9 +13,48 @@ import (
        "strconv"
        "strings"
 
-       "github.com/julienschmidt/httprouter"
+       "github.com/gorilla/mux"
 )
 
+func guessAndParse(k, v string) (interface{}, error) {
+       // All of these form values arrive as strings, so we need some
+       // type-guessing to accept non-string inputs:
+       //
+       // Values for parameters that take ints (limit=1) or bools
+       // (include_trash=1) are parsed accordingly.
+       //
+       // "null" and "" are nil.
+       //
+       // Values that look like JSON objects, arrays, or strings are
+       // parsed as JSON.
+       //
+       // The rest are left as strings.
+       switch {
+       case intParams[k]:
+               return strconv.ParseInt(v, 10, 64)
+       case boolParams[k]:
+               return stringToBool(v), nil
+       case v == "null" || v == "":
+               return nil, nil
+       case strings.HasPrefix(v, "["):
+               var j []interface{}
+               err := json.Unmarshal([]byte(v), &j)
+               return j, err
+       case strings.HasPrefix(v, "{"):
+               var j map[string]interface{}
+               err := json.Unmarshal([]byte(v), &j)
+               return j, err
+       case strings.HasPrefix(v, "\""):
+               var j string
+               err := json.Unmarshal([]byte(v), &j)
+               return j, err
+       default:
+               return v, nil
+       }
+       // TODO: Need to accept "?foo[]=bar&foo[]=baz" as
+       // foo=["bar","baz"]?
+}
+
 // Parse req as an Arvados V1 API request and return the request
 // parameters.
 //
@@ -33,56 +72,11 @@ func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[st
        // Content-Type is application/x-www-form-urlencoded -- the
        // request body.
        for k, values := range req.Form {
-               // All of these form values arrive as strings, so we
-               // need some type-guessing to accept non-string
-               // inputs:
-               //
-               // Values for parameters that take ints (limit=1) or
-               // bools (include_trash=1) are parsed accordingly.
-               //
-               // "null" and "" are nil.
-               //
-               // Values that look like JSON objects, arrays, or
-               // strings are parsed as JSON.
-               //
-               // The rest are left as strings.
                for _, v := range values {
-                       switch {
-                       case intParams[k]:
-                               params[k], err = strconv.ParseInt(v, 10, 64)
-                               if err != nil {
-                                       return nil, err
-                               }
-                       case boolParams[k]:
-                               params[k] = stringToBool(v)
-                       case v == "null" || v == "":
-                               params[k] = nil
-                       case strings.HasPrefix(v, "["):
-                               var j []interface{}
-                               err := json.Unmarshal([]byte(v), &j)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               params[k] = j
-                       case strings.HasPrefix(v, "{"):
-                               var j map[string]interface{}
-                               err := json.Unmarshal([]byte(v), &j)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               params[k] = j
-                       case strings.HasPrefix(v, "\""):
-                               var j string
-                               err := json.Unmarshal([]byte(v), &j)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               params[k] = j
-                       default:
-                               params[k] = v
+                       params[k], err = guessAndParse(k, v)
+                       if err != nil {
+                               return nil, err
                        }
-                       // TODO: Need to accept "?foo[]=bar&foo[]=baz"
-                       // as foo=["bar","baz"]?
                }
        }
 
@@ -98,7 +92,20 @@ func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[st
                        return nil, httpError(http.StatusBadRequest, err)
                }
                for k, v := range jsonParams {
-                       params[k] = v
+                       switch v := v.(type) {
+                       case string:
+                               // The Ruby "arv" cli tool sends a
+                               // JSON-encode params map with
+                               // JSON-encoded values.
+                               dec, err := guessAndParse(k, v)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               jsonParams[k] = dec
+                               params[k] = dec
+                       default:
+                               params[k] = v
+                       }
                }
                if attrsKey != "" && params[attrsKey] == nil {
                        // Copy top-level parameters from JSON request
@@ -109,9 +116,8 @@ func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[st
                }
        }
 
-       routeParams, _ := req.Context().Value(httprouter.ParamsKey).(httprouter.Params)
-       for _, p := range routeParams {
-               params[p.Key] = p.Value
+       for k, v := range mux.Vars(req) {
+               params[k] = v
        }
 
        if v, ok := params[attrsKey]; ok && attrsKey != "" {
index 89238f656345aa2d7ccf19b4889ead4955f17034..118415cb40a2211c702ecf5fbfe0b0256da542b4 100644 (file)
@@ -49,10 +49,26 @@ func (tr *testReq) Request() *http.Request {
        } else if tr.json {
                if tr.jsonAttrsTop {
                        for k, v := range tr.attrs {
-                               param[k] = v
+                               if tr.jsonStringParam {
+                                       j, err := json.Marshal(v)
+                                       if err != nil {
+                                               panic(err)
+                                       }
+                                       param[k] = string(j)
+                               } else {
+                                       param[k] = v
+                               }
                        }
                } else if tr.attrs != nil {
-                       param[tr.attrsKey] = tr.attrs
+                       if tr.jsonStringParam {
+                               j, err := json.Marshal(tr.attrs)
+                               if err != nil {
+                                       panic(err)
+                               }
+                               param[tr.attrsKey] = string(j)
+                       } else {
+                               param[tr.attrsKey] = tr.attrs
+                       }
                }
                tr.body = bytes.NewBuffer(nil)
                err := json.NewEncoder(tr.body).Encode(param)
@@ -118,6 +134,8 @@ func (s *RouterSuite) TestAttrsInBody(c *check.C) {
        for _, tr := range []testReq{
                {attrsKey: "model_name", json: true, attrs: attrs},
                {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: true},
+               {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: true, jsonStringParam: true},
+               {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: false, jsonStringParam: true},
        } {
                c.Logf("tr: %#v", tr)
                req := tr.Request()
index d3bdce527211e1b26245a820dcbc9cd174f3dc62..47082197a01316f4db874b0989c836e7d4a0850f 100644 (file)
@@ -14,18 +14,18 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/auth"
        "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
-       "github.com/julienschmidt/httprouter"
+       "github.com/gorilla/mux"
        "github.com/sirupsen/logrus"
 )
 
 type router struct {
-       mux *httprouter.Router
+       mux *mux.Router
        fed arvados.API
 }
 
 func New(fed arvados.API) *router {
        rtr := &router{
-               mux: httprouter.New(),
+               mux: mux.NewRouter(),
                fed: fed,
        }
        rtr.addRoutes()
@@ -205,6 +205,97 @@ func (rtr *router) addRoutes() {
                                return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
+               {
+                       arvados.EndpointUserCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserMerge,
+                       func() interface{} { return &arvados.UserMergeOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserActivate,
+                       func() interface{} { return &arvados.UserActivateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserSetup,
+                       func() interface{} { return &arvados.UserSetupOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserUnsetup,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserGetCurrent,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserGetSystem,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserUpdateUUID,
+                       func() interface{} { return &arvados.UpdateUUIDOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserBatchUpdate,
+                       func() interface{} { return &arvados.UserBatchUpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
        } {
                rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
                if route.endpoint.Method == "PATCH" {
@@ -214,16 +305,16 @@ func (rtr *router) addRoutes() {
                        rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
                }
        }
-       rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+       rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
        })
-       rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+       rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
        })
 }
 
 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
-       rtr.mux.HandlerFunc(endpoint.Method, "/"+endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
+       rtr.mux.Methods(endpoint.Method).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                logger := ctxlog.FromContext(req.Context())
                params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
                if err != nil {
@@ -250,6 +341,11 @@ func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() int
                }
 
                creds := auth.CredentialsFromRequest(req)
+               err = creds.LoadTokensFromHTTPRequestBody(req)
+               if err != nil {
+                       rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
+                       return
+               }
                if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
                        for _, t := range rt {
                                if t, ok := t.(string); ok {
index a42df278f43043ee7eeb4621d5ecaa2faa0cb275..b1bc9bce32b202548942dd3869f3ed2073ddaa85 100644 (file)
@@ -19,7 +19,7 @@ import (
        "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
-       "github.com/julienschmidt/httprouter"
+       "github.com/gorilla/mux"
        check "gopkg.in/check.v1"
 )
 
@@ -38,7 +38,7 @@ type RouterSuite struct {
 func (s *RouterSuite) SetUpTest(c *check.C) {
        s.stub = arvadostest.APIStub{}
        s.rtr = &router{
-               mux: httprouter.New(),
+               mux: mux.NewRouter(),
                fed: &s.stub,
        }
        s.rtr.addRoutes()
index 3d6a9852089c3005a084e61290da0b45f0a67489..f4bc1733eaff7112caf4bdfc7c69f62ddae5c4a8 100644 (file)
@@ -118,9 +118,9 @@ func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arva
                params["reader_tokens"] = tokens[1:]
        }
        path := ep.Path
-       if strings.Contains(ep.Path, "/:uuid") {
+       if strings.Contains(ep.Path, "/{uuid}") {
                uuid, _ := params["uuid"].(string)
-               path = strings.Replace(path, "/:uuid", "/"+uuid, 1)
+               path = strings.Replace(path, "/{uuid}", "/"+uuid, 1)
                delete(params, "uuid")
        }
        return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
@@ -308,6 +308,79 @@ func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOpti
        return resp, err
 }
 
+func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserCreate
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserUpdate
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserUpdateUUID
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserUpdateUUID
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserUpdateUUID
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
+       ep := arvados.EndpointUserUpdateUUID
+       var resp map[string]interface{}
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserUpdateUUID
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserGet
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserGetCurrent
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserGetSystem
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
+       ep := arvados.EndpointUserList
+       var resp arvados.UserList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
+       ep := arvados.EndpointUserDelete
+       var resp arvados.User
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
 func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
        ep := arvados.EndpointAPIClientAuthorizationCurrent
        var resp arvados.APIClientAuthorization
@@ -334,3 +407,10 @@ func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCrea
        err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
        return resp, err
 }
+
+func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
+       ep := arvados.APIEndpoint{Method: "PATCH", Path: "arvados/v1/users/batch_update"}
+       var resp arvados.UserList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
index 5531cf71d344cbb795caaa2cac670d7a3ff88ca1..7d6ddb317621fde9800c385cc024218d16eb4630 100644 (file)
@@ -20,26 +20,41 @@ var (
        EndpointConfigGet                     = APIEndpoint{"GET", "arvados/v1/config", ""}
        EndpointLogin                         = APIEndpoint{"GET", "login", ""}
        EndpointCollectionCreate              = APIEndpoint{"POST", "arvados/v1/collections", "collection"}
-       EndpointCollectionUpdate              = APIEndpoint{"PATCH", "arvados/v1/collections/:uuid", "collection"}
-       EndpointCollectionGet                 = APIEndpoint{"GET", "arvados/v1/collections/:uuid", ""}
+       EndpointCollectionUpdate              = APIEndpoint{"PATCH", "arvados/v1/collections/{uuid}", "collection"}
+       EndpointCollectionGet                 = APIEndpoint{"GET", "arvados/v1/collections/{uuid}", ""}
        EndpointCollectionList                = APIEndpoint{"GET", "arvados/v1/collections", ""}
-       EndpointCollectionProvenance          = APIEndpoint{"GET", "arvados/v1/collections/:uuid/provenance", ""}
-       EndpointCollectionUsedBy              = APIEndpoint{"GET", "arvados/v1/collections/:uuid/used_by", ""}
-       EndpointCollectionDelete              = APIEndpoint{"DELETE", "arvados/v1/collections/:uuid", ""}
-       EndpointCollectionTrash               = APIEndpoint{"POST", "arvados/v1/collections/:uuid/trash", ""}
-       EndpointCollectionUntrash             = APIEndpoint{"POST", "arvados/v1/collections/:uuid/untrash", ""}
+       EndpointCollectionProvenance          = APIEndpoint{"GET", "arvados/v1/collections/{uuid}/provenance", ""}
+       EndpointCollectionUsedBy              = APIEndpoint{"GET", "arvados/v1/collections/{uuid}/used_by", ""}
+       EndpointCollectionDelete              = APIEndpoint{"DELETE", "arvados/v1/collections/{uuid}", ""}
+       EndpointCollectionTrash               = APIEndpoint{"POST", "arvados/v1/collections/{uuid}/trash", ""}
+       EndpointCollectionUntrash             = APIEndpoint{"POST", "arvados/v1/collections/{uuid}/untrash", ""}
        EndpointSpecimenCreate                = APIEndpoint{"POST", "arvados/v1/specimens", "specimen"}
-       EndpointSpecimenUpdate                = APIEndpoint{"PATCH", "arvados/v1/specimens/:uuid", "specimen"}
-       EndpointSpecimenGet                   = APIEndpoint{"GET", "arvados/v1/specimens/:uuid", ""}
+       EndpointSpecimenUpdate                = APIEndpoint{"PATCH", "arvados/v1/specimens/{uuid}", "specimen"}
+       EndpointSpecimenGet                   = APIEndpoint{"GET", "arvados/v1/specimens/{uuid}", ""}
        EndpointSpecimenList                  = APIEndpoint{"GET", "arvados/v1/specimens", ""}
-       EndpointSpecimenDelete                = APIEndpoint{"DELETE", "arvados/v1/specimens/:uuid", ""}
+       EndpointSpecimenDelete                = APIEndpoint{"DELETE", "arvados/v1/specimens/{uuid}", ""}
        EndpointContainerCreate               = APIEndpoint{"POST", "arvados/v1/containers", "container"}
-       EndpointContainerUpdate               = APIEndpoint{"PATCH", "arvados/v1/containers/:uuid", "container"}
-       EndpointContainerGet                  = APIEndpoint{"GET", "arvados/v1/containers/:uuid", ""}
+       EndpointContainerUpdate               = APIEndpoint{"PATCH", "arvados/v1/containers/{uuid}", "container"}
+       EndpointContainerGet                  = APIEndpoint{"GET", "arvados/v1/containers/{uuid}", ""}
        EndpointContainerList                 = APIEndpoint{"GET", "arvados/v1/containers", ""}
-       EndpointContainerDelete               = APIEndpoint{"DELETE", "arvados/v1/containers/:uuid", ""}
-       EndpointContainerLock                 = APIEndpoint{"POST", "arvados/v1/containers/:uuid/lock", ""}
-       EndpointContainerUnlock               = APIEndpoint{"POST", "arvados/v1/containers/:uuid/unlock", ""}
+       EndpointContainerDelete               = APIEndpoint{"DELETE", "arvados/v1/containers/{uuid}", ""}
+       EndpointContainerLock                 = APIEndpoint{"POST", "arvados/v1/containers/{uuid}/lock", ""}
+       EndpointContainerUnlock               = APIEndpoint{"POST", "arvados/v1/containers/{uuid}/unlock", ""}
+       EndpointUserActivate                  = APIEndpoint{"POST", "arvados/v1/users/{uuid}/activate", ""}
+       EndpointUserCreate                    = APIEndpoint{"POST", "arvados/v1/users", "user"}
+       EndpointUserCurrent                   = APIEndpoint{"GET", "arvados/v1/users/current", ""}
+       EndpointUserDelete                    = APIEndpoint{"DELETE", "arvados/v1/users/{uuid}", ""}
+       EndpointUserGet                       = APIEndpoint{"GET", "arvados/v1/users/{uuid}", ""}
+       EndpointUserGetCurrent                = APIEndpoint{"GET", "arvados/v1/users/current", ""}
+       EndpointUserGetSystem                 = APIEndpoint{"GET", "arvados/v1/users/system", ""}
+       EndpointUserList                      = APIEndpoint{"GET", "arvados/v1/users", ""}
+       EndpointUserMerge                     = APIEndpoint{"POST", "arvados/v1/users/merge", ""}
+       EndpointUserSetup                     = APIEndpoint{"POST", "arvados/v1/users/setup", ""}
+       EndpointUserSystem                    = APIEndpoint{"GET", "arvados/v1/users/system", ""}
+       EndpointUserUnsetup                   = APIEndpoint{"POST", "arvados/v1/users/{uuid}/unsetup", ""}
+       EndpointUserUpdate                    = APIEndpoint{"PATCH", "arvados/v1/users/{uuid}", "user"}
+       EndpointUserUpdateUUID                = APIEndpoint{"POST", "arvados/v1/users/{uuid}/update_uuid", ""}
+       EndpointUserBatchUpdate               = APIEndpoint{"PATCH", "arvados/v1/users/batch", ""}
        EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
 )
 
@@ -80,6 +95,37 @@ type UpdateOptions struct {
        Attrs map[string]interface{} `json:"attrs"`
 }
 
+type UpdateUUIDOptions struct {
+       UUID    string `json:"uuid"`
+       NewUUID string `json:"new_uuid"`
+}
+
+type UserActivateOptions struct {
+       UUID string `json:"uuid"`
+}
+
+type UserSetupOptions struct {
+       UUID                  string                 `json:"uuid"`
+       Email                 string                 `json:"email"`
+       OpenIDPrefix          string                 `json:"openid_prefix"`
+       RepoName              string                 `json:"repo_name"`
+       VMUUID                string                 `json:"vm_uuid"`
+       SendNotificationEmail bool                   `json:"send_notification_email"`
+       Attrs                 map[string]interface{} `json:"attrs"`
+}
+
+type UserMergeOptions struct {
+       NewUserUUID  string `json:"new_user_uuid,omitempty"`
+       OldUserUUID  string `json:"old_user_uuid,omitempty"`
+       NewUserToken string `json:"new_user_token,omitempty"`
+}
+
+type UserBatchUpdateOptions struct {
+       Updates map[string]map[string]interface{} `json:"updates"`
+}
+
+type UserBatchUpdateResponse struct{}
+
 type DeleteOptions struct {
        UUID string `json:"uuid"`
 }
@@ -115,5 +161,18 @@ type API interface {
        SpecimenGet(ctx context.Context, options GetOptions) (Specimen, error)
        SpecimenList(ctx context.Context, options ListOptions) (SpecimenList, error)
        SpecimenDelete(ctx context.Context, options DeleteOptions) (Specimen, error)
+       UserCreate(ctx context.Context, options CreateOptions) (User, error)
+       UserUpdate(ctx context.Context, options UpdateOptions) (User, error)
+       UserUpdateUUID(ctx context.Context, options UpdateUUIDOptions) (User, error)
+       UserMerge(ctx context.Context, options UserMergeOptions) (User, error)
+       UserActivate(ctx context.Context, options UserActivateOptions) (User, error)
+       UserSetup(ctx context.Context, options UserSetupOptions) (map[string]interface{}, error)
+       UserUnsetup(ctx context.Context, options GetOptions) (User, error)
+       UserGet(ctx context.Context, options GetOptions) (User, error)
+       UserGetCurrent(ctx context.Context, options GetOptions) (User, error)
+       UserGetSystem(ctx context.Context, options GetOptions) (User, error)
+       UserList(ctx context.Context, options ListOptions) (UserList, error)
+       UserDelete(ctx context.Context, options DeleteOptions) (User, error)
+       UserBatchUpdate(context.Context, UserBatchUpdateOptions) (UserList, error)
        APIClientAuthorizationCurrent(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
 }
index 27d2b28a42b6c5c4312d0aa16624e8061103ac5d..30bc094d07c95e91b9a930c0bb923e4d4d6d908e 100644 (file)
@@ -4,13 +4,26 @@
 
 package arvados
 
+import "time"
+
 // User is an arvados#user record
 type User struct {
-       UUID     string `json:"uuid"`
-       IsActive bool   `json:"is_active"`
-       IsAdmin  bool   `json:"is_admin"`
-       Username string `json:"username"`
-       Email    string `json:"email"`
+       UUID                 string                 `json:"uuid"`
+       IsActive             bool                   `json:"is_active"`
+       IsAdmin              bool                   `json:"is_admin"`
+       Username             string                 `json:"username"`
+       Email                string                 `json:"email"`
+       FullName             string                 `json:"full_name"`
+       FirstName            string                 `json:"first_name"`
+       LastName             string                 `json:"last_name"`
+       IdentityURL          string                 `json:"identity_url"`
+       IsInvited            bool                   `json:"is_invited"`
+       OwnerUUID            string                 `json:"owner_uuid"`
+       CreatedAt            time.Time              `json:"created_at"`
+       ModifiedAt           time.Time              `json:"modified_at"`
+       ModifiedByUserUUID   string                 `json:"modified_by_user_uuid"`
+       ModifiedByClientUUID string                 `json:"modified_by_client_uuid"`
+       Prefs                map[string]interface{} `json:"prefs"`
 }
 
 // UserList is an arvados#userList resource.
index 24e9f190865b0456a8be431449239e2ec3aba6db..91e3ee8ba66dc3df73c462ac3adaee2300c2ba23 100644 (file)
@@ -121,6 +121,58 @@ func (as *APIStub) SpecimenDelete(ctx context.Context, options arvados.DeleteOpt
        as.appendCall(as.SpecimenDelete, ctx, options)
        return arvados.Specimen{}, as.Error
 }
+func (as *APIStub) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
+       as.appendCall(as.UserCreate, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
+       as.appendCall(as.UserUpdate, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
+       as.appendCall(as.UserUpdateUUID, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
+       as.appendCall(as.UserActivate, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
+       as.appendCall(as.UserSetup, ctx, options)
+       return nil, as.Error
+}
+func (as *APIStub) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       as.appendCall(as.UserUnsetup, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       as.appendCall(as.UserGet, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       as.appendCall(as.UserGetCurrent, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
+       as.appendCall(as.UserGetSystem, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
+       as.appendCall(as.UserList, ctx, options)
+       return arvados.UserList{}, as.Error
+}
+func (as *APIStub) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
+       as.appendCall(as.UserDelete, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
+       as.appendCall(as.UserMerge, ctx, options)
+       return arvados.User{}, as.Error
+}
+func (as *APIStub) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
+       as.appendCall(as.UserBatchUpdate, ctx, options)
+       return arvados.UserList{}, as.Error
+}
 func (as *APIStub) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
        as.appendCall(as.APIClientAuthorizationCurrent, ctx, options)
        return arvados.APIClientAuthorization{}, as.Error
@@ -137,7 +189,6 @@ func (as *APIStub) Calls(method interface{}) []APIStubCall {
        defer as.mtx.Unlock()
        var calls []APIStubCall
        for _, call := range as.calls {
-
                if method == nil || (runtime.FuncForPC(reflect.ValueOf(call.Method).Pointer()).Name() ==
                        runtime.FuncForPC(reflect.ValueOf(method).Pointer()).Name()) {
                        calls = append(calls, call)
index be29bc23ef394df21ac95a19ea169873d83af4ca..10b95c037116da0dc75f01b643a0d9fb587d4c46 100644 (file)
@@ -13,6 +13,7 @@ const (
        AdminToken              = "4axaw8zxe0qm22wa6urpp5nskcne8z88cvbupv653y1njyi05h"
        AnonymousToken          = "4kg6k6lzmp9kj4cpkcoxie964cmvjahbt4fod9zru44k4jqdmi"
        DataManagerToken        = "320mkve8qkswstz7ff61glpk3mhgghmg67wmic7elw4z41pke1"
+       SystemRootToken         = "systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy"
        ManagementToken         = "jg3ajndnq63sywcd50gbs5dskdc9ckkysb0nsqmfz08nwf17nl"
        ActiveUserUUID          = "zzzzz-tpzed-xurymjxw79nv3jz"
        FederatedActiveUserUUID = "zbbbb-tpzed-xurymjxw79nv3jz"
index 48aabbbe409a5c672d7917ae8b57b73973fd7bec..dd74df2367812d48272e6be28827e7e0b806c398 100644 (file)
@@ -719,7 +719,7 @@ def setup_config():
             "zzzzz": {
                 "EnableBetaController14287": ('14287' in os.environ.get('ARVADOS_EXPERIMENTAL', '')),
                 "ManagementToken": "e687950a23c3a9bceec28c6223a06c79",
-                "SystemRootToken": auth_token('data_manager'),
+                "SystemRootToken": auth_token('system_user'),
                 "API": {
                     "RequestTimeout": "30s",
                 },
index c4bd33fda6136bbf241cc3fb702684be58636ab4..1d698afc7f9acc53a2786446dd6264fa15707b0e 100644 (file)
@@ -334,4 +334,4 @@ DEPENDENCIES
   uglifier (~> 2.0)
 
 BUNDLED WITH
-   2.0.2
+   1.17.3
index 2889eacee644ba080439faa6a0e17ad629c8171c..ddf74cec67306605c47469af52ca3644f0e812ad 100644 (file)
@@ -4,12 +4,31 @@
 
 class Arvados::V1::UsersController < ApplicationController
   accept_attribute_as_json :prefs, Hash
+  accept_param_as_json :updates
 
   skip_before_action :find_object_by_uuid, only:
-    [:activate, :current, :system, :setup, :merge]
+    [:activate, :current, :system, :setup, :merge, :batch_update]
   skip_before_action :render_404_if_no_object, only:
-    [:activate, :current, :system, :setup, :merge]
-  before_action :admin_required, only: [:setup, :unsetup, :update_uuid]
+    [:activate, :current, :system, :setup, :merge, :batch_update]
+  before_action :admin_required, only: [:setup, :unsetup, :update_uuid, :batch_update]
+
+  # Internal API used by controller to update local cache of user
+  # records from LoginCluster.
+  def batch_update
+    @objects = []
+    params[:updates].andand.each do |uuid, attrs|
+      begin
+        u = User.find_or_create_by(uuid: uuid)
+      rescue ActiveRecord::RecordNotUnique
+        retry
+      end
+      u.update_attributes!(attrs)
+      @objects << u
+    end
+    @offset = 0
+    @limit = -1
+    render_list
+  end
 
   def current
     if current_user
index f9e4e943b7a31edb39852adc649d4ee37478fb2f..77fc0a45afb32ff7ea93595a4b97ff66cd128f63 100644 (file)
@@ -237,20 +237,16 @@ class ApiClientAuthorization < ArvadosModel
 
       # Sync user record.
       if remote_user_prefix == Rails.configuration.Login.LoginCluster
-        # Remote cluster controls our user database, copy both
-        # 'is_active' and 'is_admin'
-        user.is_active = remote_user['is_active']
+        # Remote cluster controls our user database, set is_active if
+        # remote is active.  If remote is not active, user will be
+        # unsetup (see below).
+        user.is_active = true if remote_user['is_active']
         user.is_admin = remote_user['is_admin']
       else
         if Rails.configuration.Users.NewUsersAreActive ||
            Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"]
-          # Default policy is to activate users, so match activate
-          # with the remote record.
-          user.is_active = remote_user['is_active']
-        elsif !remote_user['is_active']
-          # Deactivate user if the remote is inactive, otherwise don't
-          # change 'is_active'.
-          user.is_active = false
+          # Default policy is to activate users
+          user.is_active = true if remote_user['is_active']
         end
       end
 
@@ -259,6 +255,10 @@ class ApiClientAuthorization < ArvadosModel
       end
 
       act_as_system_user do
+        if user.is_active && !remote_user['is_active']
+          user.unsetup
+        end
+
         user.save!
 
         # We will accept this token (and avoid reloading the user
index a49aa6f56a22ead2398198401c15be2a8860ec97..d9bb18c3e505338daee7152746fe58af267fff4f 100644 (file)
@@ -21,6 +21,7 @@ class User < ArvadosModel
             },
             uniqueness: true,
             allow_nil: true)
+  validate :must_unsetup_to_deactivate
   before_update :prevent_privilege_escalation
   before_update :prevent_inactive_admin
   before_update :verify_repositories_empty, :if => Proc.new { |user|
@@ -188,17 +189,19 @@ class User < ArvadosModel
 
   # create links
   def setup(openid_prefix:, repo_name: nil, vm_uuid: nil)
-    oid_login_perm = create_oid_login_perm openid_prefix
     repo_perm = create_user_repo_link repo_name
     vm_login_perm = create_vm_login_permission_link(vm_uuid, username) if vm_uuid
     group_perm = create_user_group_link
 
-    return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
+    return [repo_perm, vm_login_perm, group_perm, self].compact
   end
 
   # delete user signatures, login, repo, and vm perms, and mark as inactive
   def unsetup
     # delete oid_login_perms for this user
+    #
+    # note: these permission links are obsolete, they have no effect
+    # on anything and they are not created for new users.
     Link.where(tail_uuid: self.email,
                      link_class: 'permission',
                      name: 'can_login').destroy_all
@@ -234,6 +237,37 @@ class User < ArvadosModel
     self.save!
   end
 
+  def must_unsetup_to_deactivate
+    if self.is_active_changed? &&
+       self.is_active_was == true &&
+       !self.is_active
+
+      group = Group.where(name: 'All users').select do |g|
+        g[:uuid].match(/-f+$/)
+      end.first
+
+      # When a user is set up, they are added to the "All users"
+      # group.  A user that is part of the "All users" group is
+      # allowed to self-activate.
+      #
+      # It doesn't make sense to deactivate a user (set is_active =
+      # false) without first removing them from the "All users" group,
+      # because they would be able to immediately reactivate
+      # themselves.
+      #
+      # The 'unsetup' method removes the user from the "All users"
+      # group (and also sets is_active = false) so send a message
+      # explaining the correct way to deactivate a user.
+      #
+      if Link.where(tail_uuid: self.uuid,
+                    head_uuid: group[:uuid],
+                    link_class: 'permission',
+                    name: 'can_read').any?
+        errors.add :is_active, "cannot be set to false directly, use the 'Deactivate' button on Workbench, or the 'unsetup' API call"
+      end
+    end
+  end
+
   def set_initial_username(requested: false)
     if !requested.is_a?(String) || requested.empty?
       email_parts = email.partition("@")
@@ -577,30 +611,6 @@ class User < ArvadosModel
     merged
   end
 
-  def create_oid_login_perm(openid_prefix)
-    # Check oid_login_perm
-    oid_login_perms = Link.where(tail_uuid: self.email,
-                                 head_uuid: self.uuid,
-                                 link_class: 'permission',
-                                 name: 'can_login')
-
-    if !oid_login_perms.any?
-      # create openid login permission
-      oid_login_perm = Link.create!(link_class: 'permission',
-                                   name: 'can_login',
-                                   tail_uuid: self.email,
-                                   head_uuid: self.uuid,
-                                   properties: {
-                                     "identity_url_prefix" => openid_prefix,
-                                   })
-      logger.info { "openid login permission: " + oid_login_perm[:uuid] }
-    else
-      oid_login_perm = oid_login_perms.first
-    end
-
-    return oid_login_perm
-  end
-
   def create_user_repo_link(repo_name)
     # repo_name is optional
     if not repo_name
index b54c3c5bf170cc431140a0925b9846e7f172b397..8afd22192a62f56c002b363bf63625e07009fcec 100644 (file)
@@ -83,6 +83,7 @@ Server::Application.routes.draw do
         post 'unsetup', on: :member
         post 'update_uuid', on: :member
         post 'merge', on: :collection
+        patch 'batch_update', on: :collection
       end
       resources :virtual_machines do
         get 'logins', on: :member
index 537fe525270333317cf6ef1fb77c53a6c035dce2..cfcd917d6538ec2eacd584cc2ac18b5c1afee7e9 100644 (file)
@@ -52,7 +52,7 @@ class Arvados::V1::RepositoriesControllerTest < ActionController::TestCase
     end
     act_as_system_user do
       u = users(:active)
-      u.is_active = false
+      u.unsetup
       u.save!
     end
     authorize_with :admin
index d5db1039645cbadffc45d93317cc87664b889b38..e3763c389e243a44b0bd891a9809713f481788a1 100644 (file)
@@ -113,11 +113,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil created['email'], 'expected non-nil email'
     assert_nil created['identity_url'], 'expected no identity_url'
 
-    # arvados#user, repo link and link add user to 'All users' group
-    verify_links_added 4
-
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'User'
+    # repo link and link add user to 'All users' group
+    verify_links_added 3
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         "foo/#{repo_name}", created['uuid'], 'arvados#repository', true, 'Repository'
@@ -255,8 +252,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil response_object['uuid'], 'expected uuid for the new user'
     assert_equal response_object['email'], 'foo@example.com', 'expected given email'
 
-    # four extra links; system_group, login, group and repo perms
-    verify_links_added 4
+    # three extra links; system_group, group and repo perms
+    verify_links_added 3
   end
 
   test "setup user with fake vm and expect error" do
@@ -292,8 +289,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil response_object['uuid'], 'expected uuid for the new user'
     assert_equal response_object['email'], 'foo@example.com', 'expected given email'
 
-    # five extra links; system_group, login, group, vm, repo
-    verify_links_added 5
+    # four extra links; system_group, group, vm, repo
+    verify_links_added 4
   end
 
   test "setup user with valid email, no vm and no repo as input" do
@@ -310,11 +307,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil response_object['uuid'], 'expected uuid for new user'
     assert_equal response_object['email'], 'foo@example.com', 'expected given email'
 
-    # three extra links; system_group, login, and group
-    verify_links_added 3
-
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        response_object['uuid'], response_object['email'], 'arvados#user', false, 'User'
+    # two extra links; system_group, and group
+    verify_links_added 2
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', response_object['uuid'], 'arvados#group', true, 'Group'
@@ -347,8 +341,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_equal 'test_first_name', response_object['first_name'],
         'expecting first name'
 
-    # five extra links; system_group, login, group, repo and vm
-    verify_links_added 5
+    # four extra links; system_group, group, repo and vm
+    verify_links_added 4
   end
 
   test "setup user with an existing user email and check different object is created" do
@@ -370,8 +364,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_equal response_object['uuid'], inactive_user['uuid'],
         'expected different uuid after create operation'
     assert_equal inactive_user['email'], response_object['email'], 'expected given email'
-    # system_group, openid, group, and repo. No vm link.
-    verify_links_added 4
+    # system_group, group, and repo. No vm link.
+    verify_links_added 3
   end
 
   test "setup user with openid prefix" do
@@ -398,11 +392,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_nil created['identity_url'], 'expected no identity_url'
 
     # verify links
-    # four new links: system_group, arvados#user, repo, and 'All users' group.
-    verify_links_added 4
-
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'User'
+    # three new links: system_group, repo, and 'All users' group.
+    verify_links_added 3
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
@@ -457,12 +448,10 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil created['email'], 'expected non-nil email'
     assert_nil created['identity_url'], 'expected no identity_url'
 
-    # five new links: system_group, arvados#user, repo, vm and 'All
-    # users' group link
-    verify_links_added 5
+    # four new links: system_group, repo, vm and 'All users' group link
+    verify_links_added 4
 
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'User'
+    # system_group isn't part of the response.  See User#add_system_group_permission_link
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
@@ -1043,6 +1032,47 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_nil(users(:project_viewer).redirect_to_user_uuid)
   end
 
+  test "batch update fails for non-admin" do
+    authorize_with(:active)
+    patch(:batch_update, params: {updates: {}})
+    assert_response(403)
+  end
+
+  test "batch update" do
+    existinguuid = 'remot-tpzed-foobarbazwazqux'
+    newuuid = 'remot-tpzed-newnarnazwazqux'
+    act_as_system_user do
+      User.create!(uuid: existinguuid, email: 'root@existing.example.com')
+    end
+
+    authorize_with(:admin)
+    patch(:batch_update,
+          params: {
+            updates: {
+              existinguuid => {
+                'first_name' => 'root',
+                'email' => 'root@remot.example.com',
+                'is_active' => true,
+                'is_admin' => true,
+                'prefs' => {'foo' => 'bar'},
+              },
+              newuuid => {
+                'first_name' => 'noot',
+                'email' => 'root@remot.example.com',
+              },
+            }})
+    assert_response(:success)
+
+    assert_equal('root', User.find_by_uuid(existinguuid).first_name)
+    assert_equal('root@remot.example.com', User.find_by_uuid(existinguuid).email)
+    assert_equal(true, User.find_by_uuid(existinguuid).is_active)
+    assert_equal(true, User.find_by_uuid(existinguuid).is_admin)
+    assert_equal({'foo' => 'bar'}, User.find_by_uuid(existinguuid).prefs)
+
+    assert_equal('noot', User.find_by_uuid(newuuid).first_name)
+    assert_equal('root@remot.example.com', User.find_by_uuid(newuuid).email)
+  end
+
   NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
                          "last_name", "username"].sort
 
index 585619e7479cddc88e051cf803c3269588794b8c..6ca9977a5ebaa6b8ae672d015365b57a22b0d889 100644 (file)
@@ -48,11 +48,9 @@ module UsersTestHelper
     oid_login_perms = Link.where(tail_uuid: email,
                                  link_class: 'permission',
                                  name: 'can_login').where("head_uuid like ?", User.uuid_like_pattern)
-    if expect_oid_login_perms
-      assert oid_login_perms.any?, "expected oid_login_perms"
-    else
-      assert !oid_login_perms.any?, "expected all oid_login_perms deleted"
-    end
+
+    # these don't get added any more!  they shouldn't appear ever.
+    assert !oid_login_perms.any?, "expected all oid_login_perms deleted"
 
     repo_perms = Link.where(tail_uuid: uuid,
                             link_class: 'permission',
index b3cfe27190e78dce0e15182e7724d283c7b1755f..04a45420fd4b768c105e89f8bd600739d69c8a6f 100644 (file)
@@ -143,6 +143,30 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     assert_equal 'blarney@example.com', json_response['email']
   end
 
+  test 'remote user is deactivated' do
+    Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = true
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_response :success
+    assert_equal true, json_response['is_active']
+
+    # revoke original token
+    @stub_content[:is_active] = false
+
+    # simulate cache expiry
+    ApiClientAuthorization.where(
+      uuid: salted_active_token(remote: 'zbbbb').split('/')[1]).
+      update_all(expires_at: db_current_time - 1.minute)
+
+    # re-authorize after cache expires
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_equal false, json_response['is_active']
+
+  end
+
   test 'authenticate with remote token, remote username conflicts with local' do
     @stub_content[:username] = 'active'
     get '/arvados/v1/users/current',
@@ -255,6 +279,24 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     refute_includes(group_uuids, groups(:testusergroup_admins).uuid)
   end
 
+  test 'do not auto-activate user from untrusted cluster' do
+    Rails.configuration.RemoteClusters['zbbbb'].AutoSetupNewUsers = false
+    Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = false
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_response :success
+    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal false, json_response['is_admin']
+    assert_equal false, json_response['is_active']
+    assert_equal 'foo@example.com', json_response['email']
+    assert_equal 'barney', json_response['username']
+    post '/arvados/v1/users/zbbbb-tpzed-000000000000000/activate',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_response 422
+  end
+
   test 'auto-activate user from trusted cluster' do
     Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = true
     get '/arvados/v1/users/current',
index fdb8628c5d1377abfc9a2273c27d3b254265240d..fcc0ce4e5266b5b032d997535a72cf86d3382cbf 100644 (file)
@@ -111,6 +111,7 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
    ].each do |testcase|
     test "user auto-activate #{testcase.inspect}" do
       # Configure auto_setup behavior according to testcase[:cfg]
+      Rails.configuration.Users.NewUsersAreActive = false
       Rails.configuration.Users.AutoSetupNewUsers = testcase[:cfg][:auto]
       Rails.configuration.Users.AutoSetupNewUsersWithVmUUID =
         (testcase[:cfg][:vm] ? virtual_machines(:testvm).uuid : "")
index 11ebb3f4fd7c96c61f0aae2be6c968b973364c87..6a1d5c8011027b817a23e16805ae9f2b12bdf8d0 100644 (file)
@@ -36,9 +36,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     assert_not_nil created['email'], 'expected non-nil email'
     assert_nil created['identity_url'], 'expected no identity_url'
 
-    # arvados#user, repo link and link add user to 'All users' group
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'arvados#user'
+    # repo link and link add user to 'All users' group
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
@@ -117,9 +115,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     assert_not_nil created['email'], 'expected non-nil email'
     assert_equal created['email'], 'foo@example.com', 'expected input email'
 
-    # three new links: system_group, arvados#user, and 'All users' group.
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'arvados#user'
+    # two new links: system_group, and 'All users' group.
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -196,9 +192,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     assert_not_nil created['uuid'], 'expected uuid for the new user'
     assert_equal created['email'], 'foo@example.com', 'expected given email'
 
-    # five extra links: system_group, login, group, repo and vm
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'arvados#user'
+    # four extra links: system_group, login, group, repo and vm
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -339,4 +333,119 @@ class UsersTest < ActionDispatch::IntegrationTest
 
   end
 
+  test "cannot set is_activate to false directly" do
+    post('/arvados/v1/users',
+      params: {
+        user: {
+          email: "bob@example.com",
+          username: "bobby"
+        },
+      },
+      headers: auth(:admin))
+    assert_response(:success)
+    user = json_response
+    assert_equal false, user['is_active']
+
+    post("/arvados/v1/users/#{user['uuid']}/activate",
+      params: {},
+      headers: auth(:admin))
+    assert_response(:success)
+    user = json_response
+    assert_equal true, user['is_active']
+
+    put("/arvados/v1/users/#{user['uuid']}",
+         params: {
+           user: {is_active: false}
+         },
+         headers: auth(:admin))
+    assert_response 422
+  end
+
+  test "cannot self activate when AutoSetupNewUsers is false" do
+    Rails.configuration.Users.NewUsersAreActive = false
+    Rails.configuration.Users.AutoSetupNewUsers = false
+
+    user = nil
+    token = nil
+    act_as_system_user do
+      user = User.create!(email: "bob@example.com", username: "bobby")
+      ap = ApiClientAuthorization.create!(user: user, api_client: ApiClient.all.first)
+      token = ap.api_token
+    end
+
+    get("/arvados/v1/users/#{user['uuid']}",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response(:success)
+    user = json_response
+    assert_equal false, user['is_active']
+
+    post("/arvados/v1/users/#{user['uuid']}/activate",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response 422
+    assert_match(/Cannot activate without being invited/, json_response['errors'][0])
+  end
+
+
+  test "cannot self activate after unsetup" do
+    Rails.configuration.Users.NewUsersAreActive = false
+    Rails.configuration.Users.AutoSetupNewUsers = false
+
+    user = nil
+    token = nil
+    act_as_system_user do
+      user = User.create!(email: "bob@example.com", username: "bobby")
+      ap = ApiClientAuthorization.create!(user: user, api_client_id: 0)
+      token = ap.api_token
+    end
+
+    post("/arvados/v1/users/setup",
+        params: {uuid: user['uuid']},
+        headers: auth(:admin))
+    assert_response :success
+
+    post("/arvados/v1/users/#{user['uuid']}/activate",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response 403
+    assert_match(/Cannot activate without user agreements/, json_response['errors'][0])
+
+    post("/arvados/v1/user_agreements/sign",
+        params: {uuid: 'zzzzz-4zz18-t68oksiu9m80s4y'},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response :success
+
+    post("/arvados/v1/users/#{user['uuid']}/activate",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response :success
+
+    get("/arvados/v1/users/#{user['uuid']}",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response(:success)
+    user = json_response
+    assert_equal true, user['is_active']
+
+    post("/arvados/v1/users/#{user['uuid']}/unsetup",
+        params: {},
+        headers: auth(:admin))
+    assert_response :success
+
+    get("/arvados/v1/users/#{user['uuid']}",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response(:success)
+    user = json_response
+    assert_equal false, user['is_active']
+
+    post("/arvados/v1/users/#{user['uuid']}/activate",
+        params: {},
+        headers: {"HTTP_AUTHORIZATION" => "Bearer #{token}"})
+    assert_response 422
+    assert_match(/Cannot activate without being invited/, json_response['errors'][0])
+  end
+
+
 end
index adc37cc5951d231ceef9bedae120f495ad780917..3480e55318917015b1d33dd8d6deac26d3b82fe8 100644 (file)
@@ -458,14 +458,6 @@ class UserTest < ActiveSupport::TestCase
     resp_user = find_obj_in_resp response, 'User'
     verify_user resp_user, email
 
-    oid_login_perm = find_obj_in_resp response, 'Link', 'arvados#user'
-
-    verify_link oid_login_perm, 'permission', 'can_login', resp_user[:email],
-        resp_user[:uuid]
-
-    assert_equal openid_prefix, oid_login_perm[:properties]['identity_url_prefix'],
-        'expected identity_url_prefix not found for oid_login_perm'
-
     group_perm = find_obj_in_resp response, 'Link', 'arvados#group'
     verify_link group_perm, 'permission', 'can_read', resp_user[:uuid], nil
 
@@ -503,14 +495,6 @@ class UserTest < ActiveSupport::TestCase
     resp_user = find_obj_in_resp response, 'User'
     verify_user resp_user, email
 
-    oid_login_perm = find_obj_in_resp response, 'Link', 'arvados#user'
-
-    verify_link oid_login_perm, 'permission', 'can_login', resp_user[:email],
-        resp_user[:uuid]
-
-    assert_equal openid_prefix, oid_login_perm[:properties]['identity_url_prefix'],
-        'expected identity_url_prefix not found for oid_login_perm'
-
     group_perm = find_obj_in_resp response, 'Link', 'arvados#group'
     verify_link group_perm, 'permission', 'can_read', resp_user[:uuid], nil
 
@@ -535,12 +519,6 @@ class UserTest < ActiveSupport::TestCase
     resp_user = find_obj_in_resp response, 'User'
     verify_user resp_user, email
 
-    oid_login_perm = find_obj_in_resp response, 'Link', 'arvados#user'
-    verify_link oid_login_perm, 'permission', 'can_login', resp_user[:email],
-        resp_user[:uuid]
-    assert_equal openid_prefix, oid_login_perm[:properties]['identity_url_prefix'],
-        'expected identity_url_prefix not found for oid_login_perm'
-
     group_perm = find_obj_in_resp response, 'Link', 'arvados#group'
     verify_link group_perm, 'permission', 'can_read', resp_user[:uuid], nil
 
@@ -646,9 +624,7 @@ class UserTest < ActiveSupport::TestCase
     verify_link_exists(Rails.configuration.Users.AutoSetupNewUsers || active,
                        groups(:all_users).uuid, user.uuid,
                        "permission", "can_read")
-    # Check for OID login link.
-    verify_link_exists(Rails.configuration.Users.AutoSetupNewUsers || active,
-                       user.uuid, user.email, "permission", "can_login")
+
     # Check for repository.
     if named_repo = (prior_repo or
                      Repository.where(name: expect_repo_name).first)
index 5b0dc123ae49a627a98c4f5254ff6a1649e869e6..43816a213b157c2f293f6d54bc0812005b227a28 100644 (file)
@@ -39,7 +39,7 @@ func (s *integrationSuite) SetUpSuite(c *check.C) {
        arvadostest.StartKeep(4, true)
 
        arv, err := arvadosclient.MakeArvadosClient()
-       arv.ApiToken = arvadostest.DataManagerToken
+       arv.ApiToken = arvadostest.SystemRootToken
        c.Assert(err, check.IsNil)
 
        s.keepClient, err = keepclient.MakeKeepClient(arv)
@@ -71,7 +71,7 @@ func (s *integrationSuite) SetUpTest(c *check.C) {
 
        s.client = &arvados.Client{
                APIHost:   os.Getenv("ARVADOS_API_HOST"),
-               AuthToken: arvadostest.DataManagerToken,
+               AuthToken: arvadostest.SystemRootToken,
                Insecure:  true,
        }
 }
index a6a73c831721e0c4489c041e2d893f5ac68599f9..00dd3c317c570644ac9b51e1ce38b6ba9ab6ca17 100644 (file)
@@ -485,7 +485,7 @@ func (s *ServerRequiredSuite) TestGetIndex(c *C) {
        _, _, err = kc.PutB([]byte("some-more-index-data"))
        c.Check(err, IsNil)
 
-       kc.Arvados.ApiToken = arvadostest.DataManagerToken
+       kc.Arvados.ApiToken = arvadostest.SystemRootToken
 
        // Invoke GetIndex
        for _, spec := range []struct {
index 54b4871fab89a59a2b95ba893912080ec13ff070..8247ce480dd37abe96f8f2c7514745ac517c8e34 100644 (file)
@@ -46,7 +46,7 @@ func testCluster(t TB) *arvados.Cluster {
        if err != nil {
                t.Fatal(err)
        }
-       cluster.SystemRootToken = arvadostest.DataManagerToken
+       cluster.SystemRootToken = arvadostest.SystemRootToken
        cluster.ManagementToken = arvadostest.ManagementToken
        cluster.Collections.BlobSigning = false
        return cluster
index dd6247f8bb2824879669f6bb7d9c21eae2d61df2..30193415353b2abf2723a91a672810bdd4b82a8d 100644 (file)
@@ -55,7 +55,7 @@ func (s *HandlerSuite) TestMounts(c *check.C) {
                c.Check(resp.Body.String(), check.Equals, "Unauthorized\n")
        }
 
-       tok := arvadostest.DataManagerToken
+       tok := arvadostest.SystemRootToken
 
        // Nonexistent mount UUID
        resp = s.call("GET", "/mounts/X/blocks", tok, nil)
index 6483d6cf01310af98d78b5fd1074b3301004bd83..fd98aa9cb01d605cc12eaf91619f53e44a0edb65 100644 (file)
@@ -89,7 +89,7 @@ func (s *ProxyRemoteSuite) SetUpTest(c *check.C) {
        s.remoteAPI.StartTLS()
        s.cluster = testCluster(c)
        s.cluster.Collections.BlobSigningKey = knownKey
-       s.cluster.SystemRootToken = arvadostest.DataManagerToken
+       s.cluster.SystemRootToken = arvadostest.SystemRootToken
        s.cluster.RemoteClusters = map[string]arvados.RemoteCluster{
                s.remoteClusterID: arvados.RemoteCluster{
                        Host:     strings.Split(s.remoteAPI.URL, "//")[1],
index 44c3b36ee43ccedc7dc61def4d3945d225ed32ee..5f163e87c32188e04ac4a05573ad9d922f185a07 100644 (file)
@@ -85,4 +85,4 @@ DEPENDENCIES
   rake
 
 BUNDLED WITH
-   2.0.2
+   1.17.3
index 85c03399f79e1410582ec21a844558dcf4c2708c..e14704d71dddc72fe7ece655681f526592363831 100755 (executable)
@@ -21,8 +21,8 @@ fi
 cat <<EOF > /usr/src/workbench2/public/config.json
 {
   "API_HOST": "${localip}:${services[controller-ssl]}",
-  "VOCABULARY_URL": "vocabulary-example.json",
-  "FILE_VIEWERS_CONFIG_URL": "file-viewers-example.json"
+  "VOCABULARY_URL": "/vocabulary-example.json",
+  "FILE_VIEWERS_CONFIG_URL": "/file-viewers-example.json"
 }
 EOF
 
index 9c37e38906f0ebd59562c47a50f53dd67f0bdb78..22979dc98fbd0b2c83625bdb89745fa65343fbac 100644 (file)
@@ -89,13 +89,13 @@ func setupRsync(c *C, enforcePermissions bool, replications int) {
        // srcConfig
        var srcConfig apiConfig
        srcConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
-       srcConfig.APIToken = arvadostest.DataManagerToken
+       srcConfig.APIToken = arvadostest.SystemRootToken
        srcConfig.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
 
        // dstConfig
        var dstConfig apiConfig
        dstConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
-       dstConfig.APIToken = arvadostest.DataManagerToken
+       dstConfig.APIToken = arvadostest.SystemRootToken
        dstConfig.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
 
        if enforcePermissions {
@@ -370,7 +370,7 @@ func (s *ServerNotRequiredSuite) TestLoadConfig(c *C) {
        c.Check(err, IsNil)
 
        c.Assert(srcConfig.APIHost, Equals, os.Getenv("ARVADOS_API_HOST"))
-       c.Assert(srcConfig.APIToken, Equals, arvadostest.DataManagerToken)
+       c.Assert(srcConfig.APIToken, Equals, arvadostest.SystemRootToken)
        c.Assert(srcConfig.APIHostInsecure, Equals, arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE")))
        c.Assert(srcConfig.ExternalClient, Equals, false)
 
@@ -378,7 +378,7 @@ func (s *ServerNotRequiredSuite) TestLoadConfig(c *C) {
        c.Check(err, IsNil)
 
        c.Assert(dstConfig.APIHost, Equals, os.Getenv("ARVADOS_API_HOST"))
-       c.Assert(dstConfig.APIToken, Equals, arvadostest.DataManagerToken)
+       c.Assert(dstConfig.APIToken, Equals, arvadostest.SystemRootToken)
        c.Assert(dstConfig.APIHostInsecure, Equals, arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE")))
        c.Assert(dstConfig.ExternalClient, Equals, false)
 
@@ -401,7 +401,7 @@ func (s *ServerNotRequiredSuite) TestLoadConfig_ErrorLoadingSrcConfig(c *C) {
 func (s *ServerNotRequiredSuite) TestSetupKeepClient_NoBlobSignatureTTL(c *C) {
        var srcConfig apiConfig
        srcConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
-       srcConfig.APIToken = arvadostest.DataManagerToken
+       srcConfig.APIToken = arvadostest.SystemRootToken
        srcConfig.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
 
        _, ttl, err := setupKeepClient(srcConfig, srcKeepServicesJSON, false, 0, 0)
@@ -415,7 +415,7 @@ func setupConfigFile(c *C, name string) *os.File {
        c.Check(err, IsNil)
 
        fileContent := "ARVADOS_API_HOST=" + os.Getenv("ARVADOS_API_HOST") + "\n"
-       fileContent += "ARVADOS_API_TOKEN=" + arvadostest.DataManagerToken + "\n"
+       fileContent += "ARVADOS_API_TOKEN=" + arvadostest.SystemRootToken + "\n"
        fileContent += "ARVADOS_API_HOST_INSECURE=" + os.Getenv("ARVADOS_API_HOST_INSECURE") + "\n"
        fileContent += "ARVADOS_EXTERNAL_CLIENT=false\n"
        fileContent += "ARVADOS_BLOB_SIGNING_KEY=abcdefg"
diff --git a/tools/vocabulary-migrate/vocabulary-migrate.py b/tools/vocabulary-migrate/vocabulary-migrate.py
new file mode 100644 (file)
index 0000000..920344b
--- /dev/null
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+#
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: CC-BY-SA-3.0
+
+import argparse
+import copy
+import json
+import logging
+import os
+import sys
+
+import arvados
+import arvados.util
+
+logger = logging.getLogger('arvados.vocabulary_migrate')
+logger.setLevel(logging.INFO)
+
+class VocabularyError(Exception):
+    pass
+
+opts = argparse.ArgumentParser(add_help=False)
+opts.add_argument('--vocabulary-file', type=str, metavar='PATH', required=True,
+                  help="""
+Use vocabulary definition file at PATH for migration decisions.
+""")
+opts.add_argument('--dry-run', action='store_true', default=False,
+                  help="""
+Don't actually migrate properties, but only check if any collection/project
+should be migrated.
+""")
+opts.add_argument('--debug', action='store_true', default=False,
+                  help="""
+Sets logging level to DEBUG.
+""")
+arg_parser = argparse.ArgumentParser(
+    description='Migrate collections & projects properties to the new vocabulary format.',
+    parents=[opts])
+
+def parse_arguments(arguments):
+    args = arg_parser.parse_args(arguments)
+    if args.debug:
+        logger.setLevel(logging.DEBUG)
+    if not os.path.isfile(args.vocabulary_file):
+        arg_parser.error("{} doesn't exist or isn't a file.".format(args.vocabulary_file))
+    return args
+
+def _label_to_id_mappings(data, obj_name):
+    result = {}
+    for obj_id, obj_data in data.items():
+        for lbl in obj_data['labels']:
+            obj_lbl = lbl['label']
+            if obj_lbl not in result:
+                result[obj_lbl] = obj_id
+            else:
+                raise VocabularyError('{} label "{}" for {} ID "{}" already seen at {} ID "{}".'.format(obj_name, obj_lbl, obj_name, obj_id, obj_name, result[obj_lbl]))
+    return result
+
+def key_labels_to_ids(vocab):
+    return _label_to_id_mappings(vocab['tags'], 'key')
+
+def value_labels_to_ids(vocab, key_id):
+    if key_id in vocab['tags'] and 'values' in vocab['tags'][key_id]:
+        return _label_to_id_mappings(vocab['tags'][key_id]['values'], 'value')
+    return {}
+
+def migrate_properties(properties, key_map, vocab):
+    result = {}
+    for k, v in properties.items():
+        key = key_map.get(k, k)
+        value = value_labels_to_ids(vocab, key).get(v, v)
+        result[key] = value
+    return result
+
+def main(arguments=None):
+    args = parse_arguments(arguments)
+    vocab = None
+    with open(args.vocabulary_file, 'r') as f:
+        vocab = json.load(f)
+    arv = arvados.api('v1')
+    if 'tags' not in vocab or vocab['tags'] == {}:
+        logger.warning('Empty vocabulary file, exiting.')
+        return 1
+    if not arv.users().current().execute()['is_admin']:
+        logger.error('Admin privileges required.')
+        return 1
+    key_label_to_id_map = key_labels_to_ids(vocab)
+    migrated_counter = 0
+
+    for key_label in key_label_to_id_map:
+        logger.debug('Querying objects with property key "{}"'.format(key_label))
+        for resource in [arv.collections(), arv.groups()]:
+            objs = arvados.util.list_all(
+                resource.list,
+                order=['created_at'],
+                select=['uuid', 'properties'],
+                filters=[['properties', 'exists', key_label]]
+            )
+            for o in objs:
+                props = copy.copy(o['properties'])
+                migrated_props = migrate_properties(props, key_label_to_id_map, vocab)
+                if not args.dry_run:
+                    logger.debug('Migrating {}: {} -> {}'.format(o['uuid'], props, migrated_props))
+                    arv.collections().update(uuid=o['uuid'], body={
+                        'properties': migrated_props
+                    }).execute()
+                else:
+                    logger.info('Should migrate {}: {} -> {}'.format(o['uuid'], props, migrated_props))
+                migrated_counter += 1
+                if not args.dry_run and migrated_counter % 100 == 0:
+                    logger.info('Migrating {} objects...'.format(migrated_counter))
+
+    if args.dry_run and migrated_counter == 0:
+        logger.info('Nothing to do.')
+    elif not args.dry_run:
+        logger.info('Done, total objects migrated: {}.'.format(migrated_counter))
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main())
\ No newline at end of file