From 2c4aed5b775ded3074c0200628e0679048eff03d Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Mon, 25 Oct 2021 17:11:38 +0000 Subject: [PATCH] Reorder credentials precedence so config beats env (#5328) Co-authored-by: megan07 Signed-off-by: Modular Magician --- .changelog/5328.txt | 3 + google/config.go | 10 +- google/provider.go | 59 +++++++----- .../guides/provider_reference.html.markdown | 93 ++++++++++++------- .../guides/version_4_upgrade.html.markdown | 16 ++++ 5 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 .changelog/5328.txt diff --git a/.changelog/5328.txt b/.changelog/5328.txt new file mode 100644 index 00000000000..a28310ecb7b --- /dev/null +++ b/.changelog/5328.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +provider: changed `credentials`, `access_token` precedence so that `credentials` values in configuration take precedence over `access_token` values assigned through environment variables +``` diff --git a/google/config.go b/google/config.go index 0f3da262fdd..80630a12152 100644 --- a/google/config.go +++ b/google/config.go @@ -1043,14 +1043,13 @@ type staticTokenSource struct { // If initialCredentialsOnly is true, don't follow the impersonation settings and return the initial set of creds // instead. func (c *Config) GetCredentials(clientScopes []string, initialCredentialsOnly bool) (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} + token := &oauth2.Token{AccessToken: contents} if c.ImpersonateServiceAccount != "" && !initialCredentialsOnly { opts := []option.ClientOption{option.WithTokenSource(oauth2.StaticTokenSource(token)), option.ImpersonateCredentials(c.ImpersonateServiceAccount, c.ImpersonateServiceAccountDelegates...), option.WithScopes(clientScopes...)} creds, err := transport.Creds(context.TODO(), opts...) @@ -1062,7 +1061,6 @@ func (c *Config) GetCredentials(clientScopes []string, initialCredentialsOnly bo log.Printf("[INFO] Authenticating using configured Google JSON 'access_token'...") log.Printf("[INFO] -- Scopes: %s", clientScopes) - return googleoauth.Credentials{ TokenSource: staticTokenSource{oauth2.StaticTokenSource(token)}, }, nil @@ -1073,6 +1071,7 @@ func (c *Config) GetCredentials(clientScopes []string, initialCredentialsOnly bo if err != nil { return googleoauth.Credentials{}, fmt.Errorf("error loading credentials: %s", err) } + if c.ImpersonateServiceAccount != "" && !initialCredentialsOnly { opts := []option.ClientOption{option.WithCredentialsJSON([]byte(contents)), option.ImpersonateCredentials(c.ImpersonateServiceAccount, c.ImpersonateServiceAccountDelegates...), option.WithScopes(clientScopes...)} creds, err := transport.Creds(context.TODO(), opts...) @@ -1081,6 +1080,7 @@ func (c *Config) GetCredentials(clientScopes []string, initialCredentialsOnly bo } 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) @@ -1097,17 +1097,17 @@ func (c *Config) GetCredentials(clientScopes []string, initialCredentialsOnly bo if err != nil { return googleoauth.Credentials{}, err } - return *creds, nil + return *creds, nil } log.Printf("[INFO] Authenticating using DefaultClient...") log.Printf("[INFO] -- Scopes: %s", clientScopes) - defaultTS, err := googleoauth.DefaultTokenSource(context.Background(), clientScopes...) if err != nil { return googleoauth.Credentials{}, fmt.Errorf("Attempted to load application default credentials since neither `credentials` nor `access_token` was set in the provider block. No credentials loaded. To use your gcloud credentials, run 'gcloud auth application-default login'. Original error: %w", err) } + return googleoauth.Credentials{ TokenSource: defaultTS, }, err diff --git a/google/provider.go b/google/provider.go index f489fc2a126..bc677cc09c8 100644 --- a/google/provider.go +++ b/google/provider.go @@ -35,9 +35,10 @@ func Provider() *schema.Provider { provider := &schema.Provider{ Schema: map[string]*schema.Schema{ "credentials": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validateCredentials, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCredentials, + ConflictsWith: []string{"access_token"}, }, "access_token": { @@ -45,6 +46,7 @@ func Provider() *schema.Provider { Optional: true, ConflictsWith: []string{"credentials"}, }, + "impersonate_service_account": { Type: schema.TypeString, Optional: true, @@ -1308,33 +1310,34 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.RequestReason = v.(string) } - // Search for default credentials - config.Credentials = multiEnvSearch([]string{ - "GOOGLE_CREDENTIALS", - "GOOGLE_CLOUD_KEYFILE_JSON", - "GCLOUD_KEYFILE_JSON", - }) - - config.AccessToken = multiEnvSearch([]string{ - "GOOGLE_OAUTH_ACCESS_TOKEN", - }) - - // Add credential source + // Check for primary credentials in config. Note that if neither is set, ADCs + // will be used if available. if v, ok := d.GetOk("access_token"); ok { 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) + + if v, ok := d.GetOk("credentials"); ok { + config.Credentials = v.(string) } - scopes := d.Get("scopes").([]interface{}) - if len(scopes) > 0 { - config.Scopes = make([]string, len(scopes)) + // only check environment variables if neither value was set in config- this + // means config beats env var in all cases. + if config.AccessToken == "" && config.Credentials == "" { + config.Credentials = multiEnvSearch([]string{ + "GOOGLE_CREDENTIALS", + "GOOGLE_CLOUD_KEYFILE_JSON", + "GCLOUD_KEYFILE_JSON", + }) + + config.AccessToken = multiEnvSearch([]string{ + "GOOGLE_OAUTH_ACCESS_TOKEN", + }) } - for i, scope := range scopes { - config.Scopes[i] = scope.(string) + + // Given that impersonate_service_account is a secondary auth method, it has + // no conflicts to worry about. We pull the env var in a DefaultFunc. + if v, ok := d.GetOk("impersonate_service_account"); ok { + config.ImpersonateServiceAccount = v.(string) } delegates := d.Get("impersonate_service_account_delegates").([]interface{}) @@ -1345,6 +1348,14 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.ImpersonateServiceAccountDelegates[i] = delegate.(string) } + scopes := d.Get("scopes").([]interface{}) + if len(scopes) > 0 { + config.Scopes = make([]string, len(scopes)) + } + for i, scope := range scopes { + config.Scopes[i] = scope.(string) + } + batchCfg, err := expandProviderBatchingConfig(d.Get("batching")) if err != nil { return nil, diag.FromErr(err) diff --git a/website/docs/guides/provider_reference.html.markdown b/website/docs/guides/provider_reference.html.markdown index 02aa0f6334e..0f1220a43f0 100644 --- a/website/docs/guides/provider_reference.html.markdown +++ b/website/docs/guides/provider_reference.html.markdown @@ -60,32 +60,40 @@ provider "google-beta" {} ## Authentication -### Running Terraform on your workstation. +### Primary Authentication -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) by running the command `gcloud auth application-default login`. +#### Running Terraform on your workstation. -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.` +If you are using Terraform on your workstation we recommend that you install +`gcloud` and authenticate using [User Application Default Credentials ("ADCs")](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) +as a primary authentication method. You can enable ADCs by running the command +`gcloud auth application-default login`. -### Running Terraform on Google Cloud +Google Cloud reads the quota project for requests will be read automatically +from the `core/project` value. You can override this project by specifying the +`--project` flag when running `gcloud auth application-default login`. `gcloud` +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.` -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. Ensure that the scope of the VM/Cluster is set to or includes `https://www.googleapis.com/auth/cloud-platform`. +#### Running Terraform on Google Cloud -### Running Terraform outside of Google Cloud +If you are running Terraform in a machine on Google Cloud, you can configure +that instance or cluster to use a [Google Service Account](https://cloud.google.com/compute/docs/authentication). +This allows Terraform to authenticate to Google Cloud without a separate +credential/authentication file. Ensure that the scope of the VM/Cluster is set +to or includes `https://www.googleapis.com/auth/cloud-platform`. -If you are running terraform outside of Google Cloud, generate an external credential configuration file ([example for OIDC based federation](https://cloud.google.com/iam/docs/access-resources-oidc#generate-automatic)) or a service account key file and set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of the JSON file. Terraform will use that file for authentication. In general Terraform supports the full range of authentication options [documented for Google Cloud](https://cloud.google.com/docs/authentication). +#### Running Terraform Outside of Google Cloud -### Disabling mtls authentication +If you are running Terraform outside of Google Cloud, generate an external +credential configuration file ([example for OIDC based federation](https://cloud.google.com/iam/docs/access-resources-oidc#generate-automatic)) +or a service account key file and set the `GOOGLE_APPLICATION_CREDENTIALS` +environment variable to the path of the JSON file. Terraform will use that file +for authentication. Terraform supports the full range of +authentication options [documented for Google Cloud](https://cloud.google.com/docs/authentication). -[mtls authentication](https://google.aip.dev/auth/4114) will soon become enabled by default if your system supports it. To disable mtls authentication at any point set `GOOGLE_API_USE_CLIENT_CERTIFICATE` to `false`. +#### Using Terraform Cloud -### 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. - -### Using Terraform Cloud as the Backend Place your credentials in a Terraform Cloud [environment variable](https://www.terraform.io/docs/cloud/workspaces/variables.html): 1. Create an environment variable called `GOOGLE_CREDENTIALS` in your Terraform Cloud workspace. 2. Remove the newline characters from your JSON key file and then paste the credentials into the environment variable value field. @@ -93,9 +101,22 @@ Place your credentials in a Terraform Cloud [environment variable](https://www.t All runs within the workspace will use the `GOOGLE_CREDENTIALS` variable to authenticate with Google Cloud Platform. +### Impersonating Service Accounts + +Terraform can [impersonate a Google service account](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials), +allowing you to act as an appropriate service account regardless of your primary +authentication mechanism. If you authenticate as a service account, Google Cloud +derives your quota project and permissions from that service account rather than +your primary authentication method, even if your primary authentication method +was another service account. + +A valid primary authentication mechanism must be provided for the impersonation +call, and your primary identity must have the `roles/iam.serviceAccountTokenCreator` +role on the service account you are impersonating. + ## Configuration Reference -The following attributes can be used to configure the provider. The quick +You can use the following attributes to configure the provider. The quick reference should be sufficient for most use cases, but see the full reference if you're interested in more details. Both `google` and `google-beta` share the same configuration. @@ -126,8 +147,7 @@ an access token using the service account key specified in `credentials`. * `access_token` - (Optional) A temporary [OAuth 2.0 access token] obtained from the Google Authorization server, i.e. the `Authorization: Bearer` token used to authenticate HTTP requests to GCP APIs. This is an alternative to `credentials`, -and ignores the `scopes` field. If both are specified, `access_token` will be -used over the `credentials` field. +and ignores the `scopes` field. * `user_project_override` - (Optional) Defaults to `false`. Controls the quota project used in requests to GCP APIs for the purpose of preconditions, quota, @@ -176,8 +196,8 @@ after which a request should be sent. Defaults to 3s. Note that if you increase [manage key files using the Cloud Console]. Your service account key file is used to complete a two-legged OAuth 2.0 flow to obtain access tokens to authenticate with the GCP API as needed; Terraform will use it to reauthenticate -automatically when tokens expire. Alternatively, this can be specified using the -`GOOGLE_CREDENTIALS` environment variable or any of the following ordered +automatically when tokens expire. You can alternatively use the +`GOOGLE_CREDENTIALS` environment variable, or any of the following ordered by precedence. * GOOGLE_CREDENTIALS @@ -201,6 +221,21 @@ for more details. running [`gcloud auth application-default login`][gcloud adc]. --- + +* `access_token` - (Optional) A temporary [OAuth 2.0 access token] obtained from +the Google Authorization server, i.e. the `Authorization: Bearer` token used to +authenticate HTTP requests to GCP APIs. This is an alternative to `credentials`, +and ignores the `scopes` field. You can alternatively use the +`GOOGLE_OAUTH_ACCESS_TOKEN` environment variable. If you specify both with +environment variables, Terraform uses the `access_token` instead of the +`credentials` field. + + -> Terraform cannot renew these access tokens, and they will eventually +expire (default `1 hour`). If Terraform needs access for longer than a token's +lifetime, use a service account key with `credentials` instead. + +--- + * `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. @@ -246,20 +281,6 @@ following ordered by precedence. --- -* `access_token` - (Optional) A temporary [OAuth 2.0 access token] obtained from -the Google Authorization server, i.e. the `Authorization: Bearer` token used to -authenticate HTTP requests to GCP APIs. If both are specified, `access_token` will be -used over the `credentials` field. This is an alternative to `credentials`, -and ignores the `scopes` field. Alternatively, this can be specified using the -`GOOGLE_OAUTH_ACCESS_TOKEN` environment variable. - - -> These access tokens cannot be renewed by Terraform and thus will only - work until they expire. If you anticipate Terraform needing access for - longer than a token's lifetime (default `1 hour`), please use a service - account key with `credentials` instead. - ---- - * `scopes` - (Optional) The list of OAuth 2.0 [scopes] requested when generating an access token using the service account key specified in `credentials`. diff --git a/website/docs/guides/version_4_upgrade.html.markdown b/website/docs/guides/version_4_upgrade.html.markdown index 3eeeffe7425..e417aaeeda8 100644 --- a/website/docs/guides/version_4_upgrade.html.markdown +++ b/website/docs/guides/version_4_upgrade.html.markdown @@ -12,6 +12,7 @@ description: |- - [I accidentally upgraded to 4.0.0, how do I downgrade to `3.X`?](#i-accidentally-upgraded-to-400-how-do-i-downgrade-to-3x) - [Provider Version Configuration](#provider-version-configuration) - [Provider](#provider) + - [`credentials`, `access_token` precedence has changed](#credentials-access_token-precedence-has-changed) - [Redundant default scopes are removed](#redundant-default-scopes-are-removed) - [Runtime Configurator (`runtimeconfig`) resources have been removed from the GA provider](#runtime-configurator-runtimeconfig-resources-have-been-removed-from-the-ga-provider) - [Service account scopes no longer accept `trace-append` or `trace-ro`, use `trace` instead](#service-account-scopes-no-longer-accept-trace-append-or-trace-ro-use-trace-instead) @@ -156,6 +157,21 @@ terraform { ## Provider +### `credentials`, `access_token` precedence has changed + +Terraform can draw values for both the `credentials` and `access_token` from the +config directly or from environment variables. + +In earlier versions of the provider, `access_token` values specified through +environment variables took precedence over `credentials` values specified in +config. From `4.0.0` onwards, config takes precedence over environment variables, +and the `access_token` environment variable takes precedence over the +`credential` environment variable. + +Service account impersonation is unchanged. Terraform will continue to use +the service account if it is specified through an environment variable, even +if `credentials` or `access_token` are specified in config. + ### Redundant default scopes are removed Several default scopes are removed from the provider: