Merge branch '17165-vscode-training-doc' refs #17165
[arvados.git] / lib / controller / rpc / conn.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package rpc
6
7 import (
8         "bufio"
9         "bytes"
10         "context"
11         "crypto/tls"
12         "encoding/json"
13         "errors"
14         "fmt"
15         "io"
16         "io/ioutil"
17         "net"
18         "net/http"
19         "net/url"
20         "strconv"
21         "strings"
22         "time"
23
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/httpserver"
27 )
28
29 const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
30
31 type TokenProvider func(context.Context) ([]string, error)
32
33 func PassthroughTokenProvider(ctx context.Context) ([]string, error) {
34         incoming, ok := auth.FromContext(ctx)
35         if !ok {
36                 return nil, errors.New("no token provided")
37         }
38         return incoming.Tokens, nil
39 }
40
41 type Conn struct {
42         SendHeader    http.Header
43         clusterID     string
44         httpClient    http.Client
45         baseURL       url.URL
46         tokenProvider TokenProvider
47 }
48
49 func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *Conn {
50         transport := http.DefaultTransport
51         if insecure {
52                 // It's not safe to copy *http.DefaultTransport
53                 // because it has a mutex (which might be locked)
54                 // protecting a private map (which might not be nil).
55                 // So we build our own, using the Go 1.12 default
56                 // values, ignoring any changes the application has
57                 // made to http.DefaultTransport.
58                 transport = &http.Transport{
59                         DialContext: (&net.Dialer{
60                                 Timeout:   30 * time.Second,
61                                 KeepAlive: 30 * time.Second,
62                                 DualStack: true,
63                         }).DialContext,
64                         MaxIdleConns:          100,
65                         IdleConnTimeout:       90 * time.Second,
66                         TLSHandshakeTimeout:   10 * time.Second,
67                         ExpectContinueTimeout: 1 * time.Second,
68                         TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
69                 }
70         }
71         return &Conn{
72                 clusterID: clusterID,
73                 httpClient: http.Client{
74                         CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
75                         Transport:     transport,
76                 },
77                 baseURL:       *url,
78                 tokenProvider: tp,
79         }
80 }
81
82 func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arvados.APIEndpoint, body io.Reader, opts interface{}) error {
83         aClient := arvados.Client{
84                 Client:     &conn.httpClient,
85                 Scheme:     conn.baseURL.Scheme,
86                 APIHost:    conn.baseURL.Host,
87                 SendHeader: conn.SendHeader,
88         }
89         tokens, err := conn.tokenProvider(ctx)
90         if err != nil {
91                 return err
92         } else if len(tokens) > 0 {
93                 ctx = arvados.ContextWithAuthorization(ctx, "Bearer "+tokens[0])
94         } else {
95                 // Use a non-empty auth string to ensure we override
96                 // any default token set on aClient -- and to avoid
97                 // having the remote prompt us to send a token by
98                 // responding 401.
99                 ctx = arvados.ContextWithAuthorization(ctx, "Bearer -")
100         }
101
102         // Encode opts to JSON and decode from there to a
103         // map[string]interface{}, so we can munge the query params
104         // using the JSON key names specified by opts' struct tags.
105         j, err := json.Marshal(opts)
106         if err != nil {
107                 return fmt.Errorf("%T: requestAndDecode: Marshal opts: %s", conn, err)
108         }
109         var params map[string]interface{}
110         dec := json.NewDecoder(bytes.NewBuffer(j))
111         dec.UseNumber()
112         err = dec.Decode(&params)
113         if err != nil {
114                 return fmt.Errorf("%T: requestAndDecode: Decode opts: %s", conn, err)
115         }
116         if attrs, ok := params["attrs"]; ok && ep.AttrsKey != "" {
117                 params[ep.AttrsKey] = attrs
118                 delete(params, "attrs")
119         }
120         if limitStr, ok := params["limit"]; ok {
121                 if limit, err := strconv.ParseInt(string(limitStr.(json.Number)), 10, 64); err == nil && limit < 0 {
122                         // Negative limit means "not specified" here, but some
123                         // servers/versions do not accept that, so we need to
124                         // remove it entirely.
125                         delete(params, "limit")
126                 }
127         }
128
129         if authinfo, ok := params["auth_info"]; ok {
130                 if tmp, ok2 := authinfo.(map[string]interface{}); ok2 {
131                         for k, v := range tmp {
132                                 if strings.HasSuffix(k, "_at") {
133                                         // Change zero times values to nil
134                                         if v, ok3 := v.(string); ok3 && (strings.HasPrefix(v, "0001-01-01T00:00:00") || v == "") {
135                                                 tmp[k] = nil
136                                         }
137                                 }
138                         }
139                 }
140         }
141
142         if len(tokens) > 1 {
143                 params["reader_tokens"] = tokens[1:]
144         }
145         path := ep.Path
146         if strings.Contains(ep.Path, "/{uuid}") {
147                 uuid, _ := params["uuid"].(string)
148                 path = strings.Replace(path, "/{uuid}", "/"+uuid, 1)
149                 delete(params, "uuid")
150         }
151         return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
152 }
153
154 func (conn *Conn) BaseURL() url.URL {
155         return conn.baseURL
156 }
157
158 func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
159         ep := arvados.EndpointConfigGet
160         var resp json.RawMessage
161         err := conn.requestAndDecode(ctx, &resp, ep, nil, nil)
162         return resp, err
163 }
164
165 func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) {
166         ep := arvados.EndpointLogin
167         var resp arvados.LoginResponse
168         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
169         resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
170         return resp, err
171 }
172
173 func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
174         ep := arvados.EndpointLogout
175         var resp arvados.LogoutResponse
176         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
177         resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
178         return resp, err
179 }
180
181 // If the given location is a valid URL and its origin is the same as
182 // conn.baseURL, return it as a relative URL. Otherwise, return it
183 // unmodified.
184 func (conn *Conn) relativeToBaseURL(location string) string {
185         u, err := url.Parse(location)
186         if err == nil && u.Scheme == conn.baseURL.Scheme && strings.ToLower(u.Host) == strings.ToLower(conn.baseURL.Host) {
187                 u.Opaque = ""
188                 u.Scheme = ""
189                 u.User = nil
190                 u.Host = ""
191                 return u.String()
192         }
193         return location
194 }
195
196 func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
197         ep := arvados.EndpointCollectionCreate
198         var resp arvados.Collection
199         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
200         return resp, err
201 }
202
203 func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
204         ep := arvados.EndpointCollectionUpdate
205         var resp arvados.Collection
206         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
207         return resp, err
208 }
209
210 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
211         ep := arvados.EndpointCollectionGet
212         var resp arvados.Collection
213         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
214         return resp, err
215 }
216
217 func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
218         ep := arvados.EndpointCollectionList
219         var resp arvados.CollectionList
220         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
221         return resp, err
222 }
223
224 func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
225         ep := arvados.EndpointCollectionProvenance
226         var resp map[string]interface{}
227         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
228         return resp, err
229 }
230
231 func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
232         ep := arvados.EndpointCollectionUsedBy
233         var resp map[string]interface{}
234         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
235         return resp, err
236 }
237
238 func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
239         ep := arvados.EndpointCollectionDelete
240         var resp arvados.Collection
241         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
242         return resp, err
243 }
244
245 func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
246         ep := arvados.EndpointCollectionTrash
247         var resp arvados.Collection
248         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
249         return resp, err
250 }
251
252 func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
253         ep := arvados.EndpointCollectionUntrash
254         var resp arvados.Collection
255         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
256         return resp, err
257 }
258
259 func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
260         ep := arvados.EndpointContainerCreate
261         var resp arvados.Container
262         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
263         return resp, err
264 }
265
266 func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
267         ep := arvados.EndpointContainerUpdate
268         var resp arvados.Container
269         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
270         return resp, err
271 }
272
273 func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
274         ep := arvados.EndpointContainerGet
275         var resp arvados.Container
276         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
277         return resp, err
278 }
279
280 func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
281         ep := arvados.EndpointContainerList
282         var resp arvados.ContainerList
283         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
284         return resp, err
285 }
286
287 func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
288         ep := arvados.EndpointContainerDelete
289         var resp arvados.Container
290         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
291         return resp, err
292 }
293
294 func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
295         ep := arvados.EndpointContainerLock
296         var resp arvados.Container
297         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
298         return resp, err
299 }
300
301 func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
302         ep := arvados.EndpointContainerUnlock
303         var resp arvados.Container
304         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
305         return resp, err
306 }
307
308 // ContainerSSH returns a connection to the out-of-band SSH server for
309 // a running container. If the returned error is nil, the caller is
310 // responsible for closing sshconn.Conn.
311 func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
312         addr := conn.baseURL.Host
313         if strings.Index(addr, ":") < 1 || (strings.Contains(addr, "::") && addr[0] != '[') {
314                 // hostname or ::1 or 1::1
315                 addr = net.JoinHostPort(addr, "https")
316         }
317         insecure := false
318         if tlsconf := conn.httpClient.Transport.(*http.Transport).TLSClientConfig; tlsconf != nil && tlsconf.InsecureSkipVerify {
319                 insecure = true
320         }
321         netconn, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: insecure})
322         if err != nil {
323                 err = fmt.Errorf("tls.Dial: %w", err)
324                 return
325         }
326         defer func() {
327                 if err != nil {
328                         netconn.Close()
329                 }
330         }()
331         bufr := bufio.NewReader(netconn)
332         bufw := bufio.NewWriter(netconn)
333
334         u, err := conn.baseURL.Parse("/" + strings.Replace(arvados.EndpointContainerSSH.Path, "{uuid}", options.UUID, -1))
335         if err != nil {
336                 err = fmt.Errorf("tls.Dial: %w", err)
337                 return
338         }
339         u.RawQuery = url.Values{
340                 "detach_keys":    {options.DetachKeys},
341                 "login_username": {options.LoginUsername},
342         }.Encode()
343         tokens, err := conn.tokenProvider(ctx)
344         if err != nil {
345                 return
346         } else if len(tokens) < 1 {
347                 err = httpserver.ErrorWithStatus(errors.New("unauthorized"), http.StatusUnauthorized)
348                 return
349         }
350         bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
351         bufw.WriteString("Authorization: Bearer " + tokens[0] + "\r\n")
352         bufw.WriteString("Host: " + u.Host + "\r\n")
353         bufw.WriteString("Upgrade: ssh\r\n")
354         bufw.WriteString("\r\n")
355         bufw.Flush()
356         resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
357         if err != nil {
358                 err = fmt.Errorf("http.ReadResponse: %w", err)
359                 return
360         }
361         if resp.StatusCode != http.StatusSwitchingProtocols {
362                 defer resp.Body.Close()
363                 body, _ := ioutil.ReadAll(resp.Body)
364                 var message string
365                 var errDoc httpserver.ErrorResponse
366                 if err := json.Unmarshal(body, &errDoc); err == nil {
367                         message = strings.Join(errDoc.Errors, "; ")
368                 } else {
369                         message = fmt.Sprintf("%q", body)
370                 }
371                 err = fmt.Errorf("server did not provide a tunnel: %s (HTTP %d)", message, resp.StatusCode)
372                 return
373         }
374         if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
375                 strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
376                 err = fmt.Errorf("bad response from server: Upgrade %q Connection %q", resp.Header.Get("Upgrade"), resp.Header.Get("Connection"))
377                 return
378         }
379         sshconn.Conn = netconn
380         sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
381         return
382 }
383
384 func (conn *Conn) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
385         ep := arvados.EndpointContainerRequestCreate
386         var resp arvados.ContainerRequest
387         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
388         return resp, err
389 }
390
391 func (conn *Conn) ContainerRequestUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.ContainerRequest, error) {
392         ep := arvados.EndpointContainerRequestUpdate
393         var resp arvados.ContainerRequest
394         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
395         return resp, err
396 }
397
398 func (conn *Conn) ContainerRequestGet(ctx context.Context, options arvados.GetOptions) (arvados.ContainerRequest, error) {
399         ep := arvados.EndpointContainerRequestGet
400         var resp arvados.ContainerRequest
401         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
402         return resp, err
403 }
404
405 func (conn *Conn) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
406         ep := arvados.EndpointContainerRequestList
407         var resp arvados.ContainerRequestList
408         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
409         return resp, err
410 }
411
412 func (conn *Conn) ContainerRequestDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.ContainerRequest, error) {
413         ep := arvados.EndpointContainerRequestDelete
414         var resp arvados.ContainerRequest
415         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
416         return resp, err
417 }
418
419 func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
420         ep := arvados.EndpointSpecimenCreate
421         var resp arvados.Specimen
422         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
423         return resp, err
424 }
425
426 func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
427         ep := arvados.EndpointSpecimenUpdate
428         var resp arvados.Specimen
429         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
430         return resp, err
431 }
432
433 func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
434         ep := arvados.EndpointSpecimenGet
435         var resp arvados.Specimen
436         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
437         return resp, err
438 }
439
440 func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
441         ep := arvados.EndpointSpecimenList
442         var resp arvados.SpecimenList
443         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
444         return resp, err
445 }
446
447 func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
448         ep := arvados.EndpointSpecimenDelete
449         var resp arvados.Specimen
450         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
451         return resp, err
452 }
453
454 func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
455         ep := arvados.EndpointUserCreate
456         var resp arvados.User
457         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
458         return resp, err
459 }
460 func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
461         ep := arvados.EndpointUserUpdate
462         var resp arvados.User
463         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
464         return resp, err
465 }
466 func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
467         ep := arvados.EndpointUserUpdateUUID
468         var resp arvados.User
469         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
470         return resp, err
471 }
472 func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
473         ep := arvados.EndpointUserMerge
474         var resp arvados.User
475         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
476         return resp, err
477 }
478 func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
479         ep := arvados.EndpointUserActivate
480         var resp arvados.User
481         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
482         return resp, err
483 }
484 func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
485         ep := arvados.EndpointUserSetup
486         var resp map[string]interface{}
487         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
488         return resp, err
489 }
490 func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
491         ep := arvados.EndpointUserUnsetup
492         var resp arvados.User
493         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
494         return resp, err
495 }
496 func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
497         ep := arvados.EndpointUserGet
498         var resp arvados.User
499         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
500         return resp, err
501 }
502 func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
503         ep := arvados.EndpointUserGetCurrent
504         var resp arvados.User
505         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
506         return resp, err
507 }
508 func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
509         ep := arvados.EndpointUserGetSystem
510         var resp arvados.User
511         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
512         return resp, err
513 }
514 func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
515         ep := arvados.EndpointUserList
516         var resp arvados.UserList
517         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
518         return resp, err
519 }
520 func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
521         ep := arvados.EndpointUserDelete
522         var resp arvados.User
523         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
524         return resp, err
525 }
526
527 func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
528         ep := arvados.EndpointAPIClientAuthorizationCurrent
529         var resp arvados.APIClientAuthorization
530         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
531         return resp, err
532 }
533
534 type UserSessionAuthInfo struct {
535         UserUUID        string    `json:"user_uuid"`
536         Email           string    `json:"email"`
537         AlternateEmails []string  `json:"alternate_emails"`
538         FirstName       string    `json:"first_name"`
539         LastName        string    `json:"last_name"`
540         Username        string    `json:"username"`
541         ExpiresAt       time.Time `json:"expires_at"`
542 }
543
544 type UserSessionCreateOptions struct {
545         AuthInfo UserSessionAuthInfo `json:"auth_info"`
546         ReturnTo string              `json:"return_to"`
547 }
548
549 func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCreateOptions) (arvados.LoginResponse, error) {
550         ep := arvados.APIEndpoint{Method: "POST", Path: "auth/controller/callback"}
551         var resp arvados.LoginResponse
552         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
553         return resp, err
554 }
555
556 func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
557         ep := arvados.EndpointUserBatchUpdate
558         var resp arvados.UserList
559         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
560         return resp, err
561 }
562
563 func (conn *Conn) UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
564         ep := arvados.EndpointUserAuthenticate
565         var resp arvados.APIClientAuthorization
566         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
567         return resp, err
568 }