...

Source file src/google.golang.org/api/integration-tests/byoid/integration_test.go

Documentation: google.golang.org/api/integration-tests/byoid

     1  // Copyright 2021 Google LLC.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // To run this test locally, you will need to do the following:
     6  // • Navigate to your Google Cloud Project
     7  // • Get a copy of a Service Account Key File for testing (should be in .json format)
     8  // • If you are unable to obtain an existing key file, create one:
     9  //    • > IAM and Admin > Service Accounts
    10  //    • Under the needed Service Account > Actions > Manage Keys
    11  //    • Add Key > Create New Key
    12  //    • Select JSON, and the click Create
    13  // • Look for an available VM Instance, or create one- > Compute > Compute Engine > VM Instances
    14  // • On the VM Instance, click the SSH Button.  Then upload:
    15  //    • Your Service Account Key File
    16  //    • This script, along with setup.sh
    17  //    • A copy of env.conf, containing the required environment variables (see existing skeleton)/
    18  // • Set your environment variables (Usually this will be `source env.conf`)
    19  // • Ensure that your VM is properly set up to run the integration test e.g.
    20  //    • wget -c https://golang.org/dl/go1.15.2.linux-amd64.tar.gz
    21  //       • Check https://golang.org/dl/for the latest version of Go
    22  //    • sudo tar -C /usr/local -xvzf go1.15.2.linux-amd64.tar.gz
    23  //    • go mod init google.golang.org/api/google-api-go-client
    24  //    • go mod tidy
    25  // • Run setup.sh
    26  // • go test -tags integration`
    27  
    28  package byoid
    29  
    30  import (
    31  	"context"
    32  	"encoding/json"
    33  	"encoding/xml"
    34  	"flag"
    35  	"fmt"
    36  	"io"
    37  	"log"
    38  	"net/http"
    39  	"net/http/httptest"
    40  	"net/url"
    41  	"os"
    42  	"testing"
    43  	"time"
    44  
    45  	"golang.org/x/oauth2/google"
    46  	"google.golang.org/api/dns/v1"
    47  	"google.golang.org/api/idtoken"
    48  	"google.golang.org/api/option"
    49  )
    50  
    51  const (
    52  	envCredentials  = "GOOGLE_APPLICATION_CREDENTIALS"
    53  	envAudienceOIDC = "GCLOUD_TESTS_GOLANG_AUDIENCE_OIDC"
    54  	envAudienceAWS  = "GCLOUD_TESTS_GOLANG_AUDIENCE_AWS"
    55  	envProject      = "GOOGLE_CLOUD_PROJECT"
    56  )
    57  
    58  var (
    59  	oidcAudience string
    60  	awsAudience  string
    61  	oidcToken    string
    62  	clientID     string
    63  	projectID    string
    64  )
    65  
    66  // TestMain contains all of the setup code that needs to be run once before any of the tests are run
    67  func TestMain(m *testing.M) {
    68  	flag.Parse()
    69  	if testing.Short() {
    70  		// This line runs all of our individual tests
    71  		os.Exit(m.Run())
    72  	}
    73  	keyFileName := os.Getenv(envCredentials)
    74  	if keyFileName == "" {
    75  		log.Fatalf("Please set %s to your keyfile", envCredentials)
    76  	}
    77  
    78  	projectID = os.Getenv(envProject)
    79  	if projectID == "" {
    80  		log.Fatalf("Please set %s to the ID of the project", envProject)
    81  	}
    82  
    83  	oidcAudience = os.Getenv(envAudienceOIDC)
    84  	if oidcAudience == "" {
    85  		log.Fatalf("Please set %s to the OIDC Audience", envAudienceOIDC)
    86  	}
    87  
    88  	awsAudience = os.Getenv(envAudienceAWS)
    89  	if awsAudience == "" {
    90  		log.Fatalf("Please set %s to the AWS Audience", envAudienceAWS)
    91  	}
    92  
    93  	var err error
    94  
    95  	clientID, err = getClientID(keyFileName)
    96  	if err != nil {
    97  		log.Fatalf("Error getting Client ID: %v", err)
    98  	}
    99  
   100  	oidcToken, err = generateGoogleToken(keyFileName)
   101  	if err != nil {
   102  		log.Fatalf("Error generating Google token: %v", err)
   103  	}
   104  
   105  	// This line runs all of our individual tests
   106  	os.Exit(m.Run())
   107  }
   108  
   109  // keyFile is a struct to extract the relevant json fields for our ServiceAccount KeyFile
   110  type keyFile struct {
   111  	ClientEmail string `json:"client_email"`
   112  	ClientID    string `json:"client_id"`
   113  }
   114  
   115  func getClientID(keyFileName string) (string, error) {
   116  	kf, err := os.Open(keyFileName)
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  	defer kf.Close()
   121  
   122  	decoder := json.NewDecoder(kf)
   123  	var keyFileSettings keyFile
   124  	if err = decoder.Decode(&keyFileSettings); err != nil {
   125  		return "", err
   126  	}
   127  
   128  	return fmt.Sprintf("projects/-/serviceAccounts/%s", keyFileSettings.ClientEmail), nil
   129  }
   130  
   131  func generateGoogleToken(keyFileName string) (string, error) {
   132  	ts, err := idtoken.NewTokenSource(context.Background(), oidcAudience, option.WithCredentialsFile(keyFileName))
   133  	if err != nil {
   134  		return "", nil
   135  	}
   136  
   137  	token, err := ts.Token()
   138  	if err != nil {
   139  		return "", nil
   140  	}
   141  
   142  	return token.AccessToken, nil
   143  }
   144  
   145  // writeConfig writes a temporary config file to memory, and cleans it up after
   146  // testing code is run.
   147  func writeConfig(t *testing.T, c config, f func(name string)) {
   148  	t.Helper()
   149  
   150  	// Set up config file.
   151  	configFile, err := os.CreateTemp("", "config.json")
   152  	if err != nil {
   153  		t.Fatalf("Error creating config file: %v", err)
   154  	}
   155  	defer os.Remove(configFile.Name())
   156  
   157  	err = json.NewEncoder(configFile).Encode(c)
   158  	if err != nil {
   159  		t.Errorf("Error writing to config file: %v", err)
   160  	}
   161  	configFile.Close()
   162  
   163  	f(configFile.Name())
   164  }
   165  
   166  // testBYOID makes sure that the default credentials works for
   167  // whatever preconditions have been set beforehand
   168  // by using those credentials to run our client libraries.
   169  //
   170  // In each test we will set up whatever preconditions we need,
   171  // and then use this function.
   172  func testBYOID(t *testing.T, c config) {
   173  	t.Helper()
   174  
   175  	writeConfig(t, c, func(name string) {
   176  		// Once the default credentials are obtained,
   177  		// we should be able to access Google Cloud resources.
   178  		dnsService, err := dns.NewService(context.Background(), option.WithCredentialsFile(name))
   179  		if err != nil {
   180  			t.Fatalf("Could not establish DNS Service: %v", err)
   181  		}
   182  
   183  		_, err = dnsService.Projects.Get(projectID).Do()
   184  		if err != nil {
   185  			t.Fatalf("DNS Service failed: %v", err)
   186  		}
   187  	})
   188  }
   189  
   190  // These structs makes writing our config as json to a file much easier.
   191  type config struct {
   192  	Type                           string                          `json:"type"`
   193  	Audience                       string                          `json:"audience"`
   194  	SubjectTokenType               string                          `json:"subject_token_type"`
   195  	TokenURL                       string                          `json:"token_url"`
   196  	ServiceAccountImpersonationURL string                          `json:"service_account_impersonation_url"`
   197  	ServiceAccountImpersonation    serviceAccountImpersonationInfo `json:"service_account_impersonation,omitempty"`
   198  	CredentialSource               credentialSource                `json:"credential_source"`
   199  }
   200  
   201  type serviceAccountImpersonationInfo struct {
   202  	TokenLifetimeSeconds int `json:"token_lifetime_seconds,omitempty"`
   203  }
   204  
   205  type credentialSource struct {
   206  	File                        string           `json:"file,omitempty"`
   207  	URL                         string           `json:"url,omitempty"`
   208  	Executable                  executableConfig `json:"executable,omitempty"`
   209  	EnvironmentID               string           `json:"environment_id,omitempty"`
   210  	RegionURL                   string           `json:"region_url,omitempty"`
   211  	RegionalCredVerificationURL string           `json:"regional_cred_verification_url,omitempty"`
   212  }
   213  
   214  type executableConfig struct {
   215  	Command       string `json:"command,omitempty"`
   216  	TimeoutMillis int    `json:"timeout_millis,omitempty"`
   217  	OutputFile    string `json:"output_file,omitempty"`
   218  }
   219  
   220  // Tests to make sure File based external credentials continues to work.
   221  func TestFileBasedCredentials(t *testing.T) {
   222  	if testing.Short() {
   223  		t.Skip("skipping integration test")
   224  	}
   225  	// Set up Token as a file
   226  	tokenFile, err := os.CreateTemp("", "token.txt")
   227  	if err != nil {
   228  		t.Fatalf("Error creating token file:")
   229  	}
   230  	defer os.Remove(tokenFile.Name())
   231  
   232  	tokenFile.WriteString(oidcToken)
   233  	tokenFile.Close()
   234  
   235  	// Run our test!
   236  	testBYOID(t, config{
   237  		Type:                           "external_account",
   238  		Audience:                       oidcAudience,
   239  		SubjectTokenType:               "urn:ietf:params:oauth:token-type:jwt",
   240  		TokenURL:                       "https://sts.googleapis.com/v1beta/token",
   241  		ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", clientID),
   242  		CredentialSource: credentialSource{
   243  			File: tokenFile.Name(),
   244  		},
   245  	})
   246  }
   247  
   248  // Tests to make sure URL based external credentials work properly.
   249  func TestURLBasedCredentials(t *testing.T) {
   250  	if testing.Short() {
   251  		t.Skip("skipping integration test")
   252  	}
   253  	//Set up a server to return a token
   254  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   255  		if r.Method != "GET" {
   256  			t.Errorf("Unexpected request method, %v is found", r.Method)
   257  		}
   258  		w.Write([]byte(oidcToken))
   259  	}))
   260  
   261  	testBYOID(t, config{
   262  		Type:                           "external_account",
   263  		Audience:                       oidcAudience,
   264  		SubjectTokenType:               "urn:ietf:params:oauth:token-type:jwt",
   265  		TokenURL:                       "https://sts.googleapis.com/v1/token",
   266  		ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", clientID),
   267  		CredentialSource: credentialSource{
   268  			URL: ts.URL,
   269  		},
   270  	})
   271  }
   272  
   273  // Tests to make sure AWS based external credentials work properly.
   274  func TestAWSBasedCredentials(t *testing.T) {
   275  	if testing.Short() {
   276  		t.Skip("skipping integration test")
   277  	}
   278  	data := url.Values{}
   279  	data.Set("audience", clientID)
   280  	data.Set("includeEmail", "true")
   281  
   282  	client, err := google.DefaultClient(context.Background(), "https://www.googleapis.com/auth/cloud-platform")
   283  	if err != nil {
   284  		t.Fatalf("Failed to create default client: %v", err)
   285  	}
   286  	resp, err := client.PostForm(fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateIdToken", clientID), data)
   287  	if err != nil {
   288  		t.Fatalf("Failed to generate an ID token: %v", err)
   289  	}
   290  	if resp.StatusCode != 200 {
   291  		t.Fatalf("Failed to get Google ID token for AWS test: %v", err)
   292  	}
   293  
   294  	var res map[string]interface{}
   295  
   296  	if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
   297  		t.Fatalf("Could not successfully parse response from generateIDToken: %v", err)
   298  	}
   299  	token, ok := res["token"]
   300  	if !ok {
   301  		t.Fatalf("Didn't receieve an ID token back from generateIDToken")
   302  	}
   303  
   304  	data = url.Values{}
   305  	data.Set("Action", "AssumeRoleWithWebIdentity")
   306  	data.Set("Version", "2011-06-15")
   307  	data.Set("DurationSeconds", "3600")
   308  	data.Set("RoleSessionName", os.Getenv("GCLOUD_TESTS_GOLANG_AWS_ROLE_NAME"))
   309  	data.Set("RoleArn", os.Getenv("GCLOUD_TESTS_GOLANG_AWS_ROLE_ID"))
   310  	data.Set("WebIdentityToken", token.(string))
   311  
   312  	resp, err = http.PostForm("https://sts.amazonaws.com/", data)
   313  	if err != nil {
   314  		t.Fatalf("Failed to post data to AWS: %v", err)
   315  	}
   316  	bodyBytes, err := io.ReadAll(resp.Body)
   317  	if err != nil {
   318  		t.Fatalf("Failed to parse response body from AWS: %v", err)
   319  	}
   320  
   321  	var respVars struct {
   322  		SessionToken    string `xml:"AssumeRoleWithWebIdentityResult>Credentials>SessionToken"`
   323  		SecretAccessKey string `xml:"AssumeRoleWithWebIdentityResult>Credentials>SecretAccessKey"`
   324  		AccessKeyID     string `xml:"AssumeRoleWithWebIdentityResult>Credentials>AccessKeyId"`
   325  	}
   326  
   327  	if err = xml.Unmarshal(bodyBytes, &respVars); err != nil {
   328  		t.Fatalf("Failed to unmarshal XML response from AWS.")
   329  	}
   330  
   331  	if respVars.SessionToken == "" || respVars.SecretAccessKey == "" || respVars.AccessKeyID == "" {
   332  		t.Fatalf("Couldn't find the required variables in the response from the AWS server.")
   333  	}
   334  
   335  	currSessTokEnv := os.Getenv("AWS_SESSION_TOKEN")
   336  	defer os.Setenv("AWS_SESSION_TOKEN", currSessTokEnv)
   337  	os.Setenv("AWS_SESSION_TOKEN", respVars.SessionToken)
   338  
   339  	currSecAccKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
   340  	defer os.Setenv("AWS_SECRET_ACCESS_KEY", currSecAccKey)
   341  	os.Setenv("AWS_SECRET_ACCESS_KEY", respVars.SecretAccessKey)
   342  
   343  	currAccKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
   344  	defer os.Setenv("AWS_ACCESS_KEY_ID", currAccKeyID)
   345  	os.Setenv("AWS_ACCESS_KEY_ID", respVars.AccessKeyID)
   346  
   347  	currRegion := os.Getenv("AWS_REGION")
   348  	defer os.Setenv("AWS_REGION", currRegion)
   349  	os.Setenv("AWS_REGION", "us-east-1")
   350  
   351  	testBYOID(t, config{
   352  		Type:                           "external_account",
   353  		Audience:                       awsAudience,
   354  		SubjectTokenType:               "urn:ietf:params:aws:token-type:aws4_request",
   355  		TokenURL:                       "https://sts.googleapis.com/v1/token",
   356  		ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", clientID),
   357  		CredentialSource: credentialSource{
   358  			EnvironmentID:               "aws1",
   359  			RegionalCredVerificationURL: "https://sts.us-east-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
   360  		},
   361  	})
   362  }
   363  
   364  // Tests to make sure executable based external credentials continues to work.
   365  // We're using the same setup as file based external account credentials, and using `cat` as the command
   366  func TestExecutableBasedCredentials(t *testing.T) {
   367  	if testing.Short() {
   368  		t.Skip("skipping integration test")
   369  	}
   370  
   371  	// Set up Script as a executable file
   372  	scriptFile, err := os.CreateTemp("", "script.sh")
   373  	if err != nil {
   374  		t.Fatalf("Error creating token file:")
   375  	}
   376  	defer os.Remove(scriptFile.Name())
   377  
   378  	fmt.Fprintf(scriptFile, `#!/bin/bash
   379  echo "{\"success\":true,\"version\":1,\"expiration_time\":%v,\"token_type\":\"urn:ietf:params:oauth:token-type:jwt\",\"id_token\":\"%v\"}"`,
   380  		time.Now().Add(time.Hour).Unix(), oidcToken)
   381  	scriptFile.Close()
   382  	os.Chmod(scriptFile.Name(), 0700)
   383  
   384  	// Run our test!
   385  	testBYOID(t, config{
   386  		Type:                           "external_account",
   387  		Audience:                       oidcAudience,
   388  		SubjectTokenType:               "urn:ietf:params:oauth:token-type:jwt",
   389  		TokenURL:                       "https://sts.googleapis.com/v1/token",
   390  		ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", clientID),
   391  		CredentialSource: credentialSource{
   392  			Executable: executableConfig{
   393  				Command: scriptFile.Name(),
   394  			},
   395  		},
   396  	})
   397  }
   398  
   399  func TestConfigurableTokenLifetime(t *testing.T) {
   400  	if testing.Short() {
   401  		t.Skip("skipping integration test")
   402  	}
   403  
   404  	// Set up Token as a file
   405  	tokenFile, err := os.CreateTemp("", "token.txt")
   406  	if err != nil {
   407  		t.Fatalf("Error creating token file:")
   408  	}
   409  	defer os.Remove(tokenFile.Name())
   410  
   411  	tokenFile.WriteString(oidcToken)
   412  	tokenFile.Close()
   413  
   414  	const tokenLifetimeSeconds = 2800
   415  	const safetyBuffer = 5
   416  
   417  	writeConfig(t, config{
   418  		Type:                           "external_account",
   419  		Audience:                       oidcAudience,
   420  		SubjectTokenType:               "urn:ietf:params:oauth:token-type:jwt",
   421  		TokenURL:                       "https://sts.googleapis.com/v1/token",
   422  		ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", clientID),
   423  		ServiceAccountImpersonation: serviceAccountImpersonationInfo{
   424  			TokenLifetimeSeconds: tokenLifetimeSeconds,
   425  		},
   426  		CredentialSource: credentialSource{
   427  			File: tokenFile.Name(),
   428  		},
   429  	}, func(filename string) {
   430  		b, err := os.ReadFile(filename)
   431  		if err != nil {
   432  			t.Fatalf("Coudn't read temp config file")
   433  		}
   434  
   435  		creds, err := google.CredentialsFromJSON(context.Background(), b, "https://www.googleapis.com/auth/cloud-platform")
   436  		if err != nil {
   437  			t.Fatalf("Error retrieving credentials")
   438  		}
   439  
   440  		token, err := creds.TokenSource.Token()
   441  		if err != nil {
   442  			t.Fatalf("Error getting token")
   443  		}
   444  
   445  		now := time.Now()
   446  		expiryMax := now.Add((safetyBuffer + tokenLifetimeSeconds) * time.Second)
   447  		expiryMin := now.Add((tokenLifetimeSeconds - safetyBuffer) * time.Second)
   448  		if token.Expiry.Before(expiryMin) || token.Expiry.After(expiryMax) {
   449  			t.Fatalf("Expiry time not set correctly.  Got %v, want %v", token.Expiry, expiryMax)
   450  		}
   451  	})
   452  }
   453  

View as plain text