diff --git a/third_party/terraform/utils/config.go.erb b/third_party/terraform/utils/config.go.erb index ae1be1440574..479a43d03eab 100644 --- a/third_party/terraform/utils/config.go.erb +++ b/third_party/terraform/utils/config.go.erb @@ -58,6 +58,7 @@ import ( sqladmin "google.golang.org/api/sqladmin/v1beta4" "google.golang.org/api/storage/v1" "google.golang.org/api/storagetransfer/v1" + "google.golang.org/api/transport" ) type providerMeta struct { @@ -67,16 +68,18 @@ type providerMeta struct { // Config is the configuration structure used to instantiate the Google // provider. type Config struct { - Credentials string - AccessToken string - Project string - BillingProject string - Region string - Zone string - Scopes []string - BatchingConfig *batchingConfig - UserProjectOverride bool - RequestTimeout time.Duration + AccessToken string + Credentials string + ImpersonateServiceAccount string + ImpersonateServiceAccountDelegates []string + Project string + Region string + BillingProject string + Zone string + Scopes []string + BatchingConfig *batchingConfig + UserProjectOverride bool + RequestTimeout time.Duration // PollInterval is passed to resource.StateChangeConf in common_operation.go // It controls the interval at which we poll for successful operations PollInterval time.Duration @@ -711,19 +714,29 @@ func (c *Config) NewServiceUsageClient(userAgent string) *serviceusage.Service { // staticTokenSource is used to be able to identify static token sources without reflection. type staticTokenSource struct { - oauth2.TokenSource + oauth2.TokenSource } func (c *Config) GetCredentials(clientScopes []string) (googleoauth.Credentials, error) { + if c.AccessToken != "" { contents, _, err := pathOrContents(c.AccessToken) if err != nil { return googleoauth.Credentials{}, fmt.Errorf("Error loading access token: %s", err) } + token := &oauth2.Token{AccessToken: contents} + + if c.ImpersonateServiceAccount != "" { + opts := []option.ClientOption{option.WithTokenSource(oauth2.StaticTokenSource(token)), option.ImpersonateCredentials(c.ImpersonateServiceAccount, c.ImpersonateServiceAccountDelegates...), option.WithScopes(clientScopes...)} + creds, err := transport.Creds(context.TODO(), opts...) + if err != nil { + return googleoauth.Credentials{}, err + } + return *creds, nil + } log.Printf("[INFO] Authenticating using configured Google JSON 'access_token'...") log.Printf("[INFO] -- Scopes: %s", clientScopes) - token := &oauth2.Token{AccessToken: contents} return googleoauth.Credentials{ TokenSource: staticTokenSource{oauth2.StaticTokenSource(token)}, @@ -735,7 +748,14 @@ func (c *Config) GetCredentials(clientScopes []string) (googleoauth.Credentials, if err != nil { return googleoauth.Credentials{}, fmt.Errorf("error loading credentials: %s", err) } - + if c.ImpersonateServiceAccount != "" { + opts := []option.ClientOption{option.WithCredentialsJSON([]byte(contents)), option.ImpersonateCredentials(c.ImpersonateServiceAccount, c.ImpersonateServiceAccountDelegates...), option.WithScopes(clientScopes...)} + creds, err := transport.Creds(context.TODO(), opts...) + if err != nil { + return googleoauth.Credentials{}, err + } + return *creds, nil + } creds, err := googleoauth.CredentialsFromJSON(c.context, []byte(contents), clientScopes...) if err != nil { return googleoauth.Credentials{}, fmt.Errorf("unable to parse credentials from '%s': %s", contents, err) @@ -746,6 +766,16 @@ func (c *Config) GetCredentials(clientScopes []string) (googleoauth.Credentials, return *creds, nil } + if c.ImpersonateServiceAccount != "" { + opts := option.ImpersonateCredentials(c.ImpersonateServiceAccount, c.ImpersonateServiceAccountDelegates...) + creds, err := transport.Creds(context.TODO(), opts, option.WithScopes(clientScopes...)) + if err != nil { + return googleoauth.Credentials{}, err + } + return *creds, nil + + } + log.Printf("[INFO] Authenticating using DefaultClient...") log.Printf("[INFO] -- Scopes: %s", clientScopes) diff --git a/third_party/terraform/utils/config_test.go b/third_party/terraform/utils/config_test.go index e529c1586f33..21dd0532d6ed 100644 --- a/third_party/terraform/utils/config_test.go +++ b/third_party/terraform/utils/config_test.go @@ -90,6 +90,76 @@ func TestAccConfigLoadValidate_credentials(t *testing.T) { } } +func TestAccConfigLoadValidate_impersonated(t *testing.T) { + if os.Getenv(TestEnvVar) == "" { + t.Skip(fmt.Sprintf("Network access not allowed; use %s=1 to enable", TestEnvVar)) + } + testAccPreCheck(t) + + serviceaccount := multiEnvSearch([]string{"IMPERSONATE_SERVICE_ACCOUNT_ACCTEST"}) + creds := getTestCredsFromEnv() + proj := getTestProjectFromEnv() + + config := &Config{ + Credentials: creds, + ImpersonateServiceAccount: serviceaccount, + Project: proj, + Region: "us-central1", + } + + ConfigureBasePaths(config) + + err := config.LoadAndValidate(context.Background()) + if err != nil { + t.Fatalf("error: %v", err) + } + + _, err = config.NewComputeClient(config.userAgent).Zones.Get(proj, "us-central1-a").Do() + if err != nil { + t.Fatalf("expected API call with loaded config to work, got error: %s", err) + } +} + +func TestAccConfigLoadValidate_accessTokenImpersonated(t *testing.T) { + if os.Getenv(TestEnvVar) == "" { + t.Skip(fmt.Sprintf("Network access not allowed; use %s=1 to enable", TestEnvVar)) + } + testAccPreCheck(t) + + creds := getTestCredsFromEnv() + proj := getTestProjectFromEnv() + serviceaccount := multiEnvSearch([]string{"IMPERSONATE_SERVICE_ACCOUNT_ACCTEST"}) + + c, err := google.CredentialsFromJSON(context.Background(), []byte(creds), DefaultClientScopes...) + if err != nil { + t.Fatalf("invalid test credentials: %s", err) + } + + token, err := c.TokenSource.Token() + if err != nil { + t.Fatalf("Unable to generate test access token: %s", err) + } + + config := &Config{ + AccessToken: token.AccessToken, + ImpersonateServiceAccount: serviceaccount, + Project: proj, + Region: "us-central1", + } + + ConfigureBasePaths(config) + + err = config.LoadAndValidate(context.Background()) + if err != nil { + t.Fatalf("error: %v", err) + } + + _, err = config.NewComputeClient(config.userAgent).Zones.Get(proj, "us-central1-a").Do() + if err != nil { + t.Fatalf("expected API call with loaded config to work, got error: %s", err) + } +} + func TestAccConfigLoadValidate_accessToken(t *testing.T) { if os.Getenv(TestEnvVar) == "" { t.Skip(fmt.Sprintf("Network access not allowed; use %s=1 to enable", TestEnvVar)) diff --git a/third_party/terraform/utils/provider.go.erb b/third_party/terraform/utils/provider.go.erb index f039203283f2..6f3c0690d294 100644 --- a/third_party/terraform/utils/provider.go.erb +++ b/third_party/terraform/utils/provider.go.erb @@ -41,6 +41,19 @@ func Provider() *schema.Provider { }, nil), ConflictsWith: []string{"credentials"}, }, + "impersonate_service_account": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", + }, nil), + }, + + "impersonate_service_account_delegates": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "project": &schema.Schema{ Type: schema.TypeString, @@ -468,6 +481,9 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.AccessToken = v.(string) } else if v, ok := d.GetOk("credentials"); ok { config.Credentials = v.(string) + } + if v, ok := d.GetOk("impersonate_service_account"); ok { + config.ImpersonateServiceAccount = v.(string) } scopes := d.Get("scopes").([]interface{}) @@ -478,6 +494,14 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.Scopes[i] = scope.(string) } + delegates := d.Get("impersonate_service_account_delegates").([]interface{}) + if len(delegates) > 0 { + config.ImpersonateServiceAccountDelegates = make([]string, len(delegates)) + } + for i, delegate := range delegates { + config.ImpersonateServiceAccountDelegates[i] = delegate.(string) + } + batchCfg, err := expandProviderBatchingConfig(d.Get("batching")) if err != nil { return nil, diag.FromErr(err) diff --git a/third_party/terraform/website/docs/guides/provider_reference.html.markdown b/third_party/terraform/website/docs/guides/provider_reference.html.markdown index 1e45adfe1525..63cfe899dfa0 100644 --- a/third_party/terraform/website/docs/guides/provider_reference.html.markdown +++ b/third_party/terraform/website/docs/guides/provider_reference.html.markdown @@ -18,7 +18,6 @@ location (`zone` and/or `region`) for your resources. ```hcl provider "google" { - credentials = file("account.json") project = "my-project-id" region = "us-central1" zone = "us-central1-c" @@ -27,7 +26,6 @@ provider "google" { ```hcl provider "google-beta" { - credentials = file("account.json") project = "my-project-id" region = "us-central1" zone = "us-central1-c" @@ -62,6 +60,29 @@ resource "google_compute_instance" "beta-instance" { provider "google-beta" {} ``` +## Authentication + +### Running Terraform on your workstation. + +If you are using terraform on your workstation, you will need to install the Google Cloud SDK and authenticate using [User Application Default +Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default). + +A quota project must be set which gcloud automatically reads from the `core/project` value. You can override this project by specifying `--project` flag when running `gcloud auth application-default login`. The SDK should return this message if you have set the correct billing project. `Quota project "your-project" was added to ADC which can be used by Google client libraries for billing and quota.` + +### Running Terraform on Google Cloud + +If you are running terraform on Google Cloud, you can configure that instance or cluster to use a [Google Service +Account](https://cloud.google.com/compute/docs/authentication). This will allow Terraform to authenticate to Google Cloud without having to bake in a separate +credential/authentication file. Make sure that the scope of the VM/Cluster is set to cloud-platform. + +### Running Terraform outside of Google Cloud + +If you are running terraform outside of Google Cloud, generate a service account key and set the `GOOGLE_APPPLICATION_CREDENTIALS` environment variable to +the path of the service account key. Terraform will use that key for authentication. + +### Impersonating Service Accounts + +Terraform can impersonate a Google Service Account as described [here](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials). A valid credential must be provided as mentioned in the earlier section and that identity must have the `roles/iam.serviceAccountTokenCreator` role on the service account you are impersonating. ## Configuration Reference @@ -72,15 +93,6 @@ same configuration. ### Quick Reference -* `credentials` - (Optional) Either the path to or the contents of a -[service account key file] in JSON format. You can -[manage key files using the Cloud Console]. If not provided, the -application default credentials will be used. You can configure -Application Default Credentials on your personal machine by -running `gcloud auth application-default login`. If -terraform is running on a GCP machine, and this value is unset, -it will automatically use that machine's configured service account. - * `project` - (Optional) The default project to manage resources in. If another project is specified on a resource, it will take precedence. @@ -91,6 +103,13 @@ region is specified on a regional resource, it will take precedence. zone should be within the default region you specified. If another zone is specified on a zonal resource, it will take precedence. +* `impersonate_service_account` - (Optional) The service account to impersonate for all Google API Calls. +You must have `roles/iam.serviceAccountTokenCreator` role on that account for the impersonation to succeed. + +* `credentials` - (Optional) Either the path to or the contents of a +[service account key file] in JSON format. You can +[manage key files using the Cloud Console]. If not provided, the +application default credentials will be used. --- * `scopes` - (Optional) The list of OAuth 2.0 [scopes] requested when generating @@ -166,9 +185,16 @@ are automatically available. See for more details. * On your computer, you can make your Google identity available by -running [`gcloud auth application-default login`][gcloud adc]. This -approach isn't recommended- some APIs are not compatible with -credentials obtained through `gcloud`. +running [`gcloud auth application-default login`][gcloud adc]. + +--- +* `impersonate_service_account` - (Optional) The service account to impersonate for all Google API Calls. +You must have `roles/iam.serviceAccountTokenCreator` role on that account for the impersonation to succeed. +If you are using a delegation chain, you can specify that using the `impersonate_service_account_delegates` field. +Alternatively, this can be specified using the `GOOGLE_IMPERSONATE_SERVICE_ACCOUNT` environment +variable. + +* `impersonate_service_account_delegates` - (Optional) The delegation chain for an impersonating a service account as described [here](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-delegated). ---