16263: Decodes JSON numbers as strings instead of float64.
[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         "bytes"
9         "context"
10         "crypto/tls"
11         "encoding/json"
12         "errors"
13         "fmt"
14         "io"
15         "net"
16         "net/http"
17         "net/url"
18         "strconv"
19         "strings"
20         "time"
21
22         "git.arvados.org/arvados.git/sdk/go/arvados"
23         "git.arvados.org/arvados.git/sdk/go/auth"
24 )
25
26 type TokenProvider func(context.Context) ([]string, error)
27
28 func PassthroughTokenProvider(ctx context.Context) ([]string, error) {
29         if incoming, ok := auth.FromContext(ctx); !ok {
30                 return nil, errors.New("no token provided")
31         } else {
32                 return incoming.Tokens, nil
33         }
34 }
35
36 type Conn struct {
37         SendHeader    http.Header
38         clusterID     string
39         httpClient    http.Client
40         baseURL       url.URL
41         tokenProvider TokenProvider
42 }
43
44 func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *Conn {
45         transport := http.DefaultTransport
46         if insecure {
47                 // It's not safe to copy *http.DefaultTransport
48                 // because it has a mutex (which might be locked)
49                 // protecting a private map (which might not be nil).
50                 // So we build our own, using the Go 1.12 default
51                 // values, ignoring any changes the application has
52                 // made to http.DefaultTransport.
53                 transport = &http.Transport{
54                         DialContext: (&net.Dialer{
55                                 Timeout:   30 * time.Second,
56                                 KeepAlive: 30 * time.Second,
57                                 DualStack: true,
58                         }).DialContext,
59                         MaxIdleConns:          100,
60                         IdleConnTimeout:       90 * time.Second,
61                         TLSHandshakeTimeout:   10 * time.Second,
62                         ExpectContinueTimeout: 1 * time.Second,
63                         TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
64                 }
65         }
66         return &Conn{
67                 clusterID: clusterID,
68                 httpClient: http.Client{
69                         CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
70                         Transport:     transport,
71                 },
72                 baseURL:       *url,
73                 tokenProvider: tp,
74         }
75 }
76
77 func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arvados.APIEndpoint, body io.Reader, opts interface{}) error {
78         aClient := arvados.Client{
79                 Client:     &conn.httpClient,
80                 Scheme:     conn.baseURL.Scheme,
81                 APIHost:    conn.baseURL.Host,
82                 SendHeader: conn.SendHeader,
83         }
84         tokens, err := conn.tokenProvider(ctx)
85         if err != nil {
86                 return err
87         } else if len(tokens) > 0 {
88                 ctx = arvados.ContextWithAuthorization(ctx, "Bearer "+tokens[0])
89         } else {
90                 // Use a non-empty auth string to ensure we override
91                 // any default token set on aClient -- and to avoid
92                 // having the remote prompt us to send a token by
93                 // responding 401.
94                 ctx = arvados.ContextWithAuthorization(ctx, "Bearer -")
95         }
96
97         // Encode opts to JSON and decode from there to a
98         // map[string]interface{}, so we can munge the query params
99         // using the JSON key names specified by opts' struct tags.
100         j, err := json.Marshal(opts)
101         if err != nil {
102                 return fmt.Errorf("%T: requestAndDecode: Marshal opts: %s", conn, err)
103         }
104         var params map[string]interface{}
105         dec := json.NewDecoder(bytes.NewBuffer(j))
106         dec.UseNumber()
107         err = dec.Decode(&params)
108         if err != nil {
109                 return fmt.Errorf("%T: requestAndDecode: Decode opts: %s", conn, err)
110         }
111         if attrs, ok := params["attrs"]; ok && ep.AttrsKey != "" {
112                 params[ep.AttrsKey] = attrs
113                 delete(params, "attrs")
114         }
115         if limitStr, ok := params["limit"]; ok {
116                 if limit, err := strconv.ParseInt(string(limitStr.(json.Number)), 10, 64); err == nil && limit < 0 {
117                         // Negative limit means "not specified" here, but some
118                         // servers/versions do not accept that, so we need to
119                         // remove it entirely.
120                         delete(params, "limit")
121                 }
122         }
123         if len(tokens) > 1 {
124                 params["reader_tokens"] = tokens[1:]
125         }
126         path := ep.Path
127         if strings.Contains(ep.Path, "/{uuid}") {
128                 uuid, _ := params["uuid"].(string)
129                 path = strings.Replace(path, "/{uuid}", "/"+uuid, 1)
130                 delete(params, "uuid")
131         }
132         return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
133 }
134
135 func (conn *Conn) BaseURL() url.URL {
136         return conn.baseURL
137 }
138
139 func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
140         ep := arvados.EndpointConfigGet
141         var resp json.RawMessage
142         err := conn.requestAndDecode(ctx, &resp, ep, nil, nil)
143         return resp, err
144 }
145
146 func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) {
147         ep := arvados.EndpointLogin
148         var resp arvados.LoginResponse
149         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
150         resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
151         return resp, err
152 }
153
154 func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
155         ep := arvados.EndpointLogout
156         var resp arvados.LogoutResponse
157         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
158         resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
159         return resp, err
160 }
161
162 // If the given location is a valid URL and its origin is the same as
163 // conn.baseURL, return it as a relative URL. Otherwise, return it
164 // unmodified.
165 func (conn *Conn) relativeToBaseURL(location string) string {
166         u, err := url.Parse(location)
167         if err == nil && u.Scheme == conn.baseURL.Scheme && strings.ToLower(u.Host) == strings.ToLower(conn.baseURL.Host) {
168                 u.Opaque = ""
169                 u.Scheme = ""
170                 u.User = nil
171                 u.Host = ""
172                 return u.String()
173         } else {
174                 return location
175         }
176 }
177
178 func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
179         ep := arvados.EndpointCollectionCreate
180         var resp arvados.Collection
181         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
182         return resp, err
183 }
184
185 func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
186         ep := arvados.EndpointCollectionUpdate
187         var resp arvados.Collection
188         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
189         return resp, err
190 }
191
192 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
193         ep := arvados.EndpointCollectionGet
194         var resp arvados.Collection
195         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
196         return resp, err
197 }
198
199 func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
200         ep := arvados.EndpointCollectionList
201         var resp arvados.CollectionList
202         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
203         return resp, err
204 }
205
206 func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
207         ep := arvados.EndpointCollectionProvenance
208         var resp map[string]interface{}
209         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
210         return resp, err
211 }
212
213 func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
214         ep := arvados.EndpointCollectionUsedBy
215         var resp map[string]interface{}
216         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
217         return resp, err
218 }
219
220 func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
221         ep := arvados.EndpointCollectionDelete
222         var resp arvados.Collection
223         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
224         return resp, err
225 }
226
227 func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
228         ep := arvados.EndpointCollectionTrash
229         var resp arvados.Collection
230         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
231         return resp, err
232 }
233
234 func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
235         ep := arvados.EndpointCollectionUntrash
236         var resp arvados.Collection
237         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
238         return resp, err
239 }
240
241 func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
242         ep := arvados.EndpointContainerCreate
243         var resp arvados.Container
244         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
245         return resp, err
246 }
247
248 func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
249         ep := arvados.EndpointContainerUpdate
250         var resp arvados.Container
251         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
252         return resp, err
253 }
254
255 func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
256         ep := arvados.EndpointContainerGet
257         var resp arvados.Container
258         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
259         return resp, err
260 }
261
262 func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
263         ep := arvados.EndpointContainerList
264         var resp arvados.ContainerList
265         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
266         return resp, err
267 }
268
269 func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
270         ep := arvados.EndpointContainerDelete
271         var resp arvados.Container
272         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
273         return resp, err
274 }
275
276 func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
277         ep := arvados.EndpointContainerLock
278         var resp arvados.Container
279         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
280         return resp, err
281 }
282
283 func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
284         ep := arvados.EndpointContainerUnlock
285         var resp arvados.Container
286         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
287         return resp, err
288 }
289
290 func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
291         ep := arvados.EndpointSpecimenCreate
292         var resp arvados.Specimen
293         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
294         return resp, err
295 }
296
297 func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
298         ep := arvados.EndpointSpecimenUpdate
299         var resp arvados.Specimen
300         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
301         return resp, err
302 }
303
304 func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
305         ep := arvados.EndpointSpecimenGet
306         var resp arvados.Specimen
307         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
308         return resp, err
309 }
310
311 func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
312         ep := arvados.EndpointSpecimenList
313         var resp arvados.SpecimenList
314         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
315         return resp, err
316 }
317
318 func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
319         ep := arvados.EndpointSpecimenDelete
320         var resp arvados.Specimen
321         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
322         return resp, err
323 }
324
325 func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
326         ep := arvados.EndpointUserCreate
327         var resp arvados.User
328         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
329         return resp, err
330 }
331 func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
332         ep := arvados.EndpointUserUpdate
333         var resp arvados.User
334         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
335         return resp, err
336 }
337 func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
338         ep := arvados.EndpointUserUpdateUUID
339         var resp arvados.User
340         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
341         return resp, err
342 }
343 func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
344         ep := arvados.EndpointUserMerge
345         var resp arvados.User
346         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
347         return resp, err
348 }
349 func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
350         ep := arvados.EndpointUserActivate
351         var resp arvados.User
352         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
353         return resp, err
354 }
355 func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
356         ep := arvados.EndpointUserSetup
357         var resp map[string]interface{}
358         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
359         return resp, err
360 }
361 func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
362         ep := arvados.EndpointUserUnsetup
363         var resp arvados.User
364         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
365         return resp, err
366 }
367 func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
368         ep := arvados.EndpointUserGet
369         var resp arvados.User
370         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
371         return resp, err
372 }
373 func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
374         ep := arvados.EndpointUserGetCurrent
375         var resp arvados.User
376         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
377         return resp, err
378 }
379 func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
380         ep := arvados.EndpointUserGetSystem
381         var resp arvados.User
382         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
383         return resp, err
384 }
385 func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
386         ep := arvados.EndpointUserList
387         var resp arvados.UserList
388         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
389         return resp, err
390 }
391 func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
392         ep := arvados.EndpointUserDelete
393         var resp arvados.User
394         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
395         return resp, err
396 }
397
398 func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
399         ep := arvados.EndpointAPIClientAuthorizationCurrent
400         var resp arvados.APIClientAuthorization
401         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
402         return resp, err
403 }
404
405 type UserSessionAuthInfo struct {
406         Email           string   `json:"email"`
407         AlternateEmails []string `json:"alternate_emails"`
408         FirstName       string   `json:"first_name"`
409         LastName        string   `json:"last_name"`
410         Username        string   `json:"username"`
411 }
412
413 type UserSessionCreateOptions struct {
414         AuthInfo UserSessionAuthInfo `json:"auth_info"`
415         ReturnTo string              `json:"return_to"`
416 }
417
418 func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCreateOptions) (arvados.LoginResponse, error) {
419         ep := arvados.APIEndpoint{Method: "POST", Path: "auth/controller/callback"}
420         var resp arvados.LoginResponse
421         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
422         return resp, err
423 }
424
425 func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
426         ep := arvados.APIEndpoint{Method: "PATCH", Path: "arvados/v1/users/batch_update"}
427         var resp arvados.UserList
428         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
429         return resp, err
430 }