// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 package federation import ( "context" "encoding/json" "errors" "math" "net/url" "os" "strings" "git.arvados.org/arvados.git/lib/controller/rpc" "git.arvados.org/arvados.git/lib/ctrlctx" "git.arvados.org/arvados.git/sdk/go/arvados" "git.arvados.org/arvados.git/sdk/go/arvadostest" "git.arvados.org/arvados.git/sdk/go/auth" "git.arvados.org/arvados.git/sdk/go/ctxlog" 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.ctx, s.cluster, nil, (&ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL}).GetDB) 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: 0, Limit: math.MaxInt64, 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 err != nil { c.Logf("... UserList failed %q", err) } 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": true, } 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) } } } } func (s *UserSuite) TestLoginClusterUserGet(c *check.C) { s.cluster.ClusterID = "local" s.cluster.Login.LoginCluster = "zzzzz" s.fed = New(s.ctx, s.cluster, nil, (&ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL}).GetDB) s.addDirectRemote(c, "zzzzz", rpc.NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")}, true, rpc.PassthroughTokenProvider)) opts := arvados.GetOptions{UUID: "zzzzz-tpzed-xurymjxw79nv3jz", Select: []string{"uuid", "email"}} stub := &arvadostest.APIStub{Error: errors.New("local cluster failure")} s.fed.local = stub s.fed.UserGet(s.ctx, opts) 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": true, } 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)) } } } func (s *UserSuite) TestLoginClusterUserListBypassFederation(c *check.C) { s.cluster.ClusterID = "local" s.cluster.Login.LoginCluster = "zzzzz" s.fed = New(s.ctx, s.cluster, nil, (&ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL}).GetDB) s.addDirectRemote(c, "zzzzz", rpc.NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")}, true, rpc.PassthroughTokenProvider)) spy := arvadostest.NewProxy(c, s.cluster.Services.RailsAPI) s.fed.local = rpc.NewConn(s.cluster.ClusterID, spy.URL, true, rpc.PassthroughTokenProvider) _, err := s.fed.UserList(s.ctx, arvados.ListOptions{Offset: 0, Limit: math.MaxInt64, Select: nil, BypassFederation: true}) // this will fail because it is not using a root token c.Check(err.(*arvados.TransactionError).StatusCode, check.Equals, 403) // Now use SystemRootToken ctx := context.Background() ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c)) ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.SystemRootToken}}) // Assert that it did not try to batch update users. _, err = s.fed.UserList(ctx, arvados.ListOptions{Offset: 0, Limit: math.MaxInt64, Select: nil, BypassFederation: true}) for _, d := range spy.RequestDumps { d := string(d) if strings.Contains(d, "PATCH /arvados/v1/users/batch") { c.Fail() } } c.Check(err, check.IsNil) } // 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)) } }