From d1d1f2fcb262590db848dbdd5a5cd657a22f5890 Mon Sep 17 00:00:00 2001 From: emily Date: Wed, 9 Jan 2019 16:32:42 -0800 Subject: [PATCH] [Terraform] Add more OAuth2 credential options. (#1201) Merged PR #1201. --- third_party/terraform/utils/config.go.erb | 99 +++++++------------ third_party/terraform/utils/config_test.go | 55 +++++++++++ third_party/terraform/utils/provider.go.erb | 31 ++++-- .../docs/provider_reference.html.markdown | 11 +++ 4 files changed, 125 insertions(+), 71 deletions(-) diff --git a/third_party/terraform/utils/config.go.erb b/third_party/terraform/utils/config.go.erb index 831fb16f9383..2ec8fb51f383 100644 --- a/third_party/terraform/utils/config.go.erb +++ b/third_party/terraform/utils/config.go.erb @@ -3,17 +3,15 @@ package google import ( "context" - "encoding/json" "fmt" "log" "net/http" - "strings" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/pathorcontents" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" + googleoauth "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" appengine "google.golang.org/api/appengine/v1" <% unless version == 'ga' -%> @@ -58,6 +56,7 @@ import ( // provider. type Config struct { Credentials string + AccessToken string Project string Region string Zone string @@ -109,7 +108,6 @@ type Config struct { } func (c *Config) loadAndValidate() error { - var account accountFile clientScopes := []string{ "https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/cloud-platform", @@ -117,55 +115,13 @@ func (c *Config) loadAndValidate() error { "https://www.googleapis.com/auth/devstorage.full_control", } - var client *http.Client - var tokenSource oauth2.TokenSource - - if c.Credentials != "" { - contents, _, err := pathorcontents.Read(c.Credentials) - if err != nil { - return fmt.Errorf("Error loading credentials: %s", err) - } - - // Assume account_file is a JSON string - if err := parseJSON(&account, contents); err != nil { - return fmt.Errorf("Error parsing credentials '%s': %s", contents, err) - } - - // Get the token for use in our requests - log.Printf("[INFO] Requesting Google token...") - log.Printf("[INFO] -- Email: %s", account.ClientEmail) - log.Printf("[INFO] -- Scopes: %s", clientScopes) - log.Printf("[INFO] -- Private Key Length: %d", len(account.PrivateKey)) - - conf := jwt.Config{ - Email: account.ClientEmail, - PrivateKey: []byte(account.PrivateKey), - Scopes: clientScopes, - TokenURL: "https://accounts.google.com/o/oauth2/token", - } - - // Initiate an http.Client. The following GET request will be - // authorized and authenticated on the behalf of - // your service account. - client = conf.Client(context.Background()) - - tokenSource = conf.TokenSource(context.Background()) - } else { - log.Printf("[INFO] Authenticating using DefaultClient") - err := error(nil) - client, err = google.DefaultClient(context.Background(), clientScopes...) - if err != nil { - return err - } - - tokenSource, err = google.DefaultTokenSource(context.Background(), clientScopes...) - if err != nil { - return err - } + tokenSource, err := c.getTokenSource(clientScopes) + if err != nil { + return err } - c.tokenSource = tokenSource + client := oauth2.NewClient(context.Background(), tokenSource) client.Transport = logging.NewTransport("Google", client.Transport) terraformVersion := httpclient.UserAgentString() @@ -180,8 +136,6 @@ func (c *Config) loadAndValidate() error { c.client = client c.userAgent = userAgent - var err error - log.Printf("[INFO] Instantiating GCE client...") c.clientCompute, err = compute.New(client) if err != nil { @@ -424,17 +378,36 @@ func (c *Config) loadAndValidate() error { return nil } -// accountFile represents the structure of the account file JSON file. -type accountFile struct { - PrivateKeyId string `json:"private_key_id"` - PrivateKey string `json:"private_key"` - ClientEmail string `json:"client_email"` - ClientId string `json:"client_id"` -} +func (c *Config) getTokenSource(clientScopes []string) (oauth2.TokenSource, error) { + if c.AccessToken != "" { + contents, _, err := pathorcontents.Read(c.AccessToken) + if err != nil { + return nil, fmt.Errorf("Error loading access token: %s", err) + } -func parseJSON(result interface{}, contents string) error { - r := strings.NewReader(contents) - dec := json.NewDecoder(r) + log.Printf("[INFO] Authenticating using configured Google JSON 'access_token'...") + log.Printf("[INFO] -- Scopes: %s", clientScopes) + token := &oauth2.Token{AccessToken: contents} + return oauth2.StaticTokenSource(token), nil + } + + if c.Credentials != "" { + contents, _, err := pathorcontents.Read(c.Credentials) + if err != nil { + return nil, fmt.Errorf("Error loading credentials: %s", err) + } + + creds, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(contents), clientScopes...) + if err != nil { + return nil, fmt.Errorf("Unable to parse credentials from '%s': %s", contents, err) + } + + log.Printf("[INFO] Authenticating using configured Google JSON 'credentials'...") + log.Printf("[INFO] -- Scopes: %s", clientScopes) + return creds.TokenSource, nil + } - return dec.Decode(result) + log.Printf("[INFO] Authenticating using DefaultClient...") + log.Printf("[INFO] -- Scopes: %s", clientScopes) + return googleoauth.DefaultTokenSource(context.Background(), clientScopes...) } diff --git a/third_party/terraform/utils/config_test.go b/third_party/terraform/utils/config_test.go index 648f93a688df..be2a87e42483 100644 --- a/third_party/terraform/utils/config_test.go +++ b/third_party/terraform/utils/config_test.go @@ -1,11 +1,14 @@ package google import ( + "context" + "golang.org/x/oauth2/google" "io/ioutil" "testing" ) const testFakeCredentialsPath = "./test-fixtures/fake_account.json" +const testOauthScope = "https://www.googleapis.com/auth/compute" func TestConfigLoadAndValidate_accountFilePath(t *testing.T) { config := Config{ @@ -48,3 +51,55 @@ func TestConfigLoadAndValidate_accountFileJSONInvalid(t *testing.T) { t.Fatalf("expected error, but got nil") } } + +func TestAccConfigLoadValidate_credentials(t *testing.T) { + creds := getTestCredsFromEnv() + proj := getTestProjectFromEnv() + + config := Config{ + Credentials: creds, + Project: proj, + Region: "us-central1", + } + + err := config.loadAndValidate() + if err != nil { + t.Fatalf("error: %v", err) + } + + _, err = config.clientCompute.Zones.Get(proj, "us-central1-a").Do() + if err != nil { + t.Fatalf("expected call with loaded config client to work, got error: %s", err) + } +} + +func TestAccConfigLoadValidate_accessToken(t *testing.T) { + creds := getTestCredsFromEnv() + proj := getTestProjectFromEnv() + + c, err := google.CredentialsFromJSON(context.Background(), []byte(creds), testOauthScope) + 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, + Project: proj, + Region: "us-central1", + } + + err = config.loadAndValidate() + if err != nil { + t.Fatalf("error: %v", err) + } + + _, err = config.clientCompute.Zones.Get(proj, "us-central1-a").Do() + if err != nil { + t.Fatalf("expected API call with loaded config to work, got error: %s", err) + } +} diff --git a/third_party/terraform/utils/provider.go.erb b/third_party/terraform/utils/provider.go.erb index 4043f08bc6c6..d5718b9536a8 100644 --- a/third_party/terraform/utils/provider.go.erb +++ b/third_party/terraform/utils/provider.go.erb @@ -11,6 +11,8 @@ import ( "github.com/hashicorp/terraform/helper/mutexkv" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + + googleoauth "golang.org/x/oauth2/google" ) // Global MutexKV @@ -31,6 +33,15 @@ func Provider() terraform.ResourceProvider { ValidateFunc: validateCredentials, }, + "access_token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_OAUTH_ACCESS_TOKEN", + }, nil), + ConflictsWith: []string{"credentials"}, + }, + "project": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -263,12 +274,17 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { } func providerConfigure(d *schema.ResourceData) (interface{}, error) { - credentials := d.Get("credentials").(string) config := Config{ - Credentials: credentials, - Project: d.Get("project").(string), - Region: d.Get("region").(string), - Zone: d.Get("zone").(string), + Project: d.Get("project").(string), + Region: d.Get("region").(string), + Zone: d.Get("zone").(string), + } + + // Add credential source + 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 err := config.loadAndValidate(); err != nil { @@ -287,10 +303,9 @@ func validateCredentials(v interface{}, k string) (warnings []string, errors []e if _, err := os.Stat(creds); err == nil { return } - var account accountFile - if err := json.Unmarshal([]byte(creds), &account); err != nil { + if _, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(creds)); err != nil { errors = append(errors, - fmt.Errorf("credentials are not valid JSON '%s': %s", creds, err)) + fmt.Errorf("JSON credentials in %q are not valid: %s", creds, err)) } return diff --git a/third_party/terraform/website/docs/provider_reference.html.markdown b/third_party/terraform/website/docs/provider_reference.html.markdown index 2182924132bc..fca6603f3518 100644 --- a/third_party/terraform/website/docs/provider_reference.html.markdown +++ b/third_party/terraform/website/docs/provider_reference.html.markdown @@ -95,6 +95,17 @@ share the same configuration. only be used when running Terraform from within [certain GCP resources](https://cloud.google.com/docs/authentication/production#obtaining_credentials_on_compute_engine_kubernetes_engine_app_engine_flexible_environment_and_cloud_functions). Credentials obtained through `gcloud` are not guaranteed to work for all APIs. +* `access_token` - (Optional) An temporary [OAuth 2.0 access token](https://developers.google.com/identity/protocols/OAuth2) + obtained from the Google Authorization server, i.e. the + `Authorization: Bearer` token used to authenticate Google API HTTP requests. + + Access tokens can also be specified using any of the following environment + variables (listed in order of precedence): + + * `GOOGLE_OAUTH_ACCESS_TOKEN` + + -> These access tokens cannot be renewed by Terraform and thus will only work for at most 1 hour. If you anticipate Terraform needing access for more than one hour per run, please use `credentials` instead. Credentials are used to complete a two-legged OAuth 2.0 flow on your behalf to obtain access tokens and can be used renew or reauthenticate for tokens as needed. + * `project` - (Optional) The ID of the project to apply any resources to. This can also be specified using any of the following environment variables (listed in order of precedence):