X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/3c18a9d8e2ebb6483413085ce6e0eb2fa382f06f..d3cd6f7557ded90258d86a25aa755328f702e488:/lib/controller/federation/conn.go diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go index d3819f6262..c65e142924 100644 --- a/lib/controller/federation/conn.go +++ b/lib/controller/federation/conn.go @@ -14,6 +14,7 @@ import ( "net/url" "regexp" "strings" + "sync" "time" "git.arvados.org/arvados.git/lib/config" @@ -23,16 +24,18 @@ import ( "git.arvados.org/arvados.git/sdk/go/auth" "git.arvados.org/arvados.git/sdk/go/ctxlog" "git.arvados.org/arvados.git/sdk/go/health" + "github.com/jmoiron/sqlx" ) type Conn struct { + bgCtx context.Context cluster *arvados.Cluster local backend remotes map[string]backend } -func New(cluster *arvados.Cluster, healthFuncs *map[string]health.Func) *Conn { - local := localdb.NewConn(cluster) +func New(bgCtx context.Context, cluster *arvados.Cluster, healthFuncs *map[string]health.Func, getdb func(context.Context) (*sqlx.DB, error)) *Conn { + local := localdb.NewConn(bgCtx, cluster, getdb) remotes := map[string]backend{} for id, remote := range cluster.RemoteClusters { if !remote.Proxy || id == cluster.ClusterID { @@ -51,6 +54,7 @@ func New(cluster *arvados.Cluster, healthFuncs *map[string]health.Func) *Conn { } return &Conn{ + bgCtx: bgCtx, cluster: cluster, local: local, remotes: remotes, @@ -69,14 +73,17 @@ func saltedTokenProvider(cluster *arvados.Cluster, local backend, remoteID strin return nil, errors.New("no token provided") } for _, token := range incoming.Tokens { - if strings.HasPrefix(token, "v2/"+cluster.ClusterID+"-") && remoteID == cluster.Login.LoginCluster { - // If we did this, the login cluster - // would call back to us and then - // reject our response because the - // user UUID prefix (i.e., the - // LoginCluster prefix) won't match - // the token UUID prefix (i.e., our - // prefix). + if strings.HasPrefix(token, "v2/"+cluster.ClusterID+"-") && + !strings.HasPrefix(token, "v2/"+cluster.ClusterID+"-gj3su-anonymouspublic/") && + remoteID == cluster.Login.LoginCluster { + // If we did this, the login cluster would call back to us and then + // reject our response because the user UUID prefix (i.e., the + // LoginCluster prefix) won't match the token UUID prefix (i.e., our + // prefix). The anonymous token is OK to forward, because (unlike other + // local tokens for real users) the validation callback will return the + // locally issued anonymous user ID instead of a login-cluster user ID. + // That anonymous user ID gets mapped to the local anonymous user + // automatically on the login cluster. return nil, httpErrorf(http.StatusUnauthorized, "cannot use a locally issued token to forward a request to our login cluster (%s)", remoteID) } salted, err := auth.SaltToken(token, remoteID) @@ -172,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) { @@ -238,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) { @@ -273,6 +330,9 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) } return c, err } + if len(options.UUID) < 34 || options.UUID[32] != '+' { + return arvados.Collection{}, httpErrorf(http.StatusNotFound, "invalid UUID or PDH %q", options.UUID) + } // UUID is a PDH first := make(chan arvados.Collection, 1) err := conn.tryLocalThenRemotes(ctx, options.ForwardedFor, func(ctx context.Context, remoteID string, be backend) error { @@ -356,6 +416,10 @@ func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOpt return conn.chooseBackend(options.UUID).ContainerUpdate(ctx, options) } +func (conn *Conn) ContainerPriorityUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) { + return conn.chooseBackend(options.UUID).ContainerPriorityUpdate(ctx, options) +} + func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) { return conn.chooseBackend(options.UUID).ContainerGet(ctx, options) } @@ -372,10 +436,14 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options) } -func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (arvados.ContainerSSHConnection, error) { +func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (arvados.ConnectionResponse, error) { return conn.chooseBackend(options.UUID).ContainerSSH(ctx, options) } +func (conn *Conn) ContainerGatewayTunnel(ctx context.Context, options arvados.ContainerGatewayTunnelOptions) (arvados.ConnectionResponse, error) { + return conn.chooseBackend(options.UUID).ContainerGatewayTunnel(ctx, options) +} + func (conn *Conn) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) { return conn.generated_ContainerRequestList(ctx, options) } @@ -438,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) } @@ -505,6 +577,26 @@ func (conn *Conn) LinkDelete(ctx context.Context, options arvados.DeleteOptions) return conn.chooseBackend(options.UUID).LinkDelete(ctx, options) } +func (conn *Conn) LogCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Log, error) { + return conn.chooseBackend(options.ClusterID).LogCreate(ctx, options) +} + +func (conn *Conn) LogUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Log, error) { + return conn.chooseBackend(options.UUID).LogUpdate(ctx, options) +} + +func (conn *Conn) LogGet(ctx context.Context, options arvados.GetOptions) (arvados.Log, error) { + return conn.chooseBackend(options.UUID).LogGet(ctx, options) +} + +func (conn *Conn) LogList(ctx context.Context, options arvados.ListOptions) (arvados.LogList, error) { + return conn.generated_LogList(ctx, options) +} + +func (conn *Conn) LogDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Log, error) { + return conn.chooseBackend(options.UUID).LogDelete(ctx, options) +} + func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) { return conn.generated_SpecimenList(ctx, options) } @@ -550,6 +642,8 @@ var userAttrsCachedFromLoginCluster = map[string]bool{ "owner_uuid": false, "uuid": false, "writable_by": false, + "can_write": false, + "can_manage": false, } func (conn *Conn) batchUpdateUsers(ctx context.Context,