X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/14bb7aa24ed7a06ce94e59f64ab32bdb44641168..2ba6cc7a5e4bfd05cd51e8ab22be2a99a883349d:/lib/controller/federation/conn.go diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go index 2aebc0e970..418b6811be 100644 --- a/lib/controller/federation/conn.go +++ b/lib/controller/federation/conn.go @@ -7,7 +7,6 @@ package federation import ( "bytes" "context" - "crypto/md5" "encoding/json" "errors" "fmt" @@ -35,7 +34,7 @@ func New(cluster *arvados.Cluster) *Conn { local := localdb.NewConn(cluster) remotes := map[string]backend{} for id, remote := range cluster.RemoteClusters { - if !remote.Proxy { + if !remote.Proxy || id == cluster.ClusterID { continue } conn := rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id)) @@ -120,8 +119,13 @@ func (conn *Conn) chooseBackend(id string) backend { // or "" for the local backend. // // A non-nil error means all backends failed. -func (conn *Conn) tryLocalThenRemotes(ctx context.Context, fn func(context.Context, string, backend) error) error { - if err := fn(ctx, "", conn.local); err == nil || errStatus(err) != http.StatusNotFound { +func (conn *Conn) tryLocalThenRemotes(ctx context.Context, forwardedFor string, fn func(context.Context, string, backend) error) error { + if err := fn(ctx, "", conn.local); err == nil || errStatus(err) != http.StatusNotFound || forwardedFor != "" { + // Note: forwardedFor != "" means this request came + // from a remote cluster, so we don't take a second + // hop. This avoids cycles, redundant calls to a + // mutually reachable remote, and use of double-salted + // tokens. return err } @@ -164,26 +168,6 @@ func rewriteManifest(mt, remoteID string) string { }) } -// this could be in sdk/go/arvados -func portableDataHash(mt string) string { - h := md5.New() - blkRe := regexp.MustCompile(`^ [0-9a-f]{32}\+\d+`) - size := 0 - _ = regexp.MustCompile(` ?[^ ]*`).ReplaceAllFunc([]byte(mt), func(tok []byte) []byte { - if m := blkRe.Find(tok); m != nil { - // write hash+size, ignore remaining block hints - tok = m - } - n, err := h.Write(tok) - if err != nil { - panic(err) - } - size += n - return nil - }) - return fmt.Sprintf("%x+%d", h.Sum(nil), size) -} - func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) { var buf bytes.Buffer err := config.ExportJSON(&buf, conn.cluster) @@ -217,9 +201,33 @@ func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arva } } +func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) { + // If the logout request comes with an API token from a known + // remote cluster, redirect to that cluster's logout handler + // so it has an opportunity to clear sessions, expire tokens, + // etc. Otherwise use the local endpoint. + reqauth, ok := auth.FromContext(ctx) + if !ok || len(reqauth.Tokens) == 0 || len(reqauth.Tokens[0]) < 8 || !strings.HasPrefix(reqauth.Tokens[0], "v2/") { + return conn.local.Logout(ctx, options) + } + id := reqauth.Tokens[0][3:8] + if id == conn.cluster.ClusterID { + return conn.local.Logout(ctx, options) + } + remote, ok := conn.remotes[id] + if !ok { + return conn.local.Logout(ctx, options) + } + baseURL := remote.BaseURL() + target, err := baseURL.Parse(arvados.EndpointLogout.Path) + if err != nil { + return arvados.LogoutResponse{}, fmt.Errorf("internal error getting redirect target: %s", err) + } + target.RawQuery = url.Values{"return_to": {options.ReturnTo}}.Encode() + return arvados.LogoutResponse{RedirectLocation: target.String()}, nil +} + func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) { - downstream := options.ForwardedFor - options.ForwardedFor = conn.cluster.ClusterID + "-" + downstream if len(options.UUID) == 27 { // UUID is really a UUID c, err := conn.chooseBackend(options.UUID).CollectionGet(ctx, options) @@ -230,24 +238,17 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) } else { // UUID is a PDH first := make(chan arvados.Collection, 1) - err := conn.tryLocalThenRemotes(ctx, func(ctx context.Context, remoteID string, be backend) error { - if remoteID != "" && downstream != "" { - // If remoteID isn't in downstream, we - // might find the collection by taking - // another hop, but we don't bother: - // token salting and blob signature - // rewriting don't work over multiple - // hops. - return notFoundError{} - } - c, err := be.CollectionGet(ctx, options) + err := conn.tryLocalThenRemotes(ctx, options.ForwardedFor, func(ctx context.Context, remoteID string, be backend) error { + remoteOpts := options + remoteOpts.ForwardedFor = conn.cluster.ClusterID + "-" + options.ForwardedFor + c, err := be.CollectionGet(ctx, remoteOpts) if err != nil { return err } // options.UUID is either hash+size or // hash+size+hints; only hash+size need to // match the computed PDH. - if pdh := portableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") { + if pdh := arvados.PortableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") { err = httpErrorf(http.StatusBadGateway, "bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID) ctxlog.FromContext(ctx).Warn(err) return err @@ -343,85 +344,97 @@ func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOpti } 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, - - "etag": false, - "full_name": false, - "identity_url": false, - "is_invited": false, - "owner_uuid": false, - "uuid": false, - "writable_by": false, -} - -func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) { + "created_at": true, + "email": true, + "first_name": true, + "is_active": true, + "is_admin": true, + "last_name": true, + "modified_at": true, + "prefs": true, + "username": true, + + "etag": false, + "full_name": false, + "identity_url": false, + "is_invited": false, + "modified_by_client_uuid": false, + "modified_by_user_uuid": false, + "owner_uuid": false, + "uuid": false, + "writable_by": false, +} + +func (conn *Conn) batchUpdateUsers(ctx context.Context, + options arvados.ListOptions, + items []arvados.User) (err error) { + + id := conn.cluster.Login.LoginCluster 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 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() } - 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 - } + var allFields map[string]interface{} + buf, err := json.Marshal(user) + if err != nil { + return fmt.Errorf("error encoding user record from remote response: %s", err) + } + err = json.Unmarshal(buf, &allFields) + if err != nil { + return 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) - } + } + } 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) - } + 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 fmt.Errorf("error updating local user records: %s", err) + } + } + return nil +} + +func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) { + if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID && !options.BypassFederation { + resp, err := conn.chooseBackend(id).UserList(ctx, options) + if err != nil { + return resp, err + } + err = conn.batchUpdateUsers(ctx, options, resp.Items) + if err != nil { + return arvados.UserList{}, err } return resp, nil } else { @@ -434,15 +447,18 @@ func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) } func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) { + if options.BypassFederation { + return conn.local.UserUpdate(ctx, options) + } 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) + return conn.local.UserUpdateUUID(ctx, options) } func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) { - return conn.chooseBackend(options.OldUserUUID).UserMerge(ctx, options) + return conn.local.UserMerge(ctx, options) } func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) { @@ -477,6 +493,10 @@ func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatch return conn.local.UserBatchUpdate(ctx, options) } +func (conn *Conn) UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) { + return conn.local.UserAuthenticate(ctx, options) +} + func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) { return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options) }