16101: Handle logout without sso-provider.
[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.arvados.org/arvados.git/sdk/go/arvados"
21         "git.arvados.org/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 func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
149         ep := arvados.EndpointLogout
150         var resp arvados.LogoutResponse
151         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
152         resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
153         return resp, err
154 }
155
156 // If the given location is a valid URL and its origin is the same as
157 // conn.baseURL, return it as a relative URL. Otherwise, return it
158 // unmodified.
159 func (conn *Conn) relativeToBaseURL(location string) string {
160         u, err := url.Parse(location)
161         if err == nil && u.Scheme == conn.baseURL.Scheme && strings.ToLower(u.Host) == strings.ToLower(conn.baseURL.Host) {
162                 u.Opaque = ""
163                 u.Scheme = ""
164                 u.User = nil
165                 u.Host = ""
166                 return u.String()
167         } else {
168                 return location
169         }
170 }
171
172 func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
173         ep := arvados.EndpointCollectionCreate
174         var resp arvados.Collection
175         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
176         return resp, err
177 }
178
179 func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
180         ep := arvados.EndpointCollectionUpdate
181         var resp arvados.Collection
182         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
183         return resp, err
184 }
185
186 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
187         ep := arvados.EndpointCollectionGet
188         var resp arvados.Collection
189         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
190         return resp, err
191 }
192
193 func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
194         ep := arvados.EndpointCollectionList
195         var resp arvados.CollectionList
196         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
197         return resp, err
198 }
199
200 func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
201         ep := arvados.EndpointCollectionProvenance
202         var resp map[string]interface{}
203         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
204         return resp, err
205 }
206
207 func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
208         ep := arvados.EndpointCollectionUsedBy
209         var resp map[string]interface{}
210         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
211         return resp, err
212 }
213
214 func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
215         ep := arvados.EndpointCollectionDelete
216         var resp arvados.Collection
217         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
218         return resp, err
219 }
220
221 func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
222         ep := arvados.EndpointCollectionTrash
223         var resp arvados.Collection
224         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
225         return resp, err
226 }
227
228 func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
229         ep := arvados.EndpointCollectionUntrash
230         var resp arvados.Collection
231         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
232         return resp, err
233 }
234
235 func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
236         ep := arvados.EndpointContainerCreate
237         var resp arvados.Container
238         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
239         return resp, err
240 }
241
242 func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
243         ep := arvados.EndpointContainerUpdate
244         var resp arvados.Container
245         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
246         return resp, err
247 }
248
249 func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
250         ep := arvados.EndpointContainerGet
251         var resp arvados.Container
252         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
253         return resp, err
254 }
255
256 func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
257         ep := arvados.EndpointContainerList
258         var resp arvados.ContainerList
259         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
260         return resp, err
261 }
262
263 func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
264         ep := arvados.EndpointContainerDelete
265         var resp arvados.Container
266         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
267         return resp, err
268 }
269
270 func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
271         ep := arvados.EndpointContainerLock
272         var resp arvados.Container
273         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
274         return resp, err
275 }
276
277 func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
278         ep := arvados.EndpointContainerUnlock
279         var resp arvados.Container
280         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
281         return resp, err
282 }
283
284 func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
285         ep := arvados.EndpointSpecimenCreate
286         var resp arvados.Specimen
287         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
288         return resp, err
289 }
290
291 func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
292         ep := arvados.EndpointSpecimenUpdate
293         var resp arvados.Specimen
294         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
295         return resp, err
296 }
297
298 func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
299         ep := arvados.EndpointSpecimenGet
300         var resp arvados.Specimen
301         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
302         return resp, err
303 }
304
305 func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
306         ep := arvados.EndpointSpecimenList
307         var resp arvados.SpecimenList
308         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
309         return resp, err
310 }
311
312 func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
313         ep := arvados.EndpointSpecimenDelete
314         var resp arvados.Specimen
315         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
316         return resp, err
317 }
318
319 func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
320         ep := arvados.EndpointUserCreate
321         var resp arvados.User
322         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
323         return resp, err
324 }
325 func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
326         ep := arvados.EndpointUserUpdate
327         var resp arvados.User
328         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
329         return resp, err
330 }
331 func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
332         ep := arvados.EndpointUserUpdateUUID
333         var resp arvados.User
334         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
335         return resp, err
336 }
337 func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
338         ep := arvados.EndpointUserMerge
339         var resp arvados.User
340         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
341         return resp, err
342 }
343 func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
344         ep := arvados.EndpointUserActivate
345         var resp arvados.User
346         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
347         return resp, err
348 }
349 func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
350         ep := arvados.EndpointUserSetup
351         var resp map[string]interface{}
352         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
353         return resp, err
354 }
355 func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
356         ep := arvados.EndpointUserUnsetup
357         var resp arvados.User
358         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
359         return resp, err
360 }
361 func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
362         ep := arvados.EndpointUserGet
363         var resp arvados.User
364         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
365         return resp, err
366 }
367 func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
368         ep := arvados.EndpointUserGetCurrent
369         var resp arvados.User
370         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
371         return resp, err
372 }
373 func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
374         ep := arvados.EndpointUserGetSystem
375         var resp arvados.User
376         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
377         return resp, err
378 }
379 func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
380         ep := arvados.EndpointUserList
381         var resp arvados.UserList
382         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
383         return resp, err
384 }
385 func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
386         ep := arvados.EndpointUserDelete
387         var resp arvados.User
388         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
389         return resp, err
390 }
391
392 func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
393         ep := arvados.EndpointAPIClientAuthorizationCurrent
394         var resp arvados.APIClientAuthorization
395         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
396         return resp, err
397 }
398
399 type UserSessionAuthInfo struct {
400         Email           string   `json:"email"`
401         AlternateEmails []string `json:"alternate_emails"`
402         FirstName       string   `json:"first_name"`
403         LastName        string   `json:"last_name"`
404         Username        string   `json:"username"`
405 }
406
407 type UserSessionCreateOptions struct {
408         AuthInfo UserSessionAuthInfo `json:"auth_info"`
409         ReturnTo string              `json:"return_to"`
410 }
411
412 func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCreateOptions) (arvados.LoginResponse, error) {
413         ep := arvados.APIEndpoint{Method: "POST", Path: "auth/controller/callback"}
414         var resp arvados.LoginResponse
415         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
416         return resp, err
417 }
418
419 func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
420         ep := arvados.APIEndpoint{Method: "PATCH", Path: "arvados/v1/users/batch_update"}
421         var resp arvados.UserList
422         err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
423         return resp, err
424 }