Skip to content

Commit

Permalink
Implement Service Account Impersonation (#4015)
Browse files Browse the repository at this point in the history
* add service account impersonation

* fix conflicts

* add the scopes block removed by error

* add service account impersonation

* undo spacing

* update docs for quota project

* fix typos in docs

* fix typo in test and the docs

* use the new gce client

* add impersonate to accesstoken + test

* fix tokensource typo

* replace the envs used for testing impersonation

* add additional scopes

* typo fix
  • Loading branch information
upodroid authored Oct 15, 2020
1 parent c3e684f commit 8726df5
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 27 deletions.
56 changes: 43 additions & 13 deletions third_party/terraform/utils/config.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -764,19 +767,29 @@ func (c *Config) NewBigTableProjectsInstancesTablesClient(userAgent string) *big

// 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)},
Expand All @@ -788,7 +801,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)
Expand All @@ -799,6 +819,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)

Expand Down
70 changes: 70 additions & 0 deletions third_party/terraform/utils/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 24 additions & 0 deletions third_party/terraform/utils/provider.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -472,6 +485,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{})
Expand All @@ -482,6 +498,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand All @@ -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.

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

---

Expand Down

0 comments on commit 8726df5

Please sign in to comment.