Merge branch '14813-config-cors'
[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.curoverse.com/arvados.git/sdk/go/arvados"
14         "git.curoverse.com/arvados.git/sdk/go/auth"
15         "git.curoverse.com/arvados.git/sdk/go/ctxlog"
16         "git.curoverse.com/arvados.git/sdk/go/httpserver"
17         "github.com/julienschmidt/httprouter"
18         "github.com/sirupsen/logrus"
19 )
20
21 type router struct {
22         mux *httprouter.Router
23         fed arvados.API
24 }
25
26 func New(fed arvados.API) *router {
27         rtr := &router{
28                 mux: httprouter.New(),
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.EndpointCollectionCreate,
52                         func() interface{} { return &arvados.CreateOptions{} },
53                         func(ctx context.Context, opts interface{}) (interface{}, error) {
54                                 return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
55                         },
56                 },
57                 {
58                         arvados.EndpointCollectionUpdate,
59                         func() interface{} { return &arvados.UpdateOptions{} },
60                         func(ctx context.Context, opts interface{}) (interface{}, error) {
61                                 return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
62                         },
63                 },
64                 {
65                         arvados.EndpointCollectionGet,
66                         func() interface{} { return &arvados.GetOptions{} },
67                         func(ctx context.Context, opts interface{}) (interface{}, error) {
68                                 return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
69                         },
70                 },
71                 {
72                         arvados.EndpointCollectionList,
73                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
74                         func(ctx context.Context, opts interface{}) (interface{}, error) {
75                                 return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
76                         },
77                 },
78                 {
79                         arvados.EndpointCollectionProvenance,
80                         func() interface{} { return &arvados.GetOptions{} },
81                         func(ctx context.Context, opts interface{}) (interface{}, error) {
82                                 return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
83                         },
84                 },
85                 {
86                         arvados.EndpointCollectionUsedBy,
87                         func() interface{} { return &arvados.GetOptions{} },
88                         func(ctx context.Context, opts interface{}) (interface{}, error) {
89                                 return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
90                         },
91                 },
92                 {
93                         arvados.EndpointCollectionDelete,
94                         func() interface{} { return &arvados.DeleteOptions{} },
95                         func(ctx context.Context, opts interface{}) (interface{}, error) {
96                                 return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
97                         },
98                 },
99                 {
100                         arvados.EndpointCollectionTrash,
101                         func() interface{} { return &arvados.DeleteOptions{} },
102                         func(ctx context.Context, opts interface{}) (interface{}, error) {
103                                 return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
104                         },
105                 },
106                 {
107                         arvados.EndpointCollectionUntrash,
108                         func() interface{} { return &arvados.UntrashOptions{} },
109                         func(ctx context.Context, opts interface{}) (interface{}, error) {
110                                 return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
111                         },
112                 },
113                 {
114                         arvados.EndpointContainerCreate,
115                         func() interface{} { return &arvados.CreateOptions{} },
116                         func(ctx context.Context, opts interface{}) (interface{}, error) {
117                                 return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
118                         },
119                 },
120                 {
121                         arvados.EndpointContainerUpdate,
122                         func() interface{} { return &arvados.UpdateOptions{} },
123                         func(ctx context.Context, opts interface{}) (interface{}, error) {
124                                 return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
125                         },
126                 },
127                 {
128                         arvados.EndpointContainerGet,
129                         func() interface{} { return &arvados.GetOptions{} },
130                         func(ctx context.Context, opts interface{}) (interface{}, error) {
131                                 return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
132                         },
133                 },
134                 {
135                         arvados.EndpointContainerList,
136                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
137                         func(ctx context.Context, opts interface{}) (interface{}, error) {
138                                 return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
139                         },
140                 },
141                 {
142                         arvados.EndpointContainerDelete,
143                         func() interface{} { return &arvados.DeleteOptions{} },
144                         func(ctx context.Context, opts interface{}) (interface{}, error) {
145                                 return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
146                         },
147                 },
148                 {
149                         arvados.EndpointContainerLock,
150                         func() interface{} {
151                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
152                         },
153                         func(ctx context.Context, opts interface{}) (interface{}, error) {
154                                 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
155                         },
156                 },
157                 {
158                         arvados.EndpointContainerUnlock,
159                         func() interface{} {
160                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
161                         },
162                         func(ctx context.Context, opts interface{}) (interface{}, error) {
163                                 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
164                         },
165                 },
166                 {
167                         arvados.EndpointSpecimenCreate,
168                         func() interface{} { return &arvados.CreateOptions{} },
169                         func(ctx context.Context, opts interface{}) (interface{}, error) {
170                                 return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
171                         },
172                 },
173                 {
174                         arvados.EndpointSpecimenUpdate,
175                         func() interface{} { return &arvados.UpdateOptions{} },
176                         func(ctx context.Context, opts interface{}) (interface{}, error) {
177                                 return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
178                         },
179                 },
180                 {
181                         arvados.EndpointSpecimenGet,
182                         func() interface{} { return &arvados.GetOptions{} },
183                         func(ctx context.Context, opts interface{}) (interface{}, error) {
184                                 return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
185                         },
186                 },
187                 {
188                         arvados.EndpointSpecimenList,
189                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
190                         func(ctx context.Context, opts interface{}) (interface{}, error) {
191                                 return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
192                         },
193                 },
194                 {
195                         arvados.EndpointSpecimenDelete,
196                         func() interface{} { return &arvados.DeleteOptions{} },
197                         func(ctx context.Context, opts interface{}) (interface{}, error) {
198                                 return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
199                         },
200                 },
201         } {
202                 rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
203                 if route.endpoint.Method == "PATCH" {
204                         // Accept PUT as a synonym for PATCH.
205                         endpointPUT := route.endpoint
206                         endpointPUT.Method = "PUT"
207                         rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
208                 }
209         }
210         rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
211                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
212         })
213         rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
214                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
215         })
216 }
217
218 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
219         rtr.mux.HandlerFunc(endpoint.Method, "/"+endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
220                 logger := ctxlog.FromContext(req.Context())
221                 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
222                 if err != nil {
223                         logger.WithFields(logrus.Fields{
224                                 "req":      req,
225                                 "method":   endpoint.Method,
226                                 "endpoint": endpoint,
227                         }).WithError(err).Debug("error loading request params")
228                         rtr.sendError(w, err)
229                         return
230                 }
231                 opts := defaultOpts()
232                 err = rtr.transcode(params, opts)
233                 if err != nil {
234                         logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
235                         rtr.sendError(w, err)
236                         return
237                 }
238                 respOpts, err := rtr.responseOptions(opts)
239                 if err != nil {
240                         logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
241                         rtr.sendError(w, err)
242                         return
243                 }
244
245                 creds := auth.CredentialsFromRequest(req)
246                 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
247                         for _, t := range rt {
248                                 if t, ok := t.(string); ok {
249                                         creds.Tokens = append(creds.Tokens, t)
250                                 }
251                         }
252                 }
253                 ctx := auth.NewContext(req.Context(), creds)
254                 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
255                 logger.WithFields(logrus.Fields{
256                         "apiEndpoint": endpoint,
257                         "apiOptsType": fmt.Sprintf("%T", opts),
258                         "apiOpts":     opts,
259                 }).Debug("exec")
260                 resp, err := exec(ctx, opts)
261                 if err != nil {
262                         logger.WithError(err).Debugf("returning error type %T", err)
263                         rtr.sendError(w, err)
264                         return
265                 }
266                 rtr.sendResponse(w, resp, respOpts)
267         })
268 }
269
270 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
271         switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
272         case "login", "logout", "auth":
273         default:
274                 w.Header().Set("Access-Control-Allow-Origin", "*")
275                 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
276                 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
277                 w.Header().Set("Access-Control-Max-Age", "86486400")
278         }
279         if r.Method == "OPTIONS" {
280                 return
281         }
282         r.ParseForm()
283         if m := r.FormValue("_method"); m != "" {
284                 r2 := *r
285                 r = &r2
286                 r.Method = m
287         }
288         rtr.mux.ServeHTTP(w, r)
289 }