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