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