Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorder credentials precedence so config beats env #10393

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/5328.txt
Original file line number Diff line number Diff line change
@@ -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
```
10 changes: 5 additions & 5 deletions google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand All @@ -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
Expand All @@ -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...)
Expand All @@ -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)
Expand All @@ -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
Expand Down
59 changes: 35 additions & 24 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ 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": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"credentials"},
},

"impersonate_service_account": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -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{})
Expand All @@ -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)
Expand Down
93 changes: 57 additions & 36 deletions website/docs/guides/provider_reference.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -60,42 +60,63 @@ 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.
3. Mark the variable as **Sensitive** and click **Save variable**.

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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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`.

Expand Down
16 changes: 16 additions & 0 deletions website/docs/guides/version_4_upgrade.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down