github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/pkg/web.go (about)

     1  // Copyright (c) 2020, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package pkg
     5  
     6  import (
     7  	"context"
     8  	"crypto/x509"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"strings"
    13  
    14  	"github.com/hashicorp/go-retryablehttp"
    15  	"github.com/onsi/gomega"
    16  	"github.com/onsi/gomega/types"
    17  	"github.com/verrazzano/verrazzano/pkg/httputil"
    18  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  )
    21  
    22  const (
    23  	// defaultEnvName - default environment name
    24  	defaultEnvName = "default"
    25  )
    26  
    27  // HTTPResponse represents an HTTP response including the read body
    28  type HTTPResponse struct {
    29  	StatusCode int
    30  	Header     http.Header
    31  	Body       []byte
    32  }
    33  
    34  // GetWebPage makes an HTTP GET request using a retryable client configured with the Verrazzano cert bundle
    35  func GetWebPage(url string, hostHeader string) (*HTTPResponse, error) {
    36  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
    37  	if err != nil {
    38  		Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err))
    39  		return nil, err
    40  	}
    41  
    42  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	return GetWebPageWithClient(client, url, hostHeader)
    47  }
    48  
    49  // GetWebPageInCluster makes an HTTP GET request using a retryable client configured with the Verrazzano cert bundle
    50  func GetWebPageInCluster(url string, hostHeader string, kubeconfigPath string) (*HTTPResponse, error) {
    51  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	return GetWebPageWithClient(client, url, hostHeader)
    56  }
    57  
    58  // GetWebPageWithClient submits a GET request using the specified client.
    59  func GetWebPageWithClient(httpClient *retryablehttp.Client, url string, hostHeader string) (*HTTPResponse, error) {
    60  	return doReq(url, "GET", "", hostHeader, "", "", nil, httpClient)
    61  }
    62  
    63  // GetWebPageWithBasicAuth gets a web page using basic auth, using a given kubeconfig
    64  func GetWebPageWithBasicAuth(url string, hostHeader string, username string, password string, kubeconfigPath string) (*HTTPResponse, error) {
    65  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return doReq(url, "GET", "", hostHeader, username, password, nil, client)
    70  }
    71  
    72  // GetCertificates will return the server SSL certificates for the given URL.
    73  func GetCertificates(url string) ([]*x509.Certificate, error) {
    74  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
    75  	if err != nil {
    76  		Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err))
    77  		return nil, err
    78  	}
    79  
    80  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	resp, err := client.Get(url)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	defer resp.Body.Close()
    89  	return resp.TLS.PeerCertificates, nil
    90  }
    91  
    92  // PostWithHostHeader posts a request with a specified Host header
    93  func PostWithHostHeader(url, contentType string, hostHeader string, body io.Reader) (*HTTPResponse, error) {
    94  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
    95  	if err != nil {
    96  		Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err))
    97  		return nil, err
    98  	}
    99  
   100  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	return doReq(url, "POST", contentType, hostHeader, "", "", body, client)
   105  }
   106  
   107  // Delete executes an HTTP DELETE
   108  func Delete(url string, hostHeader string) (*HTTPResponse, error) {
   109  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   110  	if err != nil {
   111  		Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err))
   112  		return nil, err
   113  	}
   114  
   115  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	return doReq(url, "DELETE", "", hostHeader, "", "", nil, client)
   120  }
   121  
   122  // GetVerrazzanoNoRetryHTTPClient returns an Http client configured with the Verrazzano CA cert
   123  func GetVerrazzanoNoRetryHTTPClient(kubeconfigPath string) (*http.Client, error) {
   124  	caCert, err := getVerrazzanoCACert(kubeconfigPath)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	client, err := getHTTPClientWithCABundle(caCert, kubeconfigPath)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	return client, nil
   133  }
   134  
   135  // GetVerrazzanoHTTPClient returns a retryable Http client configured with the Verrazzano CA cert
   136  func GetVerrazzanoHTTPClient(kubeconfigPath string) (*retryablehttp.Client, error) {
   137  	client, err := GetVerrazzanoNoRetryHTTPClient(kubeconfigPath)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	retryableClient := newRetryableHTTPClient(client)
   142  	return retryableClient, nil
   143  }
   144  
   145  // CheckNoServerHeader validates that the response does not include a Server header.
   146  func CheckNoServerHeader(resp *HTTPResponse) bool {
   147  	// HTTP Server headers should never be returned.
   148  	for headerName, headerValues := range resp.Header {
   149  		if strings.EqualFold(headerName, "Server") {
   150  			Log(Error, fmt.Sprintf("Unexpected Server header %v", headerValues))
   151  			return false
   152  		}
   153  	}
   154  
   155  	return true
   156  }
   157  
   158  // CheckStatusAndResponseHeaderAbsent checks that the given header name is not present in the http response, and that the
   159  // response status code is as expected. If the statusCode is <= 0, the status code check is skipped. If
   160  // the badRespHeader is "", the response headers are not checked.
   161  func CheckStatusAndResponseHeaderAbsent(httpClient *retryablehttp.Client, req *retryablehttp.Request, badRespHeader string, statusCode int) error {
   162  	resp, err := httpClient.Do(req)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	io.ReadAll(resp.Body)
   167  	resp.Body.Close()
   168  	if statusCode > 0 {
   169  		if resp.StatusCode != statusCode {
   170  			return fmt.Errorf("Expected status code %d but got %d", statusCode, resp.StatusCode)
   171  		}
   172  	}
   173  	if badRespHeader != "" {
   174  		// Check that the HTTP header we don't want is not present in the response.
   175  		badHeaderLower := strings.ToLower(badRespHeader)
   176  		for headerName, headerValues := range resp.Header {
   177  			if strings.ToLower(headerName) == badHeaderLower {
   178  				errMsg := fmt.Sprintf("Unexpected %s header %v", headerName, headerValues)
   179  				Log(Error, errMsg)
   180  				return fmt.Errorf(errMsg)
   181  			}
   182  		}
   183  	}
   184  	return nil
   185  }
   186  
   187  func EventuallyVerrazzanoRetryableHTTPClient() *retryablehttp.Client {
   188  	var client *retryablehttp.Client
   189  	gomega.Eventually(func() (*retryablehttp.Client, error) {
   190  		var err error
   191  		client, err = GetVerrazzanoRetryableHTTPClient()
   192  		return client, err
   193  	}, waitTimeout, pollingInterval).ShouldNot(gomega.BeNil(), "Unable to get Verrazzano HTTP client")
   194  	return client
   195  }
   196  
   197  // GetVerrazzanoRetryableHTTPClient returns a retryable HTTP client configured with the CA cert
   198  func GetVerrazzanoRetryableHTTPClient() (*retryablehttp.Client, error) {
   199  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   200  	if err != nil {
   201  		Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err))
   202  		return nil, err
   203  	}
   204  
   205  	caCert, err := getVerrazzanoCACert(kubeconfigPath)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	vmiRawClient, err := getHTTPClientWithCABundle(caCert, kubeconfigPath)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	return newRetryableHTTPClient(vmiRawClient), nil
   214  }
   215  
   216  func GetEnvName(kubeconfigPath string) (string, error) {
   217  	vz, err := GetVerrazzanoInstallResourceInClusterV1beta1(kubeconfigPath)
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  	if len(vz.Spec.EnvironmentName) == 0 {
   222  		return defaultEnvName, nil
   223  	}
   224  	return vz.Spec.EnvironmentName, nil
   225  }
   226  
   227  func AssertOauthURLAccessibleAndUnauthorized(httpClient *retryablehttp.Client, url string) bool {
   228  	httpClient.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   229  		Log(Info, fmt.Sprintf("oidcUnauthorized req: %v \nvia: %v\n", req, via))
   230  		return http.ErrUseLastResponse
   231  	}
   232  	resp, err := httpClient.Get(url)
   233  	if err != nil || resp == nil {
   234  		Log(Error, fmt.Sprintf("Failed making request: %v", err))
   235  		return false
   236  	}
   237  	location, err := resp.Location()
   238  	if err != nil {
   239  		Log(Error, fmt.Sprintf("Error getting location from response: %v, error: %v", resp, err))
   240  		return false
   241  	}
   242  
   243  	if location == nil {
   244  		Log(Error, fmt.Sprintf("Response location not found for %v", resp))
   245  		return false
   246  	}
   247  	Log(Info, fmt.Sprintf("oidcUnauthorized %v StatusCode:%v host:%v", url, resp.StatusCode, location.Host))
   248  	return resp.StatusCode == 302 && strings.Contains(location.Host, "keycloak")
   249  }
   250  
   251  func AssertBearerAuthorized(httpClient *retryablehttp.Client, url string) bool {
   252  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   253  	if err != nil {
   254  		Log(Error, fmt.Sprintf("Error getting kubeconfig location: %v", err))
   255  		return false
   256  	}
   257  
   258  	token := EventuallyGetAccessToken(kubeconfigPath, false)
   259  	req, _ := retryablehttp.NewRequest("GET", url, nil)
   260  	if *token != "" {
   261  		bearer := fmt.Sprintf("Bearer %v", *token)
   262  		req.Header.Set("Authorization", bearer)
   263  	}
   264  	resp, err := httpClient.Do(req)
   265  	if err != nil {
   266  		Log(Error, fmt.Sprintf("Failed making request: %v", err))
   267  		return false
   268  	}
   269  	resp.Body.Close()
   270  	Log(Info, fmt.Sprintf("assertBearerAuthorized %v Response:%v Error:%v", url, resp.StatusCode, err))
   271  	return resp.StatusCode == http.StatusOK
   272  }
   273  
   274  // PutWithHostHeader PUTs a request with a specified Host header
   275  func PutWithHostHeader(url, contentType string, hostHeader string, body io.Reader) (*HTTPResponse, error) {
   276  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   277  	if err != nil {
   278  		Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err))
   279  		return nil, err
   280  	}
   281  
   282  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	return doReq(url, "PUT", contentType, hostHeader, "", "", body, client)
   287  }
   288  
   289  // PutWithHostHeaderInCluster PUTs a request with a specified Host header
   290  func PutWithHostHeaderInCluster(url, contentType string, hostHeader string, body io.Reader, kubeconfigPath string) (*HTTPResponse, error) {
   291  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	return doReq(url, "PUT", contentType, hostHeader, "", "", body, client)
   296  }
   297  
   298  // doReq executes an HTTP request with the specified method (GET, POST, DELETE, etc)
   299  func doReq(url, method string, contentType string, hostHeader string, username string, password string,
   300  	body io.Reader, httpClient *retryablehttp.Client, additionalHeaders ...string) (*HTTPResponse, error) {
   301  	req, err := retryablehttp.NewRequest(method, url, body)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	if contentType != "" {
   306  		req.Header.Set("Content-Type", contentType)
   307  	}
   308  	if hostHeader != "" {
   309  		req.Host = hostHeader
   310  	}
   311  
   312  	for _, header := range additionalHeaders {
   313  		splitArray := strings.Split(header, ":")
   314  		if len(splitArray) != 2 {
   315  			return nil, fmt.Errorf("Invalid additional header '%s'. Not in the format key:value", header)
   316  		}
   317  		req.Header.Set(splitArray[0], splitArray[1])
   318  	}
   319  
   320  	if username != "" && password != "" {
   321  		req.SetBasicAuth(username, password)
   322  	}
   323  	resp, err := httpClient.Do(req)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	return ProcessHTTPResponse(resp)
   328  }
   329  
   330  // getHTTPClientWithCABundle returns an HTTP client configured with the provided CA cert
   331  func getHTTPClientWithCABundle(caData []byte, kubeconfigPath string) (*http.Client, error) {
   332  	ca, err := rootCertPoolInCluster(caData, kubeconfigPath)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	return httputil.GetHTTPClientWithRootCA(ca), nil
   337  }
   338  
   339  // getVerrazzanoCACert returns the Verrazzano CA cert in the specified cluster
   340  func getVerrazzanoCACert(kubeconfigPath string) ([]byte, error) {
   341  	cacert, err := GetCACertFromSecret("verrazzano-tls", "verrazzano-system", "ca.crt", kubeconfigPath)
   342  	if err != nil {
   343  		envName, err := GetEnvName(kubeconfigPath)
   344  		if err != nil {
   345  			return nil, err
   346  		}
   347  		return GetCACertFromSecret(envName+"-secret", "verrazzano-system", "ca.crt", kubeconfigPath)
   348  	}
   349  	return cacert, nil
   350  }
   351  
   352  // GetCACertFromSecret returns the CA cert from the specified kubernetes secret in the given cluster
   353  func GetCACertFromSecret(secretName string, namespace string, caKey string, kubeconfigPath string) ([]byte, error) {
   354  	clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  	certSecret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	return certSecret.Data[caKey], nil
   363  }
   364  
   365  // newRetryableHTTPClient returns a new instance of a retryable HTTP client
   366  func newRetryableHTTPClient(client *http.Client) *retryablehttp.Client {
   367  	retryableClient := retryablehttp.NewClient()
   368  	retryableClient.RetryMax = NumRetries
   369  	retryableClient.RetryWaitMin = RetryWaitMinimum
   370  	retryableClient.RetryWaitMax = RetryWaitMaximum
   371  	retryableClient.HTTPClient = client
   372  	retryableClient.CheckRetry = GetRetryPolicy()
   373  	return retryableClient
   374  }
   375  
   376  // rootCertPoolInCluster returns the root cert pool
   377  func rootCertPoolInCluster(caData []byte, kubeconfigPath string) (*x509.CertPool, error) {
   378  	var certPool *x509.CertPool
   379  
   380  	if len(caData) != 0 {
   381  		// if we have caData, use it
   382  		certPool = x509.NewCertPool()
   383  		certPool.AppendCertsFromPEM(caData)
   384  	}
   385  
   386  	env, err := GetACMEEnvironment(kubeconfigPath)
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  	if env == "staging" {
   391  		Log(Info, "Adding Let's Encrypt staging CAs to cert pool")
   392  		// Add the ACME staging CAs if necessary
   393  		if certPool == nil {
   394  			certPool = x509.NewCertPool()
   395  		}
   396  		for i, stagingCA := range getACMEStagingCAs() {
   397  			if len(stagingCA) > 0 {
   398  				Log(Info, fmt.Sprintf("Adding Let's Encrypt staging CA %v", i))
   399  				certPool.AppendCertsFromPEM(stagingCA)
   400  			}
   401  		}
   402  	}
   403  	return certPool, nil
   404  }
   405  
   406  // HasStatus asserts that an HTTPResponse has a given status.
   407  func HasStatus(expected int) types.GomegaMatcher {
   408  	return gomega.WithTransform(func(response *HTTPResponse) int {
   409  		if response == nil {
   410  			return 0
   411  		}
   412  		return response.StatusCode
   413  	}, gomega.Equal(expected))
   414  }
   415  
   416  // BodyContains asserts that an HTTPResponse body contains a given substring.
   417  func BodyContains(expected string) types.GomegaMatcher {
   418  	return gomega.WithTransform(func(response *HTTPResponse) string {
   419  		if response == nil {
   420  			return ""
   421  		}
   422  		return string(response.Body)
   423  	}, gomega.ContainSubstring(expected))
   424  }
   425  
   426  // BodyDoesNotContain asserts that an HTTPResponse body does not contain a given substring.
   427  func BodyDoesNotContain(unexpected string) types.GomegaMatcher {
   428  	return gomega.WithTransform(func(response *HTTPResponse) string { return string(response.Body) }, gomega.Not(gomega.ContainSubstring(unexpected)))
   429  }
   430  
   431  // BodyEquals asserts that an HTTPResponse body equals a given string.
   432  func BodyEquals(expected string) types.GomegaMatcher {
   433  	return gomega.WithTransform(func(response *HTTPResponse) string {
   434  		if response == nil {
   435  			return ""
   436  		}
   437  		return string(response.Body)
   438  	}, gomega.Equal(expected))
   439  }
   440  
   441  // BodyNotEmpty asserts that an HTTPResponse body is not empty.
   442  func BodyNotEmpty() types.GomegaMatcher {
   443  	return gomega.WithTransform(func(response *HTTPResponse) []byte {
   444  		if response == nil {
   445  			return nil
   446  		}
   447  		return response.Body
   448  	}, gomega.Not(gomega.BeEmpty()))
   449  }