X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/51d7a5b2a23074a130aa6dd74cbaf5f335920769..2c0bf8219eb3ff5f978d147bff7ae6c6a73e8188:/lib/controller/federation/conn.go diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go index b2d158b9a5..75b9f2136b 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" @@ -22,29 +23,38 @@ import ( "git.arvados.org/arvados.git/sdk/go/arvados" "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) *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 { continue } - conn := rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id)) + conn := rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(cluster, local, id)) // Older versions of controller rely on the Via header // to detect loops. conn.SendHeader = http.Header{"Via": {"HTTP/1.1 arvados-controller"}} remotes[id] = conn } + if healthFuncs != nil { + hf := map[string]health.Func{"vocabulary": local.LastVocabularyError} + *healthFuncs = hf + } + return &Conn{ + bgCtx: bgCtx, cluster: cluster, local: local, remotes: remotes, @@ -55,7 +65,7 @@ func New(cluster *arvados.Cluster) *Conn { // tokens from an incoming request context, determines whether they // should (and can) be salted for the given remoteID, and returns the // resulting tokens. -func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider { +func saltedTokenProvider(cluster *arvados.Cluster, local backend, remoteID string) rpc.TokenProvider { return func(ctx context.Context) ([]string, error) { var tokens []string incoming, ok := auth.FromContext(ctx) @@ -63,12 +73,28 @@ func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider { return nil, errors.New("no token provided") } for _, token := range incoming.Tokens { + 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) switch err { case nil: tokens = append(tokens, salted) case auth.ErrSalted: tokens = append(tokens, token) + case auth.ErrTokenFormat: + // pass through unmodified (assume it's an OIDC access token) + tokens = append(tokens, token) case auth.ErrObsoleteToken: ctx := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{token}}) aca, err := local.APIClientAuthorizationCurrent(ctx, arvados.GetOptions{}) @@ -79,6 +105,14 @@ func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider { } else if err != nil { return nil, err } + if strings.HasPrefix(aca.UUID, remoteID) { + // We have it cached here, but + // the token belongs to the + // remote target itself, so + // pass it through unmodified. + tokens = append(tokens, token) + continue + } salted, err := auth.SaltToken(aca.TokenV2(), remoteID) if err != nil { return nil, err @@ -114,9 +148,8 @@ func (conn *Conn) chooseBackend(id string) backend { func (conn *Conn) localOrLoginCluster() backend { if conn.cluster.Login.LoginCluster != "" { return conn.chooseBackend(conn.cluster.Login.LoginCluster) - } else { - return conn.local } + return conn.local } // Call fn with the local backend; then, if fn returned 404, call fn @@ -146,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) { @@ -182,6 +224,14 @@ func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) { return json.RawMessage(buf.Bytes()), err } +func (conn *Conn) VocabularyGet(ctx context.Context) (arvados.Vocabulary, error) { + return conn.chooseBackend(conn.cluster.ClusterID).VocabularyGet(ctx) +} + +func (conn *Conn) DiscoveryDocument(ctx context.Context) (arvados.DiscoveryDocument, error) { + return conn.chooseBackend(conn.cluster.ClusterID).DiscoveryDocument(ctx) +} + func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) { if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID { // defer entire login procedure to designated cluster @@ -204,35 +254,75 @@ func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arva return arvados.LoginResponse{ RedirectLocation: target.String(), }, nil - } else { - return conn.local.Login(ctx, options) } + 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) { @@ -243,40 +333,55 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) c.ManifestText = rewriteManifest(c.ManifestText, options.UUID[:5]) } return c, err - } else { - // 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 { - remoteOpts := options - remoteOpts.ForwardedFor = conn.cluster.ClusterID + "-" + options.ForwardedFor - c, err := be.CollectionGet(ctx, remoteOpts) - if err != nil { - return 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 { + remoteOpts := options + remoteOpts.ForwardedFor = conn.cluster.ClusterID + "-" + options.ForwardedFor + c, err := be.CollectionGet(ctx, remoteOpts) + if err != nil { + return err + } + haveManifest := true + if options.Select != nil { + haveManifest = false + for _, s := range options.Select { + if s == "manifest_text" { + haveManifest = true + break + } } + } + if haveManifest { + pdh := arvados.PortableDataHash(c.ManifestText) // options.UUID is either hash+size or // hash+size+hints; only hash+size need to // match the computed PDH. - if pdh := arvados.PortableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") { + if 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 } - if remoteID != "" { - c.ManifestText = rewriteManifest(c.ManifestText, remoteID) - } - select { - case first <- c: - return nil - default: - // lost race, return value doesn't matter - return nil - } - }) - if err != nil { - return arvados.Collection{}, err } - return <-first, nil + if remoteID != "" { + c.ManifestText = rewriteManifest(c.ManifestText, remoteID) + } + select { + case first <- c: + return nil + default: + // lost race, return value doesn't matter + return nil + } + }) + if err != nil { + return arvados.Collection{}, err } + return <-first, nil } func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) { @@ -315,6 +420,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) } @@ -331,6 +440,167 @@ 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.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) +} + +func (conn *Conn) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) { + be := conn.chooseBackend(options.ClusterID) + if be == conn.local { + return be.ContainerRequestCreate(ctx, options) + } + if _, ok := options.Attrs["runtime_token"]; !ok { + // If runtime_token is not set, create a new token + aca, err := conn.local.APIClientAuthorizationCurrent(ctx, arvados.GetOptions{}) + if err != nil { + // This should probably be StatusUnauthorized + // (need to update test in + // lib/controller/federation_test.go): + // When RoR is out of the picture this should be: + // return arvados.ContainerRequest{}, httpErrorf(http.StatusUnauthorized, "%w", err) + return arvados.ContainerRequest{}, httpErrorf(http.StatusForbidden, "%s", "invalid API token") + } + user, err := conn.local.UserGetCurrent(ctx, arvados.GetOptions{}) + if err != nil { + return arvados.ContainerRequest{}, err + } + if len(aca.Scopes) == 0 || aca.Scopes[0] != "all" { + return arvados.ContainerRequest{}, httpErrorf(http.StatusForbidden, "token scope is not [all]") + } + if strings.HasPrefix(aca.UUID, conn.cluster.ClusterID) { + // Local user, submitting to a remote cluster. + // Create a new time-limited token. + local, ok := conn.local.(*localdb.Conn) + if !ok { + return arvados.ContainerRequest{}, httpErrorf(http.StatusInternalServerError, "bug: local backend is a %T, not a *localdb.Conn", conn.local) + } + aca, err = local.CreateAPIClientAuthorization(ctx, conn.cluster.SystemRootToken, rpc.UserSessionAuthInfo{UserUUID: user.UUID, + ExpiresAt: time.Now().UTC().Add(conn.cluster.Collections.BlobSigningTTL.Duration())}) + if err != nil { + return arvados.ContainerRequest{}, err + } + options.Attrs["runtime_token"] = aca.TokenV2() + } else { + // Remote user. Container request will use the + // current token, minus the trailing portion + // (optional container uuid). + options.Attrs["runtime_token"] = aca.TokenV2() + } + } + return be.ContainerRequestCreate(ctx, options) +} + +func (conn *Conn) ContainerRequestUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.ContainerRequest, error) { + return conn.chooseBackend(options.UUID).ContainerRequestUpdate(ctx, options) +} + +func (conn *Conn) ContainerRequestGet(ctx context.Context, options arvados.GetOptions) (arvados.ContainerRequest, error) { + return conn.chooseBackend(options.UUID).ContainerRequestGet(ctx, options) +} + +func (conn *Conn) ContainerRequestDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.ContainerRequest, error) { + 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) +} + +func (conn *Conn) GroupUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Group, error) { + return conn.chooseBackend(options.UUID).GroupUpdate(ctx, options) +} + +func (conn *Conn) GroupGet(ctx context.Context, options arvados.GetOptions) (arvados.Group, error) { + return conn.chooseBackend(options.UUID).GroupGet(ctx, options) +} + +func (conn *Conn) GroupList(ctx context.Context, options arvados.ListOptions) (arvados.GroupList, error) { + return conn.generated_GroupList(ctx, options) +} + +var userUuidRe = regexp.MustCompile(`^[0-9a-z]{5}-tpzed-[0-9a-z]{15}$`) + +func (conn *Conn) GroupContents(ctx context.Context, options arvados.GroupContentsOptions) (arvados.ObjectList, error) { + if options.ClusterID != "" { + // explicitly selected cluster + return conn.chooseBackend(options.ClusterID).GroupContents(ctx, options) + } else if userUuidRe.MatchString(options.UUID) { + // user, get the things they own on the local cluster + return conn.local.GroupContents(ctx, options) + } else { + // a group, potentially want to make federated request + return conn.chooseBackend(options.UUID).GroupContents(ctx, options) + } +} + +func (conn *Conn) GroupShared(ctx context.Context, options arvados.ListOptions) (arvados.GroupList, error) { + return conn.chooseBackend(options.ClusterID).GroupShared(ctx, options) +} + +func (conn *Conn) GroupDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Group, error) { + return conn.chooseBackend(options.UUID).GroupDelete(ctx, options) +} + +func (conn *Conn) GroupTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Group, error) { + return conn.chooseBackend(options.UUID).GroupTrash(ctx, options) +} + +func (conn *Conn) GroupUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Group, error) { + return conn.chooseBackend(options.UUID).GroupUntrash(ctx, options) +} + +func (conn *Conn) LinkCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Link, error) { + return conn.chooseBackend(options.ClusterID).LinkCreate(ctx, options) +} + +func (conn *Conn) LinkUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Link, error) { + return conn.chooseBackend(options.UUID).LinkUpdate(ctx, options) +} + +func (conn *Conn) LinkGet(ctx context.Context, options arvados.GetOptions) (arvados.Link, error) { + return conn.chooseBackend(options.UUID).LinkGet(ctx, options) +} + +func (conn *Conn) LinkList(ctx context.Context, options arvados.ListOptions) (arvados.LinkList, error) { + return conn.generated_LinkList(ctx, options) +} + +func (conn *Conn) LinkDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Link, error) { + 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) } @@ -351,31 +621,39 @@ func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOpti return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options) } +func (conn *Conn) SysTrashSweep(ctx context.Context, options struct{}) (struct{}, error) { + return conn.local.SysTrashSweep(ctx, options) +} + var userAttrsCachedFromLoginCluster = map[string]bool{ "created_at": true, "email": true, "first_name": true, "is_active": true, "is_admin": true, + "is_invited": true, "last_name": true, "modified_at": true, "prefs": true, "username": true, + "kind": 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, + "can_write": false, + "can_manage": false, } func (conn *Conn) batchUpdateUsers(ctx context.Context, options arvados.ListOptions, - items []arvados.User) (err error) { + items []arvados.User, + includeAdminAndInvited bool) (err error) { id := conn.cluster.Login.LoginCluster logger := ctxlog.FromContext(ctx) @@ -422,6 +700,11 @@ func (conn *Conn) batchUpdateUsers(ctx context.Context, } } } + if !includeAdminAndInvited { + // make sure we don't send these fields. + delete(updates, "is_admin") + delete(updates, "is_invited") + } batchOpts.Updates[user.UUID] = updates } if len(batchOpts.Updates) > 0 { @@ -434,20 +717,53 @@ func (conn *Conn) batchUpdateUsers(ctx context.Context, return nil } +func (conn *Conn) includeAdminAndInvitedInBatchUpdate(ctx context.Context, be backend, updateUserUUID string) (bool, error) { + // API versions prior to 20231117 would only include the + // is_invited and is_admin fields if the current user is an + // admin, or is requesting their own user record. If those + // fields aren't actually valid then we don't want to + // send them in the batch update. + dd, err := be.DiscoveryDocument(ctx) + if err != nil { + // couldn't get discovery document + return false, err + } + if dd.Revision >= "20231117" { + // newer version, fields are valid. + return true, nil + } + selfuser, err := be.UserGetCurrent(ctx, arvados.GetOptions{}) + if err != nil { + // couldn't get our user record + return false, err + } + if selfuser.IsAdmin || selfuser.UUID == updateUserUUID { + // we are an admin, or the current user is the same as + // the user that we are updating. + return true, nil + } + // Better safe than sorry. + return false, 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) + be := conn.chooseBackend(id) + resp, err := be.UserList(ctx, options) if err != nil { return resp, err } - err = conn.batchUpdateUsers(ctx, options, resp.Items) + includeAdminAndInvited, err := conn.includeAdminAndInvitedInBatchUpdate(ctx, be, "") + if err != nil { + return arvados.UserList{}, err + } + err = conn.batchUpdateUsers(ctx, options, resp.Items, includeAdminAndInvited) if err != nil { return arvados.UserList{}, err } return resp, nil - } else { - return conn.generated_UserList(ctx, options) } + return conn.generated_UserList(ctx, options) } func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) { @@ -458,11 +774,23 @@ func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) 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.local.UserUpdateUUID(ctx, options) + be := conn.chooseBackend(options.UUID) + resp, err := be.UserUpdate(ctx, options) + if err != nil { + return resp, err + } + if !strings.HasPrefix(options.UUID, conn.cluster.ClusterID) { + includeAdminAndInvited, err := conn.includeAdminAndInvitedInBatchUpdate(ctx, be, options.UUID) + if err != nil { + return arvados.User{}, err + } + // Copy the updated user record to the local cluster + err = conn.batchUpdateUsers(ctx, arvados.ListOptions{}, []arvados.User{resp}, includeAdminAndInvited) + if err != nil { + return arvados.User{}, err + } + } + return resp, err } func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) { @@ -474,40 +802,29 @@ func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivate } func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) { - var setupVM string - var setupRepo string - if conn.cluster.Login.LoginCluster != "" { - if options.VMUUID != "" && options.VMUUID[0:5] != options.UUID[0:5] { - // When LoginCluster is in effect, and we're - // setting up a remote user, and we want to - // give that user access to a local VM, then - // we need to set up the user on the remote - // LoginCluster first, followed by calling - // setup on the local instance to give access - // to the VM. - setupVM = options.VMUUID - options.VMUUID = "" - } - if options.RepoName != "" { - // Similarly, if we want to create a git repo, - // it should be created on the local cluster, - // not the remote one. - setupRepo = options.RepoName - options.RepoName = "" - } - } - - ret, err := conn.localOrLoginCluster().UserSetup(ctx, options) - if err != nil { - return ret, err + upstream := conn.localOrLoginCluster() + if upstream != conn.local { + // When LoginCluster is in effect, and we're setting + // up a remote user, and we want to give that user + // access to a local VM, we can't include the VM in + // the setup call, because the remote cluster won't + // recognize it. + + // Similarly, if we want to create a git repo, + // it should be created on the local cluster, + // not the remote one. + + upstreamOptions := options + upstreamOptions.VMUUID = "" + upstreamOptions.RepoName = "" + + ret, err := upstream.UserSetup(ctx, upstreamOptions) + if err != nil { + return ret, err + } } - if setupVM != "" || setupRepo != "" { - options.VMUUID = setupVM - options.RepoName = setupRepo - ret, err = conn.local.UserSetup(ctx, options) - } - return ret, err + return conn.local.UserSetup(ctx, options) } func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) { @@ -515,11 +832,29 @@ func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) ( } func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) { - return conn.chooseBackend(options.UUID).UserGet(ctx, options) + be := conn.chooseBackend(options.UUID) + resp, err := be.UserGet(ctx, options) + if err != nil { + return resp, err + } + if options.UUID != resp.UUID { + return arvados.User{}, httpErrorf(http.StatusBadGateway, "Had requested %v but response was for %v", options.UUID, resp.UUID) + } + if options.UUID[:5] != conn.cluster.ClusterID { + includeAdminAndInvited, err := conn.includeAdminAndInvitedInBatchUpdate(ctx, be, options.UUID) + if err != nil { + return arvados.User{}, err + } + err = conn.batchUpdateUsers(ctx, arvados.ListOptions{Select: options.Select}, []arvados.User{resp}, includeAdminAndInvited) + if err != nil { + return arvados.User{}, err + } + } + return resp, nil } func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) { - return conn.chooseBackend(options.UUID).UserGetCurrent(ctx, options) + return conn.local.UserGetCurrent(ctx, options) } func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) { @@ -542,6 +877,39 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options) } +func (conn *Conn) APIClientAuthorizationCreate(ctx context.Context, options arvados.CreateOptions) (arvados.APIClientAuthorization, error) { + if conn.cluster.Login.LoginCluster != "" { + return conn.chooseBackend(conn.cluster.Login.LoginCluster).APIClientAuthorizationCreate(ctx, options) + } + ownerUUID, ok := options.Attrs["owner_uuid"].(string) + if ok && ownerUUID != "" { + return conn.chooseBackend(ownerUUID).APIClientAuthorizationCreate(ctx, options) + } + return conn.local.APIClientAuthorizationCreate(ctx, options) +} + +func (conn *Conn) APIClientAuthorizationUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.APIClientAuthorization, error) { + if options.BypassFederation { + return conn.local.APIClientAuthorizationUpdate(ctx, options) + } + return conn.chooseBackend(options.UUID).APIClientAuthorizationUpdate(ctx, options) +} + +func (conn *Conn) APIClientAuthorizationDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.APIClientAuthorization, error) { + return conn.chooseBackend(options.UUID).APIClientAuthorizationDelete(ctx, options) +} + +func (conn *Conn) APIClientAuthorizationList(ctx context.Context, options arvados.ListOptions) (arvados.APIClientAuthorizationList, error) { + if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID && !options.BypassFederation { + return conn.chooseBackend(conn.cluster.Login.LoginCluster).APIClientAuthorizationList(ctx, options) + } + return conn.generated_APIClientAuthorizationList(ctx, options) +} + +func (conn *Conn) APIClientAuthorizationGet(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) { + return conn.chooseBackend(options.UUID).APIClientAuthorizationGet(ctx, options) +} + type backend interface { arvados.API BaseURL() url.URL @@ -555,7 +923,6 @@ func (notFoundError) Error() string { return "not found" } func errStatus(err error) int { if httpErr, ok := err.(interface{ HTTPStatus() int }); ok { return httpErr.HTTPStatus() - } else { - return http.StatusInternalServerError } + return http.StatusInternalServerError }