13493: Merge branch 'master' into 13493-federation-proxy
[arvados.git] / lib / controller / federation.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package controller
6
7 import (
8         "bytes"
9         "io/ioutil"
10         "net/http"
11         "net/url"
12         "regexp"
13
14         "git.curoverse.com/arvados.git/sdk/go/auth"
15         "git.curoverse.com/arvados.git/sdk/go/httpserver"
16 )
17
18 var wfRe = regexp.MustCompile(`^/arvados/v1/workflows/([0-9a-z]{5})-[^/]+$`)
19
20 func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, next http.Handler) {
21         m := wfRe.FindStringSubmatch(req.URL.Path)
22         if len(m) < 2 || m[1] == h.Cluster.ClusterID {
23                 next.ServeHTTP(w, req)
24                 return
25         }
26         remoteID := m[1]
27         remote, ok := h.Cluster.RemoteClusters[remoteID]
28         if !ok {
29                 httpserver.Error(w, "no proxy available for cluster "+remoteID, http.StatusNotFound)
30                 return
31         }
32         scheme := remote.Scheme
33         if scheme == "" {
34                 scheme = "https"
35         }
36         urlOut := &url.URL{
37                 Scheme:   scheme,
38                 Host:     remote.Host,
39                 Path:     req.URL.Path,
40                 RawPath:  req.URL.RawPath,
41                 RawQuery: req.URL.RawQuery,
42         }
43         err := h.saltAuthToken(req, remoteID)
44         if err != nil {
45                 httpserver.Error(w, err.Error(), http.StatusBadRequest)
46                 return
47         }
48         client := h.secureClient
49         if remote.Insecure {
50                 client = h.insecureClient
51         }
52         h.proxy.Do(w, req, urlOut, client)
53 }
54
55 // Extract the auth token supplied in req, and replace it with a
56 // salted token for the remote cluster.
57 func (h *Handler) saltAuthToken(req *http.Request, remote string) error {
58         creds := auth.NewCredentials()
59         creds.LoadTokensFromHTTPRequest(req)
60         if len(creds.Tokens) == 0 && req.Header.Get("Content-Type") == "application/x-www-form-encoded" {
61                 // Override ParseForm's 10MiB limit by ensuring
62                 // req.Body is a *http.maxBytesReader.
63                 req.Body = http.MaxBytesReader(nil, req.Body, 1<<28) // 256MiB. TODO: use MaxRequestSize from discovery doc or config.
64                 if err := creds.LoadTokensFromHTTPRequestBody(req); err != nil {
65                         return err
66                 }
67                 // Replace req.Body with a buffer that re-encodes the
68                 // form without api_token, in case we end up
69                 // forwarding the request to RailsAPI.
70                 if req.PostForm != nil {
71                         req.PostForm.Del("api_token")
72                 }
73                 req.Body = ioutil.NopCloser(bytes.NewBufferString(req.PostForm.Encode()))
74         }
75         if len(creds.Tokens) == 0 {
76                 return nil
77         }
78         token, err := auth.SaltToken(creds.Tokens[0], remote)
79         if err == auth.ErrObsoleteToken {
80                 // FIXME: If the token exists in our own database,
81                 // salt it for the remote. Otherwise, assume it was
82                 // issued by the remote, and pass it through
83                 // unmodified.
84                 token = creds.Tokens[0]
85         } else if err != nil {
86                 return err
87         }
88         req.Header.Set("Authorization", "Bearer "+token)
89         return nil
90 }