Merge branch '21021-controller-logout'
[arvados.git] / lib / controller / federation / conn.go
index 3a232d29b89e7dcbafb79c705762da3f9ed11d45..c65e1429241a6030c9bdb063cc6d4037d6116717 100644 (file)
@@ -14,6 +14,7 @@ import (
        "net/url"
        "regexp"
        "strings"
+       "sync"
        "time"
 
        "git.arvados.org/arvados.git/lib/config"
@@ -178,20 +179,29 @@ func (conn *Conn) tryLocalThenRemotes(ctx context.Context, forwardedFor string,
                        errchan <- fn(ctx, remoteID, be)
                }()
        }
-       all404 := true
+       returncode := http.StatusNotFound
        var errs []error
        for i := 0; i < cap(errchan); i++ {
                err := <-errchan
                if err == nil {
                        return nil
                }
-               all404 = all404 && errStatus(err) == http.StatusNotFound
                errs = append(errs, err)
+               if code := errStatus(err); code >= 500 || code == http.StatusTooManyRequests {
+                       // If any of the remotes have a retryable
+                       // error (and none succeed) we'll return 502.
+                       returncode = http.StatusBadGateway
+               } else if code != http.StatusNotFound && returncode != http.StatusBadGateway {
+                       // If some of the remotes have non-retryable
+                       // non-404 errors (and none succeed or have
+                       // retryable errors) we'll return 422.
+                       returncode = http.StatusUnprocessableEntity
+               }
        }
-       if all404 {
+       if returncode == http.StatusNotFound {
                return notFoundError{}
        }
-       return httpErrorf(http.StatusBadGateway, "errors: %v", errs)
+       return httpErrorf(returncode, "errors: %v", errs)
 }
 
 func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
@@ -244,30 +254,71 @@ func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arva
        return conn.local.Login(ctx, options)
 }
 
+var v2TokenRegexp = regexp.MustCompile(`^v2/[a-z0-9]{5}-gj3su-[a-z0-9]{15}/`)
+
 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)
+       // If the token was issued by another cluster, we want to issue a logout
+       // request to the issuing instance to invalidate the token federation-wide.
+       // If this federation has a login cluster, that's always considered the
+       // issuing cluster.
+       // Otherwise, if this is a v2 token, use the UUID to find the issuing
+       // cluster.
+       // Note that remoteBE may still be conn.local even *after* one of these
+       // conditions is true.
+       var remoteBE backend = conn.local
+       if conn.cluster.Login.LoginCluster != "" {
+               remoteBE = conn.chooseBackend(conn.cluster.Login.LoginCluster)
+       } else {
+               reqauth, ok := auth.FromContext(ctx)
+               if ok && len(reqauth.Tokens) > 0 && v2TokenRegexp.MatchString(reqauth.Tokens[0]) {
+                       remoteBE = conn.chooseBackend(reqauth.Tokens[0][3:8])
+               }
        }
-       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)
+
+       // We always want to invalidate the token locally. Start that process.
+       var localResponse arvados.LogoutResponse
+       var localErr error
+       wg := sync.WaitGroup{}
+       wg.Add(1)
+       go func() {
+               localResponse, localErr = conn.local.Logout(ctx, options)
+               wg.Done()
+       }()
+
+       // If the token was issued by another cluster, log out there too.
+       if remoteBE != conn.local {
+               response, err := remoteBE.Logout(ctx, options)
+               // If the issuing cluster returns a redirect or error, that's more
+               // important to return to the user than anything that happens locally.
+               if response.RedirectLocation != "" || err != nil {
+                       return response, err
+               }
        }
-       target.RawQuery = url.Values{"return_to": {options.ReturnTo}}.Encode()
-       return arvados.LogoutResponse{RedirectLocation: target.String()}, nil
+
+       // Either the local cluster is the issuing cluster, or the issuing cluster's
+       // response was uninteresting.
+       wg.Wait()
+       return localResponse, localErr
+}
+
+func (conn *Conn) AuthorizedKeyCreate(ctx context.Context, options arvados.CreateOptions) (arvados.AuthorizedKey, error) {
+       return conn.chooseBackend(options.ClusterID).AuthorizedKeyCreate(ctx, options)
+}
+
+func (conn *Conn) AuthorizedKeyUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.AuthorizedKey, error) {
+       return conn.chooseBackend(options.UUID).AuthorizedKeyUpdate(ctx, options)
+}
+
+func (conn *Conn) AuthorizedKeyGet(ctx context.Context, options arvados.GetOptions) (arvados.AuthorizedKey, error) {
+       return conn.chooseBackend(options.UUID).AuthorizedKeyGet(ctx, options)
+}
+
+func (conn *Conn) AuthorizedKeyList(ctx context.Context, options arvados.ListOptions) (arvados.AuthorizedKeyList, error) {
+       return conn.generated_AuthorizedKeyList(ctx, options)
+}
+
+func (conn *Conn) AuthorizedKeyDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.AuthorizedKey, error) {
+       return conn.chooseBackend(options.UUID).AuthorizedKeyDelete(ctx, options)
 }
 
 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
@@ -455,6 +506,10 @@ func (conn *Conn) ContainerRequestDelete(ctx context.Context, options arvados.De
        return conn.chooseBackend(options.UUID).ContainerRequestDelete(ctx, options)
 }
 
+func (conn *Conn) ContainerRequestLog(ctx context.Context, options arvados.ContainerLogOptions) (http.Handler, error) {
+       return conn.chooseBackend(options.UUID).ContainerRequestLog(ctx, options)
+}
+
 func (conn *Conn) GroupCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Group, error) {
        return conn.chooseBackend(options.ClusterID).GroupCreate(ctx, options)
 }