Merge branch '15720-fed-user-list'
authorTom Clegg <tclegg@veritasgenetics.com>
Mon, 25 Nov 2019 21:58:52 +0000 (16:58 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Mon, 25 Nov 2019 21:58:52 +0000 (16:58 -0500)
closes #15720

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

29 files changed:
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/app/controllers/arvados/v1/users_controller.rb
services/api/config/routes.rb
services/api/test/functional/arvados/v1/users_controller_test.rb
services/keep-balance/integration_test.go
services/keepstore/handler_test.go
services/keepstore/mounts_test.go
services/keepstore/proxy_remote_test.go
tools/keep-rsync/keep-rsync_test.go

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 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 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 d5db1039645cbadffc45d93317cc87664b889b38..74f017a678f76643884ff09261f17188483328eb 100644 (file)
@@ -1043,6 +1043,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 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 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 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"