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