22075: Improve formatting of description documentation
[arvados.git] / services / keepstore / proxy_remote_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package keepstore
6
7 import (
8         "crypto/md5"
9         "encoding/json"
10         "fmt"
11         "net"
12         "net/http"
13         "net/http/httptest"
14         "strconv"
15         "strings"
16         "sync/atomic"
17         "time"
18
19         "git.arvados.org/arvados.git/sdk/go/arvados"
20         "git.arvados.org/arvados.git/sdk/go/arvadostest"
21         "git.arvados.org/arvados.git/sdk/go/auth"
22         "git.arvados.org/arvados.git/sdk/go/ctxlog"
23         "git.arvados.org/arvados.git/sdk/go/httpserver"
24         "git.arvados.org/arvados.git/sdk/go/keepclient"
25         "github.com/prometheus/client_golang/prometheus"
26         check "gopkg.in/check.v1"
27 )
28
29 var _ = check.Suite(&proxyRemoteSuite{})
30
31 type proxyRemoteSuite struct {
32         cluster *arvados.Cluster
33         handler *router
34
35         remoteClusterID      string
36         remoteBlobSigningKey []byte
37         remoteKeepLocator    string
38         remoteKeepData       []byte
39         remoteKeepproxy      *httptest.Server
40         remoteKeepRequests   int64
41         remoteAPI            *httptest.Server
42 }
43
44 func (s *proxyRemoteSuite) remoteKeepproxyHandler(w http.ResponseWriter, r *http.Request) {
45         expectToken, err := auth.SaltToken(arvadostest.ActiveTokenV2, s.remoteClusterID)
46         if err != nil {
47                 panic(err)
48         }
49         atomic.AddInt64(&s.remoteKeepRequests, 1)
50         var token string
51         if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) == 2 && (auth[0] == "OAuth2" || auth[0] == "Bearer") {
52                 token = auth[1]
53         }
54         if r.Method == "GET" && r.URL.Path == "/"+s.remoteKeepLocator && token == expectToken {
55                 w.Write(s.remoteKeepData)
56                 return
57         }
58         http.Error(w, "404", 404)
59 }
60
61 func (s *proxyRemoteSuite) remoteAPIHandler(w http.ResponseWriter, r *http.Request) {
62         host, port, _ := net.SplitHostPort(strings.Split(s.remoteKeepproxy.URL, "//")[1])
63         portnum, _ := strconv.Atoi(port)
64         if r.URL.Path == "/arvados/v1/discovery/v1/rest" {
65                 json.NewEncoder(w).Encode(arvados.DiscoveryDocument{})
66                 return
67         }
68         if r.URL.Path == "/arvados/v1/keep_services/accessible" {
69                 json.NewEncoder(w).Encode(arvados.KeepServiceList{
70                         Items: []arvados.KeepService{
71                                 {
72                                         UUID:           s.remoteClusterID + "-bi6l4-proxyproxyproxy",
73                                         ServiceType:    "proxy",
74                                         ServiceHost:    host,
75                                         ServicePort:    portnum,
76                                         ServiceSSLFlag: false,
77                                 },
78                         },
79                 })
80                 return
81         }
82         http.Error(w, "404", 404)
83 }
84
85 func (s *proxyRemoteSuite) SetUpTest(c *check.C) {
86         s.remoteClusterID = "z0000"
87         s.remoteBlobSigningKey = []byte("3b6df6fb6518afe12922a5bc8e67bf180a358bc8")
88         s.remoteKeepproxy = httptest.NewServer(httpserver.LogRequests(http.HandlerFunc(s.remoteKeepproxyHandler)))
89         s.remoteAPI = httptest.NewUnstartedServer(http.HandlerFunc(s.remoteAPIHandler))
90         s.remoteAPI.StartTLS()
91         s.cluster = testCluster(c)
92         s.cluster.RemoteClusters = map[string]arvados.RemoteCluster{
93                 s.remoteClusterID: {
94                         Host:     strings.Split(s.remoteAPI.URL, "//")[1],
95                         Proxy:    true,
96                         Scheme:   "http",
97                         Insecure: true,
98                 },
99         }
100         s.cluster.Volumes = map[string]arvados.Volume{"zzzzz-nyw5e-000000000000000": {Driver: "stub"}}
101 }
102
103 func (s *proxyRemoteSuite) TearDownTest(c *check.C) {
104         s.remoteAPI.Close()
105         s.remoteKeepproxy.Close()
106 }
107
108 func (s *proxyRemoteSuite) TestProxyRemote(c *check.C) {
109         reg := prometheus.NewRegistry()
110         router, cancel := testRouter(c, s.cluster, reg)
111         defer cancel()
112         instrumented := httpserver.Instrument(reg, ctxlog.TestLogger(c), router)
113         handler := httpserver.LogRequests(instrumented.ServeAPI(s.cluster.ManagementToken, instrumented))
114
115         data := []byte("foo bar")
116         s.remoteKeepData = data
117         locator := fmt.Sprintf("%x+%d", md5.Sum(data), len(data))
118         s.remoteKeepLocator = keepclient.SignLocator(locator, arvadostest.ActiveTokenV2, time.Now().Add(time.Minute), time.Minute, s.remoteBlobSigningKey)
119
120         path := "/" + strings.Replace(s.remoteKeepLocator, "+A", "+R"+s.remoteClusterID+"-", 1)
121
122         for _, trial := range []struct {
123                 label            string
124                 method           string
125                 token            string
126                 xKeepSignature   string
127                 expectRemoteReqs int64
128                 expectCode       int
129                 expectSignature  bool
130         }{
131                 {
132                         label:            "GET only",
133                         method:           "GET",
134                         token:            arvadostest.ActiveTokenV2,
135                         expectRemoteReqs: 1,
136                         expectCode:       http.StatusOK,
137                 },
138                 {
139                         label:            "obsolete token",
140                         method:           "GET",
141                         token:            arvadostest.ActiveToken,
142                         expectRemoteReqs: 0,
143                         expectCode:       http.StatusBadRequest,
144                 },
145                 {
146                         label:            "bad token",
147                         method:           "GET",
148                         token:            arvadostest.ActiveTokenV2[:len(arvadostest.ActiveTokenV2)-3] + "xxx",
149                         expectRemoteReqs: 1,
150                         expectCode:       http.StatusNotFound,
151                 },
152                 {
153                         label:            "HEAD only",
154                         method:           "HEAD",
155                         token:            arvadostest.ActiveTokenV2,
156                         expectRemoteReqs: 1,
157                         expectCode:       http.StatusOK,
158                 },
159                 {
160                         label:            "HEAD with local signature",
161                         method:           "HEAD",
162                         xKeepSignature:   "local, time=" + time.Now().Format(time.RFC3339),
163                         token:            arvadostest.ActiveTokenV2,
164                         expectRemoteReqs: 1,
165                         expectCode:       http.StatusOK,
166                         expectSignature:  true,
167                 },
168                 {
169                         label:            "GET with local signature",
170                         method:           "GET",
171                         xKeepSignature:   "local, time=" + time.Now().Format(time.RFC3339),
172                         token:            arvadostest.ActiveTokenV2,
173                         expectRemoteReqs: 1,
174                         expectCode:       http.StatusOK,
175                         expectSignature:  true,
176                 },
177         } {
178                 c.Logf("=== trial: %s", trial.label)
179
180                 s.remoteKeepRequests = 0
181
182                 var req *http.Request
183                 var resp *httptest.ResponseRecorder
184                 req = httptest.NewRequest(trial.method, path, nil)
185                 req.Header.Set("Authorization", "Bearer "+trial.token)
186                 if trial.xKeepSignature != "" {
187                         req.Header.Set("X-Keep-Signature", trial.xKeepSignature)
188                 }
189                 resp = httptest.NewRecorder()
190                 handler.ServeHTTP(resp, req)
191                 c.Check(s.remoteKeepRequests, check.Equals, trial.expectRemoteReqs)
192                 if !c.Check(resp.Code, check.Equals, trial.expectCode) {
193                         c.Logf("resp.Code %d came with resp.Body %q", resp.Code, resp.Body.String())
194                 }
195                 if resp.Code == http.StatusOK {
196                         if trial.method == "HEAD" {
197                                 c.Check(resp.Body.String(), check.Equals, "")
198                                 c.Check(resp.Result().ContentLength, check.Equals, int64(len(data)))
199                         } else {
200                                 c.Check(resp.Body.String(), check.Equals, string(data))
201                         }
202                 } else {
203                         c.Check(resp.Body.String(), check.Not(check.Equals), string(data))
204                 }
205
206                 c.Check(resp.Header().Get("Vary"), check.Matches, `(.*, )?X-Keep-Signature(, .*)?`)
207
208                 locHdr := resp.Header().Get("X-Keep-Locator")
209                 if !trial.expectSignature {
210                         c.Check(locHdr, check.Equals, "")
211                         continue
212                 }
213
214                 c.Check(locHdr, check.Not(check.Equals), "")
215                 c.Check(locHdr, check.Not(check.Matches), `.*\+R.*`)
216                 c.Check(arvados.VerifySignature(locHdr, trial.token, s.cluster.Collections.BlobSigningTTL.Duration(), []byte(s.cluster.Collections.BlobSigningKey)), check.IsNil)
217
218                 // Ensure block can be requested using new signature
219                 req = httptest.NewRequest("GET", "/"+locHdr, nil)
220                 req.Header.Set("Authorization", "Bearer "+trial.token)
221                 resp = httptest.NewRecorder()
222                 handler.ServeHTTP(resp, req)
223                 c.Check(resp.Code, check.Equals, http.StatusOK)
224                 c.Check(s.remoteKeepRequests, check.Equals, trial.expectRemoteReqs)
225         }
226 }