17170: Add "arvados-client shell" subcommand and backend support.
[arvados.git] / lib / controller / router / router.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package router
6
7 import (
8         "context"
9         "fmt"
10         "net/http"
11         "strings"
12
13         "git.arvados.org/arvados.git/lib/controller/api"
14         "git.arvados.org/arvados.git/sdk/go/arvados"
15         "git.arvados.org/arvados.git/sdk/go/auth"
16         "git.arvados.org/arvados.git/sdk/go/ctxlog"
17         "git.arvados.org/arvados.git/sdk/go/httpserver"
18         "github.com/gorilla/mux"
19         "github.com/sirupsen/logrus"
20 )
21
22 type router struct {
23         mux       *mux.Router
24         backend   arvados.API
25         wrapCalls func(api.RoutableFunc) api.RoutableFunc
26 }
27
28 // New returns a new router (which implements the http.Handler
29 // interface) that serves requests by calling Arvados API methods on
30 // the given backend.
31 //
32 // If wrapCalls is not nil, it is called once for each API method, and
33 // the returned method is used in its place. This can be used to
34 // install hooks before and after each API call and alter responses;
35 // see localdb.WrapCallsInTransaction for an example.
36 func New(backend arvados.API, wrapCalls func(api.RoutableFunc) api.RoutableFunc) *router {
37         rtr := &router{
38                 mux:       mux.NewRouter(),
39                 backend:   backend,
40                 wrapCalls: wrapCalls,
41         }
42         rtr.addRoutes()
43         return rtr
44 }
45
46 func (rtr *router) addRoutes() {
47         for _, route := range []struct {
48                 endpoint    arvados.APIEndpoint
49                 defaultOpts func() interface{}
50                 exec        api.RoutableFunc
51         }{
52                 {
53                         arvados.EndpointConfigGet,
54                         func() interface{} { return &struct{}{} },
55                         func(ctx context.Context, opts interface{}) (interface{}, error) {
56                                 return rtr.backend.ConfigGet(ctx)
57                         },
58                 },
59                 {
60                         arvados.EndpointLogin,
61                         func() interface{} { return &arvados.LoginOptions{} },
62                         func(ctx context.Context, opts interface{}) (interface{}, error) {
63                                 return rtr.backend.Login(ctx, *opts.(*arvados.LoginOptions))
64                         },
65                 },
66                 {
67                         arvados.EndpointLogout,
68                         func() interface{} { return &arvados.LogoutOptions{} },
69                         func(ctx context.Context, opts interface{}) (interface{}, error) {
70                                 return rtr.backend.Logout(ctx, *opts.(*arvados.LogoutOptions))
71                         },
72                 },
73                 {
74                         arvados.EndpointCollectionCreate,
75                         func() interface{} { return &arvados.CreateOptions{} },
76                         func(ctx context.Context, opts interface{}) (interface{}, error) {
77                                 return rtr.backend.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
78                         },
79                 },
80                 {
81                         arvados.EndpointCollectionUpdate,
82                         func() interface{} { return &arvados.UpdateOptions{} },
83                         func(ctx context.Context, opts interface{}) (interface{}, error) {
84                                 return rtr.backend.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
85                         },
86                 },
87                 {
88                         arvados.EndpointCollectionGet,
89                         func() interface{} { return &arvados.GetOptions{} },
90                         func(ctx context.Context, opts interface{}) (interface{}, error) {
91                                 return rtr.backend.CollectionGet(ctx, *opts.(*arvados.GetOptions))
92                         },
93                 },
94                 {
95                         arvados.EndpointCollectionList,
96                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
97                         func(ctx context.Context, opts interface{}) (interface{}, error) {
98                                 return rtr.backend.CollectionList(ctx, *opts.(*arvados.ListOptions))
99                         },
100                 },
101                 {
102                         arvados.EndpointCollectionProvenance,
103                         func() interface{} { return &arvados.GetOptions{} },
104                         func(ctx context.Context, opts interface{}) (interface{}, error) {
105                                 return rtr.backend.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
106                         },
107                 },
108                 {
109                         arvados.EndpointCollectionUsedBy,
110                         func() interface{} { return &arvados.GetOptions{} },
111                         func(ctx context.Context, opts interface{}) (interface{}, error) {
112                                 return rtr.backend.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
113                         },
114                 },
115                 {
116                         arvados.EndpointCollectionDelete,
117                         func() interface{} { return &arvados.DeleteOptions{} },
118                         func(ctx context.Context, opts interface{}) (interface{}, error) {
119                                 return rtr.backend.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
120                         },
121                 },
122                 {
123                         arvados.EndpointCollectionTrash,
124                         func() interface{} { return &arvados.DeleteOptions{} },
125                         func(ctx context.Context, opts interface{}) (interface{}, error) {
126                                 return rtr.backend.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
127                         },
128                 },
129                 {
130                         arvados.EndpointCollectionUntrash,
131                         func() interface{} { return &arvados.UntrashOptions{} },
132                         func(ctx context.Context, opts interface{}) (interface{}, error) {
133                                 return rtr.backend.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
134                         },
135                 },
136                 {
137                         arvados.EndpointContainerCreate,
138                         func() interface{} { return &arvados.CreateOptions{} },
139                         func(ctx context.Context, opts interface{}) (interface{}, error) {
140                                 return rtr.backend.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
141                         },
142                 },
143                 {
144                         arvados.EndpointContainerUpdate,
145                         func() interface{} { return &arvados.UpdateOptions{} },
146                         func(ctx context.Context, opts interface{}) (interface{}, error) {
147                                 return rtr.backend.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
148                         },
149                 },
150                 {
151                         arvados.EndpointContainerGet,
152                         func() interface{} { return &arvados.GetOptions{} },
153                         func(ctx context.Context, opts interface{}) (interface{}, error) {
154                                 return rtr.backend.ContainerGet(ctx, *opts.(*arvados.GetOptions))
155                         },
156                 },
157                 {
158                         arvados.EndpointContainerList,
159                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
160                         func(ctx context.Context, opts interface{}) (interface{}, error) {
161                                 return rtr.backend.ContainerList(ctx, *opts.(*arvados.ListOptions))
162                         },
163                 },
164                 {
165                         arvados.EndpointContainerDelete,
166                         func() interface{} { return &arvados.DeleteOptions{} },
167                         func(ctx context.Context, opts interface{}) (interface{}, error) {
168                                 return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
169                         },
170                 },
171                 {
172                         arvados.EndpointContainerLock,
173                         func() interface{} {
174                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
175                         },
176                         func(ctx context.Context, opts interface{}) (interface{}, error) {
177                                 return rtr.backend.ContainerLock(ctx, *opts.(*arvados.GetOptions))
178                         },
179                 },
180                 {
181                         arvados.EndpointContainerUnlock,
182                         func() interface{} {
183                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
184                         },
185                         func(ctx context.Context, opts interface{}) (interface{}, error) {
186                                 return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
187                         },
188                 },
189                 {
190                         arvados.EndpointContainerSSH,
191                         func() interface{} { return &arvados.ContainerSSHOptions{} },
192                         func(ctx context.Context, opts interface{}) (interface{}, error) {
193                                 return rtr.backend.ContainerSSH(ctx, *opts.(*arvados.ContainerSSHOptions))
194                         },
195                 },
196                 {
197                         arvados.EndpointSpecimenCreate,
198                         func() interface{} { return &arvados.CreateOptions{} },
199                         func(ctx context.Context, opts interface{}) (interface{}, error) {
200                                 return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
201                         },
202                 },
203                 {
204                         arvados.EndpointSpecimenUpdate,
205                         func() interface{} { return &arvados.UpdateOptions{} },
206                         func(ctx context.Context, opts interface{}) (interface{}, error) {
207                                 return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
208                         },
209                 },
210                 {
211                         arvados.EndpointSpecimenGet,
212                         func() interface{} { return &arvados.GetOptions{} },
213                         func(ctx context.Context, opts interface{}) (interface{}, error) {
214                                 return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
215                         },
216                 },
217                 {
218                         arvados.EndpointSpecimenList,
219                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
220                         func(ctx context.Context, opts interface{}) (interface{}, error) {
221                                 return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
222                         },
223                 },
224                 {
225                         arvados.EndpointSpecimenDelete,
226                         func() interface{} { return &arvados.DeleteOptions{} },
227                         func(ctx context.Context, opts interface{}) (interface{}, error) {
228                                 return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
229                         },
230                 },
231                 {
232                         arvados.EndpointUserCreate,
233                         func() interface{} { return &arvados.CreateOptions{} },
234                         func(ctx context.Context, opts interface{}) (interface{}, error) {
235                                 return rtr.backend.UserCreate(ctx, *opts.(*arvados.CreateOptions))
236                         },
237                 },
238                 {
239                         arvados.EndpointUserMerge,
240                         func() interface{} { return &arvados.UserMergeOptions{} },
241                         func(ctx context.Context, opts interface{}) (interface{}, error) {
242                                 return rtr.backend.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
243                         },
244                 },
245                 {
246                         arvados.EndpointUserActivate,
247                         func() interface{} { return &arvados.UserActivateOptions{} },
248                         func(ctx context.Context, opts interface{}) (interface{}, error) {
249                                 return rtr.backend.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
250                         },
251                 },
252                 {
253                         arvados.EndpointUserSetup,
254                         func() interface{} { return &arvados.UserSetupOptions{} },
255                         func(ctx context.Context, opts interface{}) (interface{}, error) {
256                                 return rtr.backend.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
257                         },
258                 },
259                 {
260                         arvados.EndpointUserUnsetup,
261                         func() interface{} { return &arvados.GetOptions{} },
262                         func(ctx context.Context, opts interface{}) (interface{}, error) {
263                                 return rtr.backend.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
264                         },
265                 },
266                 {
267                         arvados.EndpointUserGetCurrent,
268                         func() interface{} { return &arvados.GetOptions{} },
269                         func(ctx context.Context, opts interface{}) (interface{}, error) {
270                                 return rtr.backend.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
271                         },
272                 },
273                 {
274                         arvados.EndpointUserGetSystem,
275                         func() interface{} { return &arvados.GetOptions{} },
276                         func(ctx context.Context, opts interface{}) (interface{}, error) {
277                                 return rtr.backend.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
278                         },
279                 },
280                 {
281                         arvados.EndpointUserGet,
282                         func() interface{} { return &arvados.GetOptions{} },
283                         func(ctx context.Context, opts interface{}) (interface{}, error) {
284                                 return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
285                         },
286                 },
287                 {
288                         arvados.EndpointUserUpdateUUID,
289                         func() interface{} { return &arvados.UpdateUUIDOptions{} },
290                         func(ctx context.Context, opts interface{}) (interface{}, error) {
291                                 return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
292                         },
293                 },
294                 {
295                         arvados.EndpointUserUpdate,
296                         func() interface{} { return &arvados.UpdateOptions{} },
297                         func(ctx context.Context, opts interface{}) (interface{}, error) {
298                                 return rtr.backend.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
299                         },
300                 },
301                 {
302                         arvados.EndpointUserList,
303                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
304                         func(ctx context.Context, opts interface{}) (interface{}, error) {
305                                 return rtr.backend.UserList(ctx, *opts.(*arvados.ListOptions))
306                         },
307                 },
308                 {
309                         arvados.EndpointUserBatchUpdate,
310                         func() interface{} { return &arvados.UserBatchUpdateOptions{} },
311                         func(ctx context.Context, opts interface{}) (interface{}, error) {
312                                 return rtr.backend.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
313                         },
314                 },
315                 {
316                         arvados.EndpointUserDelete,
317                         func() interface{} { return &arvados.DeleteOptions{} },
318                         func(ctx context.Context, opts interface{}) (interface{}, error) {
319                                 return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
320                         },
321                 },
322                 {
323                         arvados.EndpointUserAuthenticate,
324                         func() interface{} { return &arvados.UserAuthenticateOptions{} },
325                         func(ctx context.Context, opts interface{}) (interface{}, error) {
326                                 return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
327                         },
328                 },
329         } {
330                 exec := route.exec
331                 if rtr.wrapCalls != nil {
332                         exec = rtr.wrapCalls(exec)
333                 }
334                 rtr.addRoute(route.endpoint, route.defaultOpts, exec)
335         }
336         rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
337                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
338         })
339         rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
340                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
341         })
342 }
343
344 var altMethod = map[string]string{
345         "PATCH": "PUT",  // Accept PUT as a synonym for PATCH
346         "GET":   "HEAD", // Accept HEAD at any GET route
347 }
348
349 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec api.RoutableFunc) {
350         methods := []string{endpoint.Method}
351         if alt, ok := altMethod[endpoint.Method]; ok {
352                 methods = append(methods, alt)
353         }
354         rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
355                 logger := ctxlog.FromContext(req.Context())
356                 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
357                 if err != nil {
358                         logger.WithFields(logrus.Fields{
359                                 "req":      req,
360                                 "method":   endpoint.Method,
361                                 "endpoint": endpoint,
362                         }).WithError(err).Debug("error loading request params")
363                         rtr.sendError(w, err)
364                         return
365                 }
366                 opts := defaultOpts()
367                 err = rtr.transcode(params, opts)
368                 if err != nil {
369                         logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
370                         rtr.sendError(w, err)
371                         return
372                 }
373                 respOpts, err := rtr.responseOptions(opts)
374                 if err != nil {
375                         logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
376                         rtr.sendError(w, err)
377                         return
378                 }
379
380                 creds := auth.CredentialsFromRequest(req)
381                 err = creds.LoadTokensFromHTTPRequestBody(req)
382                 if err != nil {
383                         rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
384                         return
385                 }
386                 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
387                         for _, t := range rt {
388                                 if t, ok := t.(string); ok {
389                                         creds.Tokens = append(creds.Tokens, t)
390                                 }
391                         }
392                 }
393                 ctx := auth.NewContext(req.Context(), creds)
394                 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
395                 logger.WithFields(logrus.Fields{
396                         "apiEndpoint": endpoint,
397                         "apiOptsType": fmt.Sprintf("%T", opts),
398                         "apiOpts":     opts,
399                 }).Debug("exec")
400                 resp, err := exec(ctx, opts)
401                 if err != nil {
402                         logger.WithError(err).Debugf("returning error type %T", err)
403                         rtr.sendError(w, err)
404                         return
405                 }
406                 rtr.sendResponse(w, req, resp, respOpts)
407         })
408 }
409
410 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
411         switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
412         case "login", "logout", "auth":
413         default:
414                 w.Header().Set("Access-Control-Allow-Origin", "*")
415                 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
416                 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Http-Method-Override")
417                 w.Header().Set("Access-Control-Max-Age", "86486400")
418         }
419         if r.Method == "OPTIONS" {
420                 return
421         }
422         if r.Method == "POST" {
423                 r.ParseForm()
424                 if m := r.FormValue("_method"); m != "" {
425                         r2 := *r
426                         r = &r2
427                         r.Method = m
428                 } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
429                         r2 := *r
430                         r = &r2
431                         r.Method = m
432                 }
433         }
434         rtr.mux.ServeHTTP(w, r)
435 }