14287: Handle CORS OPTIONS requests.
[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         "net/http"
10         "strings"
11
12         "git.curoverse.com/arvados.git/lib/controller/federation"
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         "github.com/julienschmidt/httprouter"
17 )
18
19 type router struct {
20         mux *httprouter.Router
21         fed federation.Interface
22 }
23
24 func New(cluster *arvados.Cluster) *router {
25         rtr := &router{
26                 mux: httprouter.New(),
27                 fed: federation.New(cluster),
28         }
29         rtr.addRoutes(cluster)
30         return rtr
31 }
32
33 func (rtr *router) addRoutes(cluster *arvados.Cluster) {
34         for _, route := range []struct {
35                 endpoint    arvados.APIEndpoint
36                 defaultOpts func() interface{}
37                 exec        func(ctx context.Context, opts interface{}) (interface{}, error)
38         }{
39                 {
40                         arvados.EndpointCollectionCreate,
41                         func() interface{} { return &arvados.CreateOptions{} },
42                         func(ctx context.Context, opts interface{}) (interface{}, error) {
43                                 return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
44                         },
45                 },
46                 {
47                         arvados.EndpointCollectionUpdate,
48                         func() interface{} { return &arvados.UpdateOptions{} },
49                         func(ctx context.Context, opts interface{}) (interface{}, error) {
50                                 return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
51                         },
52                 },
53                 {
54                         arvados.EndpointCollectionGet,
55                         func() interface{} { return &arvados.GetOptions{} },
56                         func(ctx context.Context, opts interface{}) (interface{}, error) {
57                                 return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
58                         },
59                 },
60                 {
61                         arvados.EndpointCollectionList,
62                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
63                         func(ctx context.Context, opts interface{}) (interface{}, error) {
64                                 return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
65                         },
66                 },
67                 {
68                         arvados.EndpointCollectionProvenance,
69                         func() interface{} { return &arvados.GetOptions{} },
70                         func(ctx context.Context, opts interface{}) (interface{}, error) {
71                                 return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
72                         },
73                 },
74                 {
75                         arvados.EndpointCollectionUsedBy,
76                         func() interface{} { return &arvados.GetOptions{} },
77                         func(ctx context.Context, opts interface{}) (interface{}, error) {
78                                 return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
79                         },
80                 },
81                 {
82                         arvados.EndpointCollectionDelete,
83                         func() interface{} { return &arvados.DeleteOptions{} },
84                         func(ctx context.Context, opts interface{}) (interface{}, error) {
85                                 return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
86                         },
87                 },
88                 {
89                         arvados.EndpointContainerCreate,
90                         func() interface{} { return &arvados.CreateOptions{} },
91                         func(ctx context.Context, opts interface{}) (interface{}, error) {
92                                 return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
93                         },
94                 },
95                 {
96                         arvados.EndpointContainerUpdate,
97                         func() interface{} { return &arvados.UpdateOptions{} },
98                         func(ctx context.Context, opts interface{}) (interface{}, error) {
99                                 return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
100                         },
101                 },
102                 {
103                         arvados.EndpointContainerGet,
104                         func() interface{} { return &arvados.GetOptions{} },
105                         func(ctx context.Context, opts interface{}) (interface{}, error) {
106                                 return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
107                         },
108                 },
109                 {
110                         arvados.EndpointContainerList,
111                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
112                         func(ctx context.Context, opts interface{}) (interface{}, error) {
113                                 return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
114                         },
115                 },
116                 {
117                         arvados.EndpointContainerDelete,
118                         func() interface{} { return &arvados.DeleteOptions{} },
119                         func(ctx context.Context, opts interface{}) (interface{}, error) {
120                                 return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
121                         },
122                 },
123                 {
124                         arvados.EndpointContainerLock,
125                         func() interface{} {
126                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
127                         },
128                         func(ctx context.Context, opts interface{}) (interface{}, error) {
129                                 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
130                         },
131                 },
132                 {
133                         arvados.EndpointContainerUnlock,
134                         func() interface{} {
135                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
136                         },
137                         func(ctx context.Context, opts interface{}) (interface{}, error) {
138                                 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
139                         },
140                 },
141                 {
142                         arvados.EndpointSpecimenCreate,
143                         func() interface{} { return &arvados.CreateOptions{} },
144                         func(ctx context.Context, opts interface{}) (interface{}, error) {
145                                 return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
146                         },
147                 },
148                 {
149                         arvados.EndpointSpecimenUpdate,
150                         func() interface{} { return &arvados.UpdateOptions{} },
151                         func(ctx context.Context, opts interface{}) (interface{}, error) {
152                                 return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
153                         },
154                 },
155                 {
156                         arvados.EndpointSpecimenGet,
157                         func() interface{} { return &arvados.GetOptions{} },
158                         func(ctx context.Context, opts interface{}) (interface{}, error) {
159                                 return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
160                         },
161                 },
162                 {
163                         arvados.EndpointSpecimenList,
164                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
165                         func(ctx context.Context, opts interface{}) (interface{}, error) {
166                                 return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
167                         },
168                 },
169                 {
170                         arvados.EndpointSpecimenDelete,
171                         func() interface{} { return &arvados.DeleteOptions{} },
172                         func(ctx context.Context, opts interface{}) (interface{}, error) {
173                                 return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
174                         },
175                 },
176         } {
177                 route := route
178                 methods := []string{route.endpoint.Method}
179                 if route.endpoint.Method == "PATCH" {
180                         methods = append(methods, "PUT")
181                 }
182                 for _, method := range methods {
183                         rtr.mux.HandlerFunc(method, "/"+route.endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
184                                 params, err := rtr.loadRequestParams(req, route.endpoint.AttrsKey)
185                                 if err != nil {
186                                         rtr.sendError(w, err)
187                                         return
188                                 }
189                                 opts := route.defaultOpts()
190                                 err = rtr.transcode(params, opts)
191                                 if err != nil {
192                                         rtr.sendError(w, err)
193                                         return
194                                 }
195                                 respOpts, err := rtr.responseOptions(opts)
196                                 if err != nil {
197                                         rtr.sendError(w, err)
198                                         return
199                                 }
200
201                                 creds := auth.CredentialsFromRequest(req)
202                                 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
203                                         for _, t := range rt {
204                                                 if t, ok := t.(string); ok {
205                                                         creds.Tokens = append(creds.Tokens, t)
206                                                 }
207                                         }
208                                 }
209                                 ctx := req.Context()
210                                 ctx = context.WithValue(ctx, auth.ContextKeyCredentials, creds)
211                                 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
212                                 resp, err := route.exec(ctx, opts)
213                                 if err != nil {
214                                         ctxlog.FromContext(ctx).WithError(err).Debugf("returning error response for %#v", err)
215                                         rtr.sendError(w, err)
216                                         return
217                                 }
218                                 rtr.sendResponse(w, resp, respOpts)
219                         })
220                 }
221         }
222 }
223
224 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
225         switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
226         case "login", "logout", "auth":
227         default:
228                 w.Header().Set("Access-Control-Allow-Origin", "*")
229                 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
230                 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
231                 w.Header().Set("Access-Control-Max-Age", "86486400")
232         }
233         if r.Method == "OPTIONS" {
234                 return
235         }
236         r.ParseForm()
237         if m := r.FormValue("_method"); m != "" {
238                 r2 := *r
239                 r = &r2
240                 r.Method = m
241         }
242         rtr.mux.ServeHTTP(w, r)
243 }