17170: Merge branch 'master'
[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.EndpointSpecimenCreate,
233                         func() interface{} { return &arvados.CreateOptions{} },
234                         func(ctx context.Context, opts interface{}) (interface{}, error) {
235                                 return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
236                         },
237                 },
238                 {
239                         arvados.EndpointSpecimenUpdate,
240                         func() interface{} { return &arvados.UpdateOptions{} },
241                         func(ctx context.Context, opts interface{}) (interface{}, error) {
242                                 return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
243                         },
244                 },
245                 {
246                         arvados.EndpointSpecimenGet,
247                         func() interface{} { return &arvados.GetOptions{} },
248                         func(ctx context.Context, opts interface{}) (interface{}, error) {
249                                 return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
250                         },
251                 },
252                 {
253                         arvados.EndpointSpecimenList,
254                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
255                         func(ctx context.Context, opts interface{}) (interface{}, error) {
256                                 return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
257                         },
258                 },
259                 {
260                         arvados.EndpointSpecimenDelete,
261                         func() interface{} { return &arvados.DeleteOptions{} },
262                         func(ctx context.Context, opts interface{}) (interface{}, error) {
263                                 return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
264                         },
265                 },
266                 {
267                         arvados.EndpointUserCreate,
268                         func() interface{} { return &arvados.CreateOptions{} },
269                         func(ctx context.Context, opts interface{}) (interface{}, error) {
270                                 return rtr.backend.UserCreate(ctx, *opts.(*arvados.CreateOptions))
271                         },
272                 },
273                 {
274                         arvados.EndpointUserMerge,
275                         func() interface{} { return &arvados.UserMergeOptions{} },
276                         func(ctx context.Context, opts interface{}) (interface{}, error) {
277                                 return rtr.backend.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
278                         },
279                 },
280                 {
281                         arvados.EndpointUserActivate,
282                         func() interface{} { return &arvados.UserActivateOptions{} },
283                         func(ctx context.Context, opts interface{}) (interface{}, error) {
284                                 return rtr.backend.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
285                         },
286                 },
287                 {
288                         arvados.EndpointUserSetup,
289                         func() interface{} { return &arvados.UserSetupOptions{} },
290                         func(ctx context.Context, opts interface{}) (interface{}, error) {
291                                 return rtr.backend.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
292                         },
293                 },
294                 {
295                         arvados.EndpointUserUnsetup,
296                         func() interface{} { return &arvados.GetOptions{} },
297                         func(ctx context.Context, opts interface{}) (interface{}, error) {
298                                 return rtr.backend.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
299                         },
300                 },
301                 {
302                         arvados.EndpointUserGetCurrent,
303                         func() interface{} { return &arvados.GetOptions{} },
304                         func(ctx context.Context, opts interface{}) (interface{}, error) {
305                                 return rtr.backend.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
306                         },
307                 },
308                 {
309                         arvados.EndpointUserGetSystem,
310                         func() interface{} { return &arvados.GetOptions{} },
311                         func(ctx context.Context, opts interface{}) (interface{}, error) {
312                                 return rtr.backend.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
313                         },
314                 },
315                 {
316                         arvados.EndpointUserGet,
317                         func() interface{} { return &arvados.GetOptions{} },
318                         func(ctx context.Context, opts interface{}) (interface{}, error) {
319                                 return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
320                         },
321                 },
322                 {
323                         arvados.EndpointUserUpdateUUID,
324                         func() interface{} { return &arvados.UpdateUUIDOptions{} },
325                         func(ctx context.Context, opts interface{}) (interface{}, error) {
326                                 return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
327                         },
328                 },
329                 {
330                         arvados.EndpointUserUpdate,
331                         func() interface{} { return &arvados.UpdateOptions{} },
332                         func(ctx context.Context, opts interface{}) (interface{}, error) {
333                                 return rtr.backend.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
334                         },
335                 },
336                 {
337                         arvados.EndpointUserList,
338                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
339                         func(ctx context.Context, opts interface{}) (interface{}, error) {
340                                 return rtr.backend.UserList(ctx, *opts.(*arvados.ListOptions))
341                         },
342                 },
343                 {
344                         arvados.EndpointUserBatchUpdate,
345                         func() interface{} { return &arvados.UserBatchUpdateOptions{} },
346                         func(ctx context.Context, opts interface{}) (interface{}, error) {
347                                 return rtr.backend.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
348                         },
349                 },
350                 {
351                         arvados.EndpointUserDelete,
352                         func() interface{} { return &arvados.DeleteOptions{} },
353                         func(ctx context.Context, opts interface{}) (interface{}, error) {
354                                 return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
355                         },
356                 },
357                 {
358                         arvados.EndpointUserAuthenticate,
359                         func() interface{} { return &arvados.UserAuthenticateOptions{} },
360                         func(ctx context.Context, opts interface{}) (interface{}, error) {
361                                 return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
362                         },
363                 },
364         } {
365                 exec := route.exec
366                 if rtr.wrapCalls != nil {
367                         exec = rtr.wrapCalls(exec)
368                 }
369                 rtr.addRoute(route.endpoint, route.defaultOpts, exec)
370         }
371         rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
372                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
373         })
374         rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
375                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
376         })
377 }
378
379 var altMethod = map[string]string{
380         "PATCH": "PUT",  // Accept PUT as a synonym for PATCH
381         "GET":   "HEAD", // Accept HEAD at any GET route
382 }
383
384 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec api.RoutableFunc) {
385         methods := []string{endpoint.Method}
386         if alt, ok := altMethod[endpoint.Method]; ok {
387                 methods = append(methods, alt)
388         }
389         rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
390                 logger := ctxlog.FromContext(req.Context())
391                 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
392                 if err != nil {
393                         logger.WithFields(logrus.Fields{
394                                 "req":      req,
395                                 "method":   endpoint.Method,
396                                 "endpoint": endpoint,
397                         }).WithError(err).Debug("error loading request params")
398                         rtr.sendError(w, err)
399                         return
400                 }
401                 opts := defaultOpts()
402                 err = rtr.transcode(params, opts)
403                 if err != nil {
404                         logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
405                         rtr.sendError(w, err)
406                         return
407                 }
408                 respOpts, err := rtr.responseOptions(opts)
409                 if err != nil {
410                         logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
411                         rtr.sendError(w, err)
412                         return
413                 }
414
415                 creds := auth.CredentialsFromRequest(req)
416                 err = creds.LoadTokensFromHTTPRequestBody(req)
417                 if err != nil {
418                         rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
419                         return
420                 }
421                 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
422                         for _, t := range rt {
423                                 if t, ok := t.(string); ok {
424                                         creds.Tokens = append(creds.Tokens, t)
425                                 }
426                         }
427                 }
428                 ctx := auth.NewContext(req.Context(), creds)
429                 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
430                 logger.WithFields(logrus.Fields{
431                         "apiEndpoint": endpoint,
432                         "apiOptsType": fmt.Sprintf("%T", opts),
433                         "apiOpts":     opts,
434                 }).Debug("exec")
435                 resp, err := exec(ctx, opts)
436                 if err != nil {
437                         logger.WithError(err).Debugf("returning error type %T", err)
438                         rtr.sendError(w, err)
439                         return
440                 }
441                 rtr.sendResponse(w, req, resp, respOpts)
442         })
443 }
444
445 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
446         switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
447         case "login", "logout", "auth":
448         default:
449                 w.Header().Set("Access-Control-Allow-Origin", "*")
450                 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
451                 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Http-Method-Override")
452                 w.Header().Set("Access-Control-Max-Age", "86486400")
453         }
454         if r.Method == "OPTIONS" {
455                 return
456         }
457         if r.Method == "POST" {
458                 r.ParseForm()
459                 if m := r.FormValue("_method"); m != "" {
460                         r2 := *r
461                         r = &r2
462                         r.Method = m
463                 } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
464                         r2 := *r
465                         r = &r2
466                         r.Method = m
467                 }
468         }
469         rtr.mux.ServeHTTP(w, r)
470 }