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