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