Merge branch '15795-sys-root-token' refs #15795
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Tue, 26 Nov 2019 20:06:42 +0000 (15:06 -0500)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Tue, 26 Nov 2019 20:06:42 +0000 (15:06 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

52 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/admin/management-token.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/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/keep-rsync/keep-rsync_test.go

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
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 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..4d18395b6a87b7cc7aa421f78e118c18551453ca 100644 (file)
@@ -13,7 +13,7 @@ import (
        "strconv"
        "strings"
 
-       "github.com/julienschmidt/httprouter"
+       "github.com/gorilla/mux"
 )
 
 // Parse req as an Arvados V1 API request and return the request
@@ -109,9 +109,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 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 2da316a91bc547ce914863f54c9d5e7e78cb641e..651eacf6264fe36b860476cd85b6025798a72659 100644 (file)
@@ -236,20 +236,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
 
@@ -258,6 +254,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 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"