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