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