1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
24 "git.arvados.org/arvados.git/sdk/go/arvados"
25 "git.arvados.org/arvados.git/sdk/go/auth"
26 "git.arvados.org/arvados.git/sdk/go/ctxlog"
27 "git.arvados.org/arvados.git/sdk/go/httpserver"
30 const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
32 type TokenProvider func(context.Context) ([]string, error)
34 func PassthroughTokenProvider(ctx context.Context) ([]string, error) {
35 incoming, ok := auth.FromContext(ctx)
37 return nil, errors.New("no token provided")
39 return incoming.Tokens, nil
43 SendHeader http.Header
45 httpClient http.Client
47 tokenProvider TokenProvider
50 func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *Conn {
51 transport := http.DefaultTransport
53 // It's not safe to copy *http.DefaultTransport
54 // because it has a mutex (which might be locked)
55 // protecting a private map (which might not be nil).
56 // So we build our own, using the Go 1.12 default
57 // values, ignoring any changes the application has
58 // made to http.DefaultTransport.
59 transport = &http.Transport{
60 DialContext: (&net.Dialer{
61 Timeout: 30 * time.Second,
62 KeepAlive: 30 * time.Second,
66 IdleConnTimeout: 90 * time.Second,
67 TLSHandshakeTimeout: 10 * time.Second,
68 ExpectContinueTimeout: 1 * time.Second,
69 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
74 httpClient: http.Client{
75 CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
83 func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arvados.APIEndpoint, body io.Reader, opts interface{}) error {
84 aClient := arvados.Client{
85 Client: &conn.httpClient,
86 Scheme: conn.baseURL.Scheme,
87 APIHost: conn.baseURL.Host,
88 SendHeader: conn.SendHeader,
90 tokens, err := conn.tokenProvider(ctx)
93 } else if len(tokens) > 0 {
94 ctx = arvados.ContextWithAuthorization(ctx, "Bearer "+tokens[0])
96 // Use a non-empty auth string to ensure we override
97 // any default token set on aClient -- and to avoid
98 // having the remote prompt us to send a token by
100 ctx = arvados.ContextWithAuthorization(ctx, "Bearer -")
103 // Encode opts to JSON and decode from there to a
104 // map[string]interface{}, so we can munge the query params
105 // using the JSON key names specified by opts' struct tags.
106 j, err := json.Marshal(opts)
108 return fmt.Errorf("%T: requestAndDecode: Marshal opts: %s", conn, err)
110 var params map[string]interface{}
111 dec := json.NewDecoder(bytes.NewBuffer(j))
113 err = dec.Decode(¶ms)
115 return fmt.Errorf("%T: requestAndDecode: Decode opts: %s", conn, err)
117 if attrs, ok := params["attrs"]; ok && ep.AttrsKey != "" {
118 params[ep.AttrsKey] = attrs
119 delete(params, "attrs")
121 if limitStr, ok := params["limit"]; ok {
122 if limit, err := strconv.ParseInt(string(limitStr.(json.Number)), 10, 64); err == nil && limit < 0 {
123 // Negative limit means "not specified" here, but some
124 // servers/versions do not accept that, so we need to
125 // remove it entirely.
126 delete(params, "limit")
130 if authinfo, ok := params["auth_info"]; ok {
131 if tmp, ok2 := authinfo.(map[string]interface{}); ok2 {
132 for k, v := range tmp {
133 if strings.HasSuffix(k, "_at") {
134 // Change zero times values to nil
135 if v, ok3 := v.(string); ok3 && (strings.HasPrefix(v, "0001-01-01T00:00:00") || v == "") {
144 params["reader_tokens"] = tokens[1:]
147 if strings.Contains(ep.Path, "/{uuid}") {
148 uuid, _ := params["uuid"].(string)
149 path = strings.Replace(path, "/{uuid}", "/"+uuid, 1)
150 delete(params, "uuid")
152 return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
155 func (conn *Conn) BaseURL() url.URL {
159 func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
160 ep := arvados.EndpointConfigGet
161 var resp json.RawMessage
162 err := conn.requestAndDecode(ctx, &resp, ep, nil, nil)
166 func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) {
167 ep := arvados.EndpointLogin
168 var resp arvados.LoginResponse
169 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
170 resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
174 func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
175 ep := arvados.EndpointLogout
176 var resp arvados.LogoutResponse
177 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
178 resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
182 // If the given location is a valid URL and its origin is the same as
183 // conn.baseURL, return it as a relative URL. Otherwise, return it
185 func (conn *Conn) relativeToBaseURL(location string) string {
186 u, err := url.Parse(location)
187 if err == nil && u.Scheme == conn.baseURL.Scheme && strings.ToLower(u.Host) == strings.ToLower(conn.baseURL.Host) {
197 func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
198 ep := arvados.EndpointCollectionCreate
199 var resp arvados.Collection
200 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
204 func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
205 ep := arvados.EndpointCollectionUpdate
206 var resp arvados.Collection
207 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
211 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
212 ep := arvados.EndpointCollectionGet
213 var resp arvados.Collection
214 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
218 func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
219 ep := arvados.EndpointCollectionList
220 var resp arvados.CollectionList
221 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
225 func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
226 ep := arvados.EndpointCollectionProvenance
227 var resp map[string]interface{}
228 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
232 func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
233 ep := arvados.EndpointCollectionUsedBy
234 var resp map[string]interface{}
235 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
239 func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
240 ep := arvados.EndpointCollectionDelete
241 var resp arvados.Collection
242 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
246 func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
247 ep := arvados.EndpointCollectionTrash
248 var resp arvados.Collection
249 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
253 func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
254 ep := arvados.EndpointCollectionUntrash
255 var resp arvados.Collection
256 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
260 func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
261 ep := arvados.EndpointContainerCreate
262 var resp arvados.Container
263 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
267 func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
268 ep := arvados.EndpointContainerUpdate
269 var resp arvados.Container
270 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
274 func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
275 ep := arvados.EndpointContainerGet
276 var resp arvados.Container
277 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
281 func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
282 ep := arvados.EndpointContainerList
283 var resp arvados.ContainerList
284 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
288 func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
289 ep := arvados.EndpointContainerDelete
290 var resp arvados.Container
291 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
295 func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
296 ep := arvados.EndpointContainerLock
297 var resp arvados.Container
298 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
302 func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
303 ep := arvados.EndpointContainerUnlock
304 var resp arvados.Container
305 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
309 // ContainerSSH returns a connection to the out-of-band SSH server for
310 // a running container. If the returned error is nil, the caller is
311 // responsible for closing sshconn.Conn.
312 func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
313 addr := conn.baseURL.Host
314 if strings.Index(addr, ":") < 1 || (strings.Contains(addr, "::") && addr[0] != '[') {
315 // hostname or ::1 or 1::1
316 addr = net.JoinHostPort(addr, "https")
319 if tlsconf := conn.httpClient.Transport.(*http.Transport).TLSClientConfig; tlsconf != nil && tlsconf.InsecureSkipVerify {
322 netconn, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: insecure})
324 err = fmt.Errorf("tls.Dial: %w", err)
332 bufr := bufio.NewReader(netconn)
333 bufw := bufio.NewWriter(netconn)
335 u, err := conn.baseURL.Parse("/" + strings.Replace(arvados.EndpointContainerSSH.Path, "{uuid}", options.UUID, -1))
337 err = fmt.Errorf("tls.Dial: %w", err)
340 u.RawQuery = url.Values{
341 "detach_keys": {options.DetachKeys},
342 "login_username": {options.LoginUsername},
344 tokens, err := conn.tokenProvider(ctx)
347 } else if len(tokens) < 1 {
348 err = httpserver.ErrorWithStatus(errors.New("unauthorized"), http.StatusUnauthorized)
351 bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
352 bufw.WriteString("Authorization: Bearer " + tokens[0] + "\r\n")
353 bufw.WriteString("Host: " + u.Host + "\r\n")
354 bufw.WriteString("Upgrade: ssh\r\n")
355 bufw.WriteString("\r\n")
357 resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
359 err = fmt.Errorf("http.ReadResponse: %w", err)
362 if resp.StatusCode != http.StatusSwitchingProtocols {
363 defer resp.Body.Close()
364 body, _ := ioutil.ReadAll(resp.Body)
366 var errDoc httpserver.ErrorResponse
367 if err := json.Unmarshal(body, &errDoc); err == nil {
368 message = strings.Join(errDoc.Errors, "; ")
370 message = fmt.Sprintf("%q", body)
372 err = fmt.Errorf("server did not provide a tunnel: %s (HTTP %d)", message, resp.StatusCode)
375 if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
376 strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
377 err = fmt.Errorf("bad response from server: Upgrade %q Connection %q", resp.Header.Get("Upgrade"), resp.Header.Get("Connection"))
380 sshconn.Conn = netconn
381 sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
385 func (conn *Conn) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
386 ep := arvados.EndpointContainerRequestCreate
387 var resp arvados.ContainerRequest
388 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
392 func (conn *Conn) ContainerRequestUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.ContainerRequest, error) {
393 ep := arvados.EndpointContainerRequestUpdate
394 var resp arvados.ContainerRequest
395 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
399 func (conn *Conn) ContainerRequestGet(ctx context.Context, options arvados.GetOptions) (arvados.ContainerRequest, error) {
400 ep := arvados.EndpointContainerRequestGet
401 var resp arvados.ContainerRequest
402 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
406 func (conn *Conn) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
407 ep := arvados.EndpointContainerRequestList
408 var resp arvados.ContainerRequestList
409 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
413 func (conn *Conn) ContainerRequestDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.ContainerRequest, error) {
414 ep := arvados.EndpointContainerRequestDelete
415 var resp arvados.ContainerRequest
416 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
420 func (conn *Conn) GroupCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Group, error) {
421 ep := arvados.EndpointGroupCreate
422 var resp arvados.Group
423 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
427 func (conn *Conn) GroupUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Group, error) {
428 ep := arvados.EndpointGroupUpdate
429 var resp arvados.Group
430 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
434 func (conn *Conn) GroupGet(ctx context.Context, options arvados.GetOptions) (arvados.Group, error) {
435 ep := arvados.EndpointGroupGet
436 var resp arvados.Group
437 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
441 func (conn *Conn) GroupList(ctx context.Context, options arvados.ListOptions) (arvados.GroupList, error) {
442 ep := arvados.EndpointGroupList
443 var resp arvados.GroupList
444 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
448 func (conn *Conn) GroupContents(ctx context.Context, options arvados.GroupContentsOptions) (arvados.ObjectList, error) {
449 // The requested UUID can be a user (virtual home project), which we just pass on to
451 if strings.Index(options.UUID, "j7d0g") != 6 {
452 ep := arvados.EndpointGroupContents
453 var resp arvados.ObjectList
454 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
458 log := ctxlog.FromContext(ctx)
459 var resp arvados.ObjectList
461 // Get the group object
462 epGet := arvados.EndpointGroupGet
463 var respGroup arvados.Group
464 err := conn.requestAndDecode(ctx, &respGroup, epGet, nil, options)
469 // If the group has groupClass 'filter', apply the filters before getting the contents.
470 if respGroup.GroupClass == "filter" {
471 if filters, ok := respGroup.Properties["filters"]; ok {
472 for _, f := range filters.([]interface{}) {
473 // f is supposed to be a []string
474 tmp, ok2 := f.([]interface{})
475 if !ok2 || len(tmp) < 3 {
476 log.Warnf("filter unparsable: %T, %+v, original field: %T, %+v\n", tmp, tmp, f, f)
479 var filter arvados.Filter
480 filter.Attr = tmp[0].(string)
481 filter.Operator = tmp[1].(string)
482 filter.Operand = tmp[2]
483 options.Filters = append(options.Filters, filter)
488 ep := arvados.EndpointGroupContents
489 err = conn.requestAndDecode(ctx, &resp, ep, nil, options)
493 func (conn *Conn) GroupShared(ctx context.Context, options arvados.ListOptions) (arvados.GroupList, error) {
494 ep := arvados.EndpointGroupShared
495 var resp arvados.GroupList
496 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
500 func (conn *Conn) GroupDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Group, error) {
501 ep := arvados.EndpointGroupDelete
502 var resp arvados.Group
503 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
507 func (conn *Conn) GroupUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Group, error) {
508 ep := arvados.EndpointGroupUntrash
509 var resp arvados.Group
510 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
514 func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
515 ep := arvados.EndpointSpecimenCreate
516 var resp arvados.Specimen
517 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
521 func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
522 ep := arvados.EndpointSpecimenUpdate
523 var resp arvados.Specimen
524 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
528 func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
529 ep := arvados.EndpointSpecimenGet
530 var resp arvados.Specimen
531 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
535 func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
536 ep := arvados.EndpointSpecimenList
537 var resp arvados.SpecimenList
538 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
542 func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
543 ep := arvados.EndpointSpecimenDelete
544 var resp arvados.Specimen
545 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
549 func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
550 ep := arvados.EndpointUserCreate
551 var resp arvados.User
552 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
555 func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
556 ep := arvados.EndpointUserUpdate
557 var resp arvados.User
558 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
561 func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
562 ep := arvados.EndpointUserUpdateUUID
563 var resp arvados.User
564 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
567 func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
568 ep := arvados.EndpointUserMerge
569 var resp arvados.User
570 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
573 func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
574 ep := arvados.EndpointUserActivate
575 var resp arvados.User
576 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
579 func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
580 ep := arvados.EndpointUserSetup
581 var resp map[string]interface{}
582 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
585 func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
586 ep := arvados.EndpointUserUnsetup
587 var resp arvados.User
588 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
591 func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
592 ep := arvados.EndpointUserGet
593 var resp arvados.User
594 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
597 func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
598 ep := arvados.EndpointUserGetCurrent
599 var resp arvados.User
600 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
603 func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
604 ep := arvados.EndpointUserGetSystem
605 var resp arvados.User
606 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
609 func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
610 ep := arvados.EndpointUserList
611 var resp arvados.UserList
612 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
615 func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
616 ep := arvados.EndpointUserDelete
617 var resp arvados.User
618 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
622 func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
623 ep := arvados.EndpointAPIClientAuthorizationCurrent
624 var resp arvados.APIClientAuthorization
625 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
629 type UserSessionAuthInfo struct {
630 UserUUID string `json:"user_uuid"`
631 Email string `json:"email"`
632 AlternateEmails []string `json:"alternate_emails"`
633 FirstName string `json:"first_name"`
634 LastName string `json:"last_name"`
635 Username string `json:"username"`
636 ExpiresAt time.Time `json:"expires_at"`
639 type UserSessionCreateOptions struct {
640 AuthInfo UserSessionAuthInfo `json:"auth_info"`
641 ReturnTo string `json:"return_to"`
644 func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCreateOptions) (arvados.LoginResponse, error) {
645 ep := arvados.APIEndpoint{Method: "POST", Path: "auth/controller/callback"}
646 var resp arvados.LoginResponse
647 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
651 func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
652 ep := arvados.EndpointUserBatchUpdate
653 var resp arvados.UserList
654 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
658 func (conn *Conn) UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
659 ep := arvados.EndpointUserAuthenticate
660 var resp arvados.APIClientAuthorization
661 err := conn.requestAndDecode(ctx, &resp, ep, nil, options)