17119: merge SharedOptions into ListOptions, which now gets an extra
[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.EndpointContainerRequestCreate,
173                         func() interface{} { return &arvados.CreateOptions{} },
174                         func(ctx context.Context, opts interface{}) (interface{}, error) {
175                                 return rtr.backend.ContainerRequestCreate(ctx, *opts.(*arvados.CreateOptions))
176                         },
177                 },
178                 {
179                         arvados.EndpointContainerRequestUpdate,
180                         func() interface{} { return &arvados.UpdateOptions{} },
181                         func(ctx context.Context, opts interface{}) (interface{}, error) {
182                                 return rtr.backend.ContainerRequestUpdate(ctx, *opts.(*arvados.UpdateOptions))
183                         },
184                 },
185                 {
186                         arvados.EndpointContainerRequestGet,
187                         func() interface{} { return &arvados.GetOptions{} },
188                         func(ctx context.Context, opts interface{}) (interface{}, error) {
189                                 return rtr.backend.ContainerRequestGet(ctx, *opts.(*arvados.GetOptions))
190                         },
191                 },
192                 {
193                         arvados.EndpointContainerRequestList,
194                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
195                         func(ctx context.Context, opts interface{}) (interface{}, error) {
196                                 return rtr.backend.ContainerRequestList(ctx, *opts.(*arvados.ListOptions))
197                         },
198                 },
199                 {
200                         arvados.EndpointContainerRequestDelete,
201                         func() interface{} { return &arvados.DeleteOptions{} },
202                         func(ctx context.Context, opts interface{}) (interface{}, error) {
203                                 return rtr.backend.ContainerRequestDelete(ctx, *opts.(*arvados.DeleteOptions))
204                         },
205                 },
206                 {
207                         arvados.EndpointContainerLock,
208                         func() interface{} {
209                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
210                         },
211                         func(ctx context.Context, opts interface{}) (interface{}, error) {
212                                 return rtr.backend.ContainerLock(ctx, *opts.(*arvados.GetOptions))
213                         },
214                 },
215                 {
216                         arvados.EndpointContainerUnlock,
217                         func() interface{} {
218                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
219                         },
220                         func(ctx context.Context, opts interface{}) (interface{}, error) {
221                                 return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
222                         },
223                 },
224                 {
225                         arvados.EndpointContainerSSH,
226                         func() interface{} { return &arvados.ContainerSSHOptions{} },
227                         func(ctx context.Context, opts interface{}) (interface{}, error) {
228                                 return rtr.backend.ContainerSSH(ctx, *opts.(*arvados.ContainerSSHOptions))
229                         },
230                 },
231                 {
232                         arvados.EndpointGroupCreate,
233                         func() interface{} { return &arvados.CreateOptions{} },
234                         func(ctx context.Context, opts interface{}) (interface{}, error) {
235                                 return rtr.backend.GroupCreate(ctx, *opts.(*arvados.CreateOptions))
236                         },
237                 },
238                 {
239                         arvados.EndpointGroupUpdate,
240                         func() interface{} { return &arvados.UpdateOptions{} },
241                         func(ctx context.Context, opts interface{}) (interface{}, error) {
242                                 return rtr.backend.GroupUpdate(ctx, *opts.(*arvados.UpdateOptions))
243                         },
244                 },
245                 {
246                         arvados.EndpointGroupList,
247                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
248                         func(ctx context.Context, opts interface{}) (interface{}, error) {
249                                 return rtr.backend.GroupList(ctx, *opts.(*arvados.ListOptions))
250                         },
251                 },
252                 {
253                         arvados.EndpointGroupContents,
254                         func() interface{} { return &arvados.ContentsOptions{Limit: -1} },
255                         func(ctx context.Context, opts interface{}) (interface{}, error) {
256                                 return rtr.backend.GroupContents(ctx, *opts.(*arvados.ContentsOptions))
257                         },
258                 },
259                 {
260                         arvados.EndpointGroupContents2,
261                         func() interface{} { return &arvados.ContentsOptions{Limit: -1} },
262                         func(ctx context.Context, opts interface{}) (interface{}, error) {
263                                 return rtr.backend.GroupContents(ctx, *opts.(*arvados.ContentsOptions))
264                         },
265                 },
266                 {
267                         arvados.EndpointGroupShared,
268                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
269                         func(ctx context.Context, opts interface{}) (interface{}, error) {
270                                 return rtr.backend.GroupShared(ctx, *opts.(*arvados.ListOptions))
271                         },
272                 },
273                 {
274                         arvados.EndpointGroupGet,
275                         func() interface{} { return &arvados.GetOptions{} },
276                         func(ctx context.Context, opts interface{}) (interface{}, error) {
277                                 return rtr.backend.GroupGet(ctx, *opts.(*arvados.GetOptions))
278                         },
279                 },
280                 {
281                         arvados.EndpointGroupDelete,
282                         func() interface{} { return &arvados.DeleteOptions{} },
283                         func(ctx context.Context, opts interface{}) (interface{}, error) {
284                                 return rtr.backend.GroupDelete(ctx, *opts.(*arvados.DeleteOptions))
285                         },
286                 },
287                 {
288                         arvados.EndpointGroupUntrash,
289                         func() interface{} { return &arvados.UntrashOptions{} },
290                         func(ctx context.Context, opts interface{}) (interface{}, error) {
291                                 return rtr.backend.GroupUntrash(ctx, *opts.(*arvados.UntrashOptions))
292                         },
293                 },
294                 {
295                         arvados.EndpointSpecimenCreate,
296                         func() interface{} { return &arvados.CreateOptions{} },
297                         func(ctx context.Context, opts interface{}) (interface{}, error) {
298                                 return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
299                         },
300                 },
301                 {
302                         arvados.EndpointSpecimenUpdate,
303                         func() interface{} { return &arvados.UpdateOptions{} },
304                         func(ctx context.Context, opts interface{}) (interface{}, error) {
305                                 return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
306                         },
307                 },
308                 {
309                         arvados.EndpointSpecimenGet,
310                         func() interface{} { return &arvados.GetOptions{} },
311                         func(ctx context.Context, opts interface{}) (interface{}, error) {
312                                 return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
313                         },
314                 },
315                 {
316                         arvados.EndpointSpecimenList,
317                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
318                         func(ctx context.Context, opts interface{}) (interface{}, error) {
319                                 return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
320                         },
321                 },
322                 {
323                         arvados.EndpointSpecimenDelete,
324                         func() interface{} { return &arvados.DeleteOptions{} },
325                         func(ctx context.Context, opts interface{}) (interface{}, error) {
326                                 return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
327                         },
328                 },
329                 {
330                         arvados.EndpointUserCreate,
331                         func() interface{} { return &arvados.CreateOptions{} },
332                         func(ctx context.Context, opts interface{}) (interface{}, error) {
333                                 return rtr.backend.UserCreate(ctx, *opts.(*arvados.CreateOptions))
334                         },
335                 },
336                 {
337                         arvados.EndpointUserMerge,
338                         func() interface{} { return &arvados.UserMergeOptions{} },
339                         func(ctx context.Context, opts interface{}) (interface{}, error) {
340                                 return rtr.backend.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
341                         },
342                 },
343                 {
344                         arvados.EndpointUserActivate,
345                         func() interface{} { return &arvados.UserActivateOptions{} },
346                         func(ctx context.Context, opts interface{}) (interface{}, error) {
347                                 return rtr.backend.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
348                         },
349                 },
350                 {
351                         arvados.EndpointUserSetup,
352                         func() interface{} { return &arvados.UserSetupOptions{} },
353                         func(ctx context.Context, opts interface{}) (interface{}, error) {
354                                 return rtr.backend.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
355                         },
356                 },
357                 {
358                         arvados.EndpointUserUnsetup,
359                         func() interface{} { return &arvados.GetOptions{} },
360                         func(ctx context.Context, opts interface{}) (interface{}, error) {
361                                 return rtr.backend.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
362                         },
363                 },
364                 {
365                         arvados.EndpointUserGetCurrent,
366                         func() interface{} { return &arvados.GetOptions{} },
367                         func(ctx context.Context, opts interface{}) (interface{}, error) {
368                                 return rtr.backend.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
369                         },
370                 },
371                 {
372                         arvados.EndpointUserGetSystem,
373                         func() interface{} { return &arvados.GetOptions{} },
374                         func(ctx context.Context, opts interface{}) (interface{}, error) {
375                                 return rtr.backend.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
376                         },
377                 },
378                 {
379                         arvados.EndpointUserGet,
380                         func() interface{} { return &arvados.GetOptions{} },
381                         func(ctx context.Context, opts interface{}) (interface{}, error) {
382                                 return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
383                         },
384                 },
385                 {
386                         arvados.EndpointUserUpdateUUID,
387                         func() interface{} { return &arvados.UpdateUUIDOptions{} },
388                         func(ctx context.Context, opts interface{}) (interface{}, error) {
389                                 return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
390                         },
391                 },
392                 {
393                         arvados.EndpointUserUpdate,
394                         func() interface{} { return &arvados.UpdateOptions{} },
395                         func(ctx context.Context, opts interface{}) (interface{}, error) {
396                                 return rtr.backend.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
397                         },
398                 },
399                 {
400                         arvados.EndpointUserList,
401                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
402                         func(ctx context.Context, opts interface{}) (interface{}, error) {
403                                 return rtr.backend.UserList(ctx, *opts.(*arvados.ListOptions))
404                         },
405                 },
406                 {
407                         arvados.EndpointUserBatchUpdate,
408                         func() interface{} { return &arvados.UserBatchUpdateOptions{} },
409                         func(ctx context.Context, opts interface{}) (interface{}, error) {
410                                 return rtr.backend.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
411                         },
412                 },
413                 {
414                         arvados.EndpointUserDelete,
415                         func() interface{} { return &arvados.DeleteOptions{} },
416                         func(ctx context.Context, opts interface{}) (interface{}, error) {
417                                 return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
418                         },
419                 },
420                 {
421                         arvados.EndpointUserAuthenticate,
422                         func() interface{} { return &arvados.UserAuthenticateOptions{} },
423                         func(ctx context.Context, opts interface{}) (interface{}, error) {
424                                 return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
425                         },
426                 },
427         } {
428                 exec := route.exec
429                 if rtr.wrapCalls != nil {
430                         exec = rtr.wrapCalls(exec)
431                 }
432                 rtr.addRoute(route.endpoint, route.defaultOpts, exec)
433         }
434         rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
435                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
436         })
437         rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
438                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
439         })
440 }
441
442 var altMethod = map[string]string{
443         "PATCH": "PUT",  // Accept PUT as a synonym for PATCH
444         "GET":   "HEAD", // Accept HEAD at any GET route
445 }
446
447 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec api.RoutableFunc) {
448         methods := []string{endpoint.Method}
449         if alt, ok := altMethod[endpoint.Method]; ok {
450                 methods = append(methods, alt)
451         }
452         rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
453                 logger := ctxlog.FromContext(req.Context())
454                 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
455                 if err != nil {
456                         logger.WithFields(logrus.Fields{
457                                 "req":      req,
458                                 "method":   endpoint.Method,
459                                 "endpoint": endpoint,
460                         }).WithError(err).Debug("error loading request params")
461                         rtr.sendError(w, err)
462                         return
463                 }
464                 opts := defaultOpts()
465                 err = rtr.transcode(params, opts)
466                 if err != nil {
467                         logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
468                         rtr.sendError(w, err)
469                         return
470                 }
471                 respOpts, err := rtr.responseOptions(opts)
472                 if err != nil {
473                         logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
474                         rtr.sendError(w, err)
475                         return
476                 }
477
478                 creds := auth.CredentialsFromRequest(req)
479                 err = creds.LoadTokensFromHTTPRequestBody(req)
480                 if err != nil {
481                         rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
482                         return
483                 }
484                 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
485                         for _, t := range rt {
486                                 if t, ok := t.(string); ok {
487                                         creds.Tokens = append(creds.Tokens, t)
488                                 }
489                         }
490                 }
491                 ctx := auth.NewContext(req.Context(), creds)
492                 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
493                 logger.WithFields(logrus.Fields{
494                         "apiEndpoint": endpoint,
495                         "apiOptsType": fmt.Sprintf("%T", opts),
496                         "apiOpts":     opts,
497                 }).Debug("exec")
498                 resp, err := exec(ctx, opts)
499                 if err != nil {
500                         logger.WithError(err).Debugf("returning error type %T", err)
501                         rtr.sendError(w, err)
502                         return
503                 }
504                 rtr.sendResponse(w, req, resp, respOpts)
505         })
506 }
507
508 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
509         switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
510         case "login", "logout", "auth":
511         default:
512                 w.Header().Set("Access-Control-Allow-Origin", "*")
513                 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
514                 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Http-Method-Override")
515                 w.Header().Set("Access-Control-Max-Age", "86486400")
516         }
517         if r.Method == "OPTIONS" {
518                 return
519         }
520         if r.Method == "POST" {
521                 r.ParseForm()
522                 if m := r.FormValue("_method"); m != "" {
523                         r2 := *r
524                         r = &r2
525                         r.Method = m
526                 } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
527                         r2 := *r
528                         r = &r2
529                         r.Method = m
530                 }
531         }
532         rtr.mux.ServeHTTP(w, r)
533 }