1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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"
24 wrapCalls func(RoutableFunc) RoutableFunc
27 // New returns a new router (which implements the http.Handler
28 // interface) that serves requests by calling Arvados API methods on
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(RoutableFunc) RoutableFunc) *router {
45 type RoutableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
47 func (rtr *router) addRoutes() {
48 for _, route := range []struct {
49 endpoint arvados.APIEndpoint
50 defaultOpts func() interface{}
54 arvados.EndpointConfigGet,
55 func() interface{} { return &struct{}{} },
56 func(ctx context.Context, opts interface{}) (interface{}, error) {
57 return rtr.backend.ConfigGet(ctx)
61 arvados.EndpointLogin,
62 func() interface{} { return &arvados.LoginOptions{} },
63 func(ctx context.Context, opts interface{}) (interface{}, error) {
64 return rtr.backend.Login(ctx, *opts.(*arvados.LoginOptions))
68 arvados.EndpointLogout,
69 func() interface{} { return &arvados.LogoutOptions{} },
70 func(ctx context.Context, opts interface{}) (interface{}, error) {
71 return rtr.backend.Logout(ctx, *opts.(*arvados.LogoutOptions))
75 arvados.EndpointCollectionCreate,
76 func() interface{} { return &arvados.CreateOptions{} },
77 func(ctx context.Context, opts interface{}) (interface{}, error) {
78 return rtr.backend.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
82 arvados.EndpointCollectionUpdate,
83 func() interface{} { return &arvados.UpdateOptions{} },
84 func(ctx context.Context, opts interface{}) (interface{}, error) {
85 return rtr.backend.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
89 arvados.EndpointCollectionGet,
90 func() interface{} { return &arvados.GetOptions{} },
91 func(ctx context.Context, opts interface{}) (interface{}, error) {
92 return rtr.backend.CollectionGet(ctx, *opts.(*arvados.GetOptions))
96 arvados.EndpointCollectionList,
97 func() interface{} { return &arvados.ListOptions{Limit: -1} },
98 func(ctx context.Context, opts interface{}) (interface{}, error) {
99 return rtr.backend.CollectionList(ctx, *opts.(*arvados.ListOptions))
103 arvados.EndpointCollectionProvenance,
104 func() interface{} { return &arvados.GetOptions{} },
105 func(ctx context.Context, opts interface{}) (interface{}, error) {
106 return rtr.backend.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
110 arvados.EndpointCollectionUsedBy,
111 func() interface{} { return &arvados.GetOptions{} },
112 func(ctx context.Context, opts interface{}) (interface{}, error) {
113 return rtr.backend.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
117 arvados.EndpointCollectionDelete,
118 func() interface{} { return &arvados.DeleteOptions{} },
119 func(ctx context.Context, opts interface{}) (interface{}, error) {
120 return rtr.backend.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
124 arvados.EndpointCollectionTrash,
125 func() interface{} { return &arvados.DeleteOptions{} },
126 func(ctx context.Context, opts interface{}) (interface{}, error) {
127 return rtr.backend.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
131 arvados.EndpointCollectionUntrash,
132 func() interface{} { return &arvados.UntrashOptions{} },
133 func(ctx context.Context, opts interface{}) (interface{}, error) {
134 return rtr.backend.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
138 arvados.EndpointContainerCreate,
139 func() interface{} { return &arvados.CreateOptions{} },
140 func(ctx context.Context, opts interface{}) (interface{}, error) {
141 return rtr.backend.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
145 arvados.EndpointContainerUpdate,
146 func() interface{} { return &arvados.UpdateOptions{} },
147 func(ctx context.Context, opts interface{}) (interface{}, error) {
148 return rtr.backend.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
152 arvados.EndpointContainerGet,
153 func() interface{} { return &arvados.GetOptions{} },
154 func(ctx context.Context, opts interface{}) (interface{}, error) {
155 return rtr.backend.ContainerGet(ctx, *opts.(*arvados.GetOptions))
159 arvados.EndpointContainerList,
160 func() interface{} { return &arvados.ListOptions{Limit: -1} },
161 func(ctx context.Context, opts interface{}) (interface{}, error) {
162 return rtr.backend.ContainerList(ctx, *opts.(*arvados.ListOptions))
166 arvados.EndpointContainerDelete,
167 func() interface{} { return &arvados.DeleteOptions{} },
168 func(ctx context.Context, opts interface{}) (interface{}, error) {
169 return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
173 arvados.EndpointContainerLock,
175 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
177 func(ctx context.Context, opts interface{}) (interface{}, error) {
178 return rtr.backend.ContainerLock(ctx, *opts.(*arvados.GetOptions))
182 arvados.EndpointContainerUnlock,
184 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
186 func(ctx context.Context, opts interface{}) (interface{}, error) {
187 return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
191 arvados.EndpointSpecimenCreate,
192 func() interface{} { return &arvados.CreateOptions{} },
193 func(ctx context.Context, opts interface{}) (interface{}, error) {
194 return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
198 arvados.EndpointSpecimenUpdate,
199 func() interface{} { return &arvados.UpdateOptions{} },
200 func(ctx context.Context, opts interface{}) (interface{}, error) {
201 return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
205 arvados.EndpointSpecimenGet,
206 func() interface{} { return &arvados.GetOptions{} },
207 func(ctx context.Context, opts interface{}) (interface{}, error) {
208 return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
212 arvados.EndpointSpecimenList,
213 func() interface{} { return &arvados.ListOptions{Limit: -1} },
214 func(ctx context.Context, opts interface{}) (interface{}, error) {
215 return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
219 arvados.EndpointSpecimenDelete,
220 func() interface{} { return &arvados.DeleteOptions{} },
221 func(ctx context.Context, opts interface{}) (interface{}, error) {
222 return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
226 arvados.EndpointUserCreate,
227 func() interface{} { return &arvados.CreateOptions{} },
228 func(ctx context.Context, opts interface{}) (interface{}, error) {
229 return rtr.backend.UserCreate(ctx, *opts.(*arvados.CreateOptions))
233 arvados.EndpointUserMerge,
234 func() interface{} { return &arvados.UserMergeOptions{} },
235 func(ctx context.Context, opts interface{}) (interface{}, error) {
236 return rtr.backend.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
240 arvados.EndpointUserActivate,
241 func() interface{} { return &arvados.UserActivateOptions{} },
242 func(ctx context.Context, opts interface{}) (interface{}, error) {
243 return rtr.backend.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
247 arvados.EndpointUserSetup,
248 func() interface{} { return &arvados.UserSetupOptions{} },
249 func(ctx context.Context, opts interface{}) (interface{}, error) {
250 return rtr.backend.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
254 arvados.EndpointUserUnsetup,
255 func() interface{} { return &arvados.GetOptions{} },
256 func(ctx context.Context, opts interface{}) (interface{}, error) {
257 return rtr.backend.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
261 arvados.EndpointUserGetCurrent,
262 func() interface{} { return &arvados.GetOptions{} },
263 func(ctx context.Context, opts interface{}) (interface{}, error) {
264 return rtr.backend.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
268 arvados.EndpointUserGetSystem,
269 func() interface{} { return &arvados.GetOptions{} },
270 func(ctx context.Context, opts interface{}) (interface{}, error) {
271 return rtr.backend.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
275 arvados.EndpointUserGet,
276 func() interface{} { return &arvados.GetOptions{} },
277 func(ctx context.Context, opts interface{}) (interface{}, error) {
278 return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
282 arvados.EndpointUserUpdateUUID,
283 func() interface{} { return &arvados.UpdateUUIDOptions{} },
284 func(ctx context.Context, opts interface{}) (interface{}, error) {
285 return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
289 arvados.EndpointUserUpdate,
290 func() interface{} { return &arvados.UpdateOptions{} },
291 func(ctx context.Context, opts interface{}) (interface{}, error) {
292 return rtr.backend.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
296 arvados.EndpointUserList,
297 func() interface{} { return &arvados.ListOptions{Limit: -1} },
298 func(ctx context.Context, opts interface{}) (interface{}, error) {
299 return rtr.backend.UserList(ctx, *opts.(*arvados.ListOptions))
303 arvados.EndpointUserBatchUpdate,
304 func() interface{} { return &arvados.UserBatchUpdateOptions{} },
305 func(ctx context.Context, opts interface{}) (interface{}, error) {
306 return rtr.backend.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
310 arvados.EndpointUserDelete,
311 func() interface{} { return &arvados.DeleteOptions{} },
312 func(ctx context.Context, opts interface{}) (interface{}, error) {
313 return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
317 arvados.EndpointUserAuthenticate,
318 func() interface{} { return &arvados.UserAuthenticateOptions{} },
319 func(ctx context.Context, opts interface{}) (interface{}, error) {
320 return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
325 if rtr.wrapCalls != nil {
326 exec = rtr.wrapCalls(exec)
328 rtr.addRoute(route.endpoint, route.defaultOpts, exec)
330 rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
331 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
333 rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
334 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
338 var altMethod = map[string]string{
339 "PATCH": "PUT", // Accept PUT as a synonym for PATCH
340 "GET": "HEAD", // Accept HEAD at any GET route
343 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec RoutableFunc) {
344 methods := []string{endpoint.Method}
345 if alt, ok := altMethod[endpoint.Method]; ok {
346 methods = append(methods, alt)
348 rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
349 logger := ctxlog.FromContext(req.Context())
350 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
352 logger.WithFields(logrus.Fields{
354 "method": endpoint.Method,
355 "endpoint": endpoint,
356 }).WithError(err).Debug("error loading request params")
357 rtr.sendError(w, err)
360 opts := defaultOpts()
361 err = rtr.transcode(params, opts)
363 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
364 rtr.sendError(w, err)
367 respOpts, err := rtr.responseOptions(opts)
369 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
370 rtr.sendError(w, err)
374 creds := auth.CredentialsFromRequest(req)
375 err = creds.LoadTokensFromHTTPRequestBody(req)
377 rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
380 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
381 for _, t := range rt {
382 if t, ok := t.(string); ok {
383 creds.Tokens = append(creds.Tokens, t)
387 ctx := auth.NewContext(req.Context(), creds)
388 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
389 logger.WithFields(logrus.Fields{
390 "apiEndpoint": endpoint,
391 "apiOptsType": fmt.Sprintf("%T", opts),
394 resp, err := exec(ctx, opts)
396 logger.WithError(err).Debugf("returning error type %T", err)
397 rtr.sendError(w, err)
400 rtr.sendResponse(w, req, resp, respOpts)
404 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
405 switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
406 case "login", "logout", "auth":
408 w.Header().Set("Access-Control-Allow-Origin", "*")
409 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
410 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Http-Method-Override")
411 w.Header().Set("Access-Control-Max-Age", "86486400")
413 if r.Method == "OPTIONS" {
416 if r.Method == "POST" {
418 if m := r.FormValue("_method"); m != "" {
422 } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
428 rtr.mux.ServeHTTP(w, r)