Skip to content

Commit

Permalink
[Terraform] Add more OAuth2 credential options. (#1201)
Browse files Browse the repository at this point in the history
Merged PR #1201.
  • Loading branch information
emilymye authored and modular-magician committed Jan 10, 2019
1 parent b5f9478 commit d1d1f2f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 71 deletions.
99 changes: 36 additions & 63 deletions third_party/terraform/utils/config.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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' -%>
Expand Down Expand Up @@ -58,6 +56,7 @@ import (
// provider.
type Config struct {
Credentials string
AccessToken string
Project string
Region string
Zone string
Expand Down Expand Up @@ -109,63 +108,20 @@ 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",
"https://www.googleapis.com/auth/ndev.clouddns.readwrite",
"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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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...)
}
55 changes: 55 additions & 0 deletions third_party/terraform/utils/config_test.go
Original file line number Diff line number Diff line change
@@ -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{
Expand Down Expand Up @@ -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)
}
}
31 changes: 23 additions & 8 deletions third_party/terraform/utils/provider.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit d1d1f2f

Please sign in to comment.