1// Copyright 2020 Istio Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package test 16 17import ( 18 "context" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "log" 23 "net" 24 "net/http" 25 "net/url" 26 "os" 27 "strings" 28 "testing" 29 "time" 30 31 "google.golang.org/grpc" 32 33 "istio.io/istio/security/pkg/stsservice/tokenmanager/google" 34 35 proxyEnv "istio.io/istio/mixer/test/client/env" 36 istioEnv "istio.io/istio/pkg/test/env" 37 xdsService "istio.io/istio/security/pkg/stsservice/mock" 38 stsServer "istio.io/istio/security/pkg/stsservice/server" 39 "istio.io/istio/security/pkg/stsservice/tokenmanager" 40 tokenBackend "istio.io/istio/security/pkg/stsservice/tokenmanager/google/mock" 41) 42 43const ( 44 jwtToken = "thisisafakejwt" 45) 46 47// Env manages test setup and teardown. 48type Env struct { 49 ProxySetup *proxyEnv.TestSetup 50 AuthServer *tokenBackend.AuthorizationServer 51 52 stsServer *stsServer.Server 53 xdsServer *grpc.Server 54 ProxyListenerPort int 55 initialToken string // initial token is sent to STS server for token exchange 56 tokenExchangePlugin *google.Plugin 57} 58 59// TearDown shuts down all the components. 60func (e *Env) TearDown() { 61 // Stop proxy first, otherwise XDS stream is still alive and server's graceful 62 // stop will be blocked. 63 e.ProxySetup.TearDown() 64 _ = e.AuthServer.Stop() 65 e.xdsServer.GracefulStop() 66 e.stsServer.Stop() 67} 68 69func getDataFromFile(filePath string, t *testing.T) string { 70 data, err := ioutil.ReadFile(filePath) 71 if err != nil { 72 t.Fatalf("failed to read %q", filePath) 73 } 74 return string(data) 75} 76 77// WriteDataToFile writes data into file 78func WriteDataToFile(path string, content string) error { 79 if path == "" { 80 return errors.New("empty file path") 81 } 82 f, err := os.Create(path) 83 if err != nil { 84 return err 85 } 86 defer f.Close() 87 if _, err = f.WriteString(content); err != nil { 88 return err 89 } 90 _ = f.Sync() 91 return nil 92} 93 94// SetupTest starts Envoy, XDS server, STS server, token manager, and a token service backend. 95// Envoy loads a test config that requires token credential to access XDS server. 96// That token credential is provisioned by STS server. 97// enableCache indicates whether to enable token cache at STS server side. 98// Here is a map between ports and servers 99// auth server : MixerPort 100// STS server : STSPort 101// Dynamic proxy listener : ClientProxyPort 102// Static proxy listener : TCPProxyPort 103// XDS server : DiscoveryPort 104// test backend : BackendPort 105// proxy admin : AdminPort 106func SetupTest(t *testing.T, cb *xdsService.XDSCallbacks, testID uint16, enableCache bool) *Env { 107 env := &Env{ 108 initialToken: jwtToken, 109 } 110 // Set up test environment for Proxy 111 proxySetup := proxyEnv.NewTestSetup(testID, t) 112 proxySetup.SetNoMixer(true) 113 proxySetup.EnvoyTemplate = getDataFromFile(istioEnv.IstioSrc+"/security/pkg/stsservice/test/testdata/bootstrap.yaml", t) 114 // Set up credential files for bootstrap config 115 if err := WriteDataToFile(proxySetup.JWTTokenPath(), jwtToken); err != nil { 116 t.Fatalf("failed to set up token file %s: %v", proxySetup.JWTTokenPath(), err) 117 } 118 caCert := getDataFromFile(istioEnv.IstioSrc+"/security/pkg/stsservice/test/testdata/ca-certificate.crt", t) 119 if err := WriteDataToFile(proxySetup.CACertPath(), caCert); err != nil { 120 t.Fatalf("failed to set up ca certificate file %s: %v", proxySetup.CACertPath(), err) 121 } 122 123 env.ProxySetup = proxySetup 124 env.DumpPortMap(t) 125 // Set up auth server that provides token service 126 backend, err := tokenBackend.StartNewServer(t, tokenBackend.Config{ 127 SubjectToken: jwtToken, 128 Port: int(proxySetup.Ports().MixerPort), 129 AccessToken: cb.ExpectedToken(), 130 }) 131 if err != nil { 132 t.Fatalf("failed to start a auth backend: %v", err) 133 } 134 env.AuthServer = backend 135 136 // Set up STS server 137 stsServer, plugin, err := setupSTS(int(proxySetup.Ports().STSPort), backend.URL, enableCache) 138 if err != nil { 139 t.Fatalf("failed to start a STS server: %v", err) 140 } 141 env.stsServer = stsServer 142 env.tokenExchangePlugin = plugin 143 144 // Make sure STS server and auth backend are running 145 env.WaitForStsFlowReady(t) 146 147 // Set up XDS server 148 env.ProxyListenerPort = int(proxySetup.Ports().ClientProxyPort) 149 ls := &xdsService.DynamicListener{Port: env.ProxyListenerPort} 150 xds, err := xdsService.StartXDSServer( 151 xdsService.XDSConf{Port: int(proxySetup.Ports().DiscoveryPort), 152 CertFile: istioEnv.IstioSrc + "/security/pkg/stsservice/test/testdata/server-certificate.crt", 153 KeyFile: istioEnv.IstioSrc + "/security/pkg/stsservice/test/testdata/server-key.key"}, cb, ls, true) 154 if err != nil { 155 t.Fatalf("failed to start XDS server: %v", err) 156 } 157 env.xdsServer = xds 158 159 return env 160} 161 162// DumpPortMap dumps port allocation status 163// auth server : MixerPort 164// STS server : STSPort 165// Dynamic proxy listener : ClientProxyPort 166// Static proxy listener : TCPProxyPort 167// XDS server : DiscoveryPort 168// test backend : BackendPort 169// proxy admin : AdminPort 170func (e *Env) DumpPortMap(t *testing.T) { 171 log.Printf("\n\tport allocation status\t\t\t\n"+ 172 "auth server\t\t:\t%d\n"+ 173 "STS server\t\t:\t%d\n"+ 174 "dynamic listener port\t:\t%d\n"+ 175 "static listener port\t:\t%d\n"+ 176 "XDS server\t\t:\t%d\n"+ 177 "test backend\t\t:\t%d\n"+ 178 "proxy admin\t\t:\t%d", e.ProxySetup.Ports().MixerPort, 179 e.ProxySetup.Ports().STSPort, e.ProxySetup.Ports().ClientProxyPort, 180 e.ProxySetup.Ports().TCPProxyPort, e.ProxySetup.Ports().DiscoveryPort, 181 e.ProxySetup.Ports().BackendPort, e.ProxySetup.Ports().AdminPort) 182} 183 184// ClearTokenCache removes cached token in token exchange plugin. 185func (e *Env) ClearTokenCache() { 186 e.tokenExchangePlugin.ClearCache() 187} 188 189// StartProxy starts proxy. 190func (e *Env) StartProxy(t *testing.T) { 191 if err := e.ProxySetup.SetUp(); err != nil { 192 t.Fatalf("failed to start proxy: %v", err) 193 } 194 log.Println("proxy is running...") 195} 196 197// WaitForStsFlowReady sends STS requests to STS server using HTTP client, and 198// verifies that the STS flow is ready. 199func (e *Env) WaitForStsFlowReady(t *testing.T) { 200 t.Logf("%s check if all servers in the STS flow are up and ready", time.Now().String()) 201 addr, _ := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", e.ProxySetup.Ports().STSPort)) 202 stsServerAddress := addr.String() 203 hTTPClient := &http.Client{ 204 Transport: &http.Transport{ 205 DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 206 t.Logf("set up server address to dial %s", addr) 207 addr = stsServerAddress 208 return net.Dial(network, addr) 209 }, 210 }, 211 } 212 // keep sending requests periodically until a success STS response is received 213 req := e.genStsReq(stsServerAddress) 214 for i := 0; i < 20; i++ { 215 resp, err := hTTPClient.Do(req) 216 if err == nil { 217 if resp.StatusCode == http.StatusOK && resp.Header.Get("Content-Type") == "application/json" { 218 t.Logf("%s all servers in the STS flow are up and ready", time.Now().String()) 219 return 220 } 221 } 222 time.Sleep(100 * time.Millisecond) 223 } 224 t.Errorf("STS flow is not ready") 225} 226 227func (e *Env) genStsReq(stsAddr string) (req *http.Request) { 228 stsQuery := url.Values{} 229 stsQuery.Set("grant_type", stsServer.TokenExchangeGrantType) 230 stsQuery.Set("resource", "https//:backend.example.com") 231 stsQuery.Set("audience", "audience") 232 stsQuery.Set("scope", "https://www.googleapis.com/auth/cloud-platform") 233 stsQuery.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") 234 stsQuery.Set("subject_token", e.initialToken) 235 stsQuery.Set("subject_token_type", stsServer.SubjectTokenType) 236 stsQuery.Set("actor_token", "") 237 stsQuery.Set("actor_token_type", "") 238 stsURL := "http://" + stsAddr + stsServer.TokenPath 239 req, _ = http.NewRequest("POST", stsURL, strings.NewReader(stsQuery.Encode())) 240 req.Header.Set("Content-Type", stsServer.URLEncodedForm) 241 return req 242} 243 244func setupSTS(stsPort int, backendURL string, enableCache bool) (*stsServer.Server, *google.Plugin, error) { 245 // Create token exchange Google plugin 246 tokenExchangePlugin, _ := google.CreateTokenManagerPlugin(tokenBackend.FakeTrustDomain, 247 tokenBackend.FakeProjectNum, tokenBackend.FakeGKEClusterURL, enableCache) 248 federatedTokenTestingEndpoint := backendURL + "/v1/identitybindingtoken" 249 accessTokenTestingEndpoint := backendURL + "/v1/projects/-/serviceAccounts/service-%s@gcp-sa-meshdataplane.iam.gserviceaccount.com:generateAccessToken" 250 tokenExchangePlugin.SetEndpoints(federatedTokenTestingEndpoint, accessTokenTestingEndpoint) 251 // Create token manager 252 tm := tokenmanager.CreateTokenManager(tokenmanager.GoogleTokenExchange, 253 tokenmanager.Config{TrustDomain: tokenBackend.FakeTrustDomain}) 254 tm.(*tokenmanager.TokenManager).SetPlugin(tokenExchangePlugin) 255 // Create STS server 256 addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", stsPort)) 257 if err != nil { 258 return nil, nil, fmt.Errorf("failed to create address %v", err) 259 } 260 server, err := stsServer.NewServer(stsServer.Config{LocalHostAddr: addr.IP.String(), LocalPort: addr.Port}, tm) 261 return server, tokenExchangePlugin, err 262} 263