14262: Refactoring, split up federation code into smaller files
[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         "database/sql"
10         "fmt"
11         "io"
12         "io/ioutil"
13         "net/http"
14         "net/url"
15         "regexp"
16         "strings"
17
18         "git.curoverse.com/arvados.git/sdk/go/arvados"
19         "git.curoverse.com/arvados.git/sdk/go/auth"
20 )
21
22 var pathPattern = `^/arvados/v1/%s(/([0-9a-z]{5})-%s-[0-9a-z]{15})?(.*)$`
23 var wfRe = regexp.MustCompile(fmt.Sprintf(pathPattern, "workflows", "7fd4e"))
24 var containersRe = regexp.MustCompile(fmt.Sprintf(pathPattern, "containers", "dz642"))
25 var containerRequestsRe = regexp.MustCompile(fmt.Sprintf(pathPattern, "container_requests", "xvhdp"))
26 var collectionRe = regexp.MustCompile(fmt.Sprintf(pathPattern, "collections", "4zz18"))
27 var collectionByPDHRe = regexp.MustCompile(`^/arvados/v1/collections/([0-9a-fA-F]{32}\+[0-9]+)+$`)
28
29 func (h *Handler) remoteClusterRequest(remoteID string, req *http.Request) (*http.Response, error) {
30         remote, ok := h.Cluster.RemoteClusters[remoteID]
31         if !ok {
32                 return nil, HTTPError{fmt.Sprintf("no proxy available for cluster %v", remoteID), http.StatusNotFound}
33         }
34         scheme := remote.Scheme
35         if scheme == "" {
36                 scheme = "https"
37         }
38         saltedReq, err := h.saltAuthToken(req, remoteID)
39         if err != nil {
40                 return nil, err
41         }
42         urlOut := &url.URL{
43                 Scheme:   scheme,
44                 Host:     remote.Host,
45                 Path:     saltedReq.URL.Path,
46                 RawPath:  saltedReq.URL.RawPath,
47                 RawQuery: saltedReq.URL.RawQuery,
48         }
49         client := h.secureClient
50         if remote.Insecure {
51                 client = h.insecureClient
52         }
53         return h.proxy.ForwardRequest(saltedReq, urlOut, client)
54 }
55
56 // Buffer request body, parse form parameters in request, and then
57 // replace original body with the buffer so it can be re-read by
58 // downstream proxy steps.
59 func loadParamsFromForm(req *http.Request) error {
60         var postBody *bytes.Buffer
61         if req.Body != nil && req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
62                 var cl int64
63                 if req.ContentLength > 0 {
64                         cl = req.ContentLength
65                 }
66                 postBody = bytes.NewBuffer(make([]byte, 0, cl))
67                 originalBody := req.Body
68                 defer originalBody.Close()
69                 req.Body = ioutil.NopCloser(io.TeeReader(req.Body, postBody))
70         }
71
72         err := req.ParseForm()
73         if err != nil {
74                 return err
75         }
76
77         if req.Body != nil && postBody != nil {
78                 req.Body = ioutil.NopCloser(postBody)
79         }
80         return nil
81 }
82
83 func (h *Handler) setupProxyRemoteCluster(next http.Handler) http.Handler {
84         mux := http.NewServeMux()
85         mux.Handle("/arvados/v1/workflows", &genericFederatedRequestHandler{next, h, wfRe})
86         mux.Handle("/arvados/v1/workflows/", &genericFederatedRequestHandler{next, h, wfRe})
87         mux.Handle("/arvados/v1/containers", &genericFederatedRequestHandler{next, h, containersRe})
88         mux.Handle("/arvados/v1/containers/", &genericFederatedRequestHandler{next, h, containersRe})
89         mux.Handle("/arvados/v1/container_requests", &genericFederatedRequestHandler{next, h, containerRequestsRe})
90         mux.Handle("/arvados/v1/container_requests/", &genericFederatedRequestHandler{next, h, containerRequestsRe})
91         mux.Handle("/arvados/v1/collections", next)
92         mux.Handle("/arvados/v1/collections/", &collectionFederatedRequestHandler{next, h})
93         mux.Handle("/", next)
94
95         return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
96                 parts := strings.Split(req.Header.Get("Authorization"), "/")
97                 alreadySalted := (len(parts) == 3 && parts[0] == "Bearer v2" && len(parts[2]) == 40)
98
99                 if alreadySalted ||
100                         strings.Index(req.Header.Get("Via"), "arvados-controller") != -1 {
101                         // The token is already salted, or this is a
102                         // request from another instance of
103                         // arvados-controller.  In either case, we
104                         // don't want to proxy this query, so just
105                         // continue down the instance handler stack.
106                         next.ServeHTTP(w, req)
107                         return
108                 }
109
110                 mux.ServeHTTP(w, req)
111         })
112
113         return mux
114 }
115
116 type CurrentUser struct {
117         Authorization arvados.APIClientAuthorization
118         UUID          string
119 }
120
121 func (h *Handler) validateAPItoken(req *http.Request, user *CurrentUser) error {
122         db, err := h.db(req)
123         if err != nil {
124                 return err
125         }
126         return db.QueryRowContext(req.Context(), `SELECT api_client_authorizations.uuid, users.uuid FROM api_client_authorizations JOIN users on api_client_authorizations.user_id=users.id WHERE api_token=$1 AND (expires_at IS NULL OR expires_at > current_timestamp) LIMIT 1`, user.Authorization.APIToken).Scan(&user.Authorization.UUID, &user.UUID)
127 }
128
129 // Extract the auth token supplied in req, and replace it with a
130 // salted token for the remote cluster.
131 func (h *Handler) saltAuthToken(req *http.Request, remote string) (updatedReq *http.Request, err error) {
132         updatedReq = (&http.Request{
133                 Method:        req.Method,
134                 URL:           req.URL,
135                 Header:        req.Header,
136                 Body:          req.Body,
137                 ContentLength: req.ContentLength,
138                 Host:          req.Host,
139         }).WithContext(req.Context())
140
141         creds := auth.NewCredentials()
142         creds.LoadTokensFromHTTPRequest(updatedReq)
143         if len(creds.Tokens) == 0 && updatedReq.Header.Get("Content-Type") == "application/x-www-form-encoded" {
144                 // Override ParseForm's 10MiB limit by ensuring
145                 // req.Body is a *http.maxBytesReader.
146                 updatedReq.Body = http.MaxBytesReader(nil, updatedReq.Body, 1<<28) // 256MiB. TODO: use MaxRequestSize from discovery doc or config.
147                 if err := creds.LoadTokensFromHTTPRequestBody(updatedReq); err != nil {
148                         return nil, err
149                 }
150                 // Replace req.Body with a buffer that re-encodes the
151                 // form without api_token, in case we end up
152                 // forwarding the request.
153                 if updatedReq.PostForm != nil {
154                         updatedReq.PostForm.Del("api_token")
155                 }
156                 updatedReq.Body = ioutil.NopCloser(bytes.NewBufferString(updatedReq.PostForm.Encode()))
157         }
158         if len(creds.Tokens) == 0 {
159                 return updatedReq, nil
160         }
161
162         token, err := auth.SaltToken(creds.Tokens[0], remote)
163
164         if err == auth.ErrObsoleteToken {
165                 // If the token exists in our own database, salt it
166                 // for the remote. Otherwise, assume it was issued by
167                 // the remote, and pass it through unmodified.
168                 currentUser := CurrentUser{Authorization: arvados.APIClientAuthorization{APIToken: creds.Tokens[0]}}
169                 err = h.validateAPItoken(req, &currentUser)
170                 if err == sql.ErrNoRows {
171                         // Not ours; pass through unmodified.
172                         token = currentUser.Authorization.APIToken
173                 } else if err != nil {
174                         return nil, err
175                 } else {
176                         // Found; make V2 version and salt it.
177                         token, err = auth.SaltToken(currentUser.Authorization.TokenV2(), remote)
178                         if err != nil {
179                                 return nil, err
180                         }
181                 }
182         } else if err != nil {
183                 return nil, err
184         }
185         updatedReq.Header = http.Header{}
186         for k, v := range req.Header {
187                 if k != "Authorization" {
188                         updatedReq.Header[k] = v
189                 }
190         }
191         updatedReq.Header.Set("Authorization", "Bearer "+token)
192
193         // Remove api_token=... from the the query string, in case we
194         // end up forwarding the request.
195         if values, err := url.ParseQuery(updatedReq.URL.RawQuery); err != nil {
196                 return nil, err
197         } else if _, ok := values["api_token"]; ok {
198                 delete(values, "api_token")
199                 updatedReq.URL = &url.URL{
200                         Scheme:   req.URL.Scheme,
201                         Host:     req.URL.Host,
202                         Path:     req.URL.Path,
203                         RawPath:  req.URL.RawPath,
204                         RawQuery: values.Encode(),
205                 }
206         }
207         return updatedReq, nil
208 }