Skip to content

Commit

Permalink
Add OAuth2 access_token and client config as credential options. (#2838)
Browse files Browse the repository at this point in the history
Signed-off-by: Modular Magician <[email protected]>
  • Loading branch information
modular-magician authored and emilymye committed Jan 10, 2019
1 parent cbfef5e commit b9cedc6
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 73 deletions.
95 changes: 31 additions & 64 deletions google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ package google

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"

"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/helper/pathorcontents"
"github.com/hashicorp/terraform/httpclient"
"github.com/terraform-providers/terraform-provider-google/version"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
googleoauth "golang.org/x/oauth2/google"
appengine "google.golang.org/api/appengine/v1"
"google.golang.org/api/bigquery/v2"
"google.golang.org/api/cloudbilling/v1"
Expand Down Expand Up @@ -53,6 +50,7 @@ import (
// provider.
type Config struct {
Credentials string
AccessToken string
Project string
Region string
Zone string
Expand Down Expand Up @@ -98,63 +96,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 @@ -165,8 +120,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 @@ -391,17 +344,31 @@ 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 != "" {
log.Printf("[INFO] Using configured Google access token (length %d)", len(c.AccessToken))
log.Printf("[INFO] -- Scopes: %s", clientScopes)
token := &oauth2.Token{AccessToken: c.AccessToken}
return oauth2.StaticTokenSource(token), nil
}

func parseJSON(result interface{}, contents string) error {
r := strings.NewReader(contents)
dec := json.NewDecoder(r)
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] Requesting Google token using Credential File %q...", c.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...)
}
15 changes: 15 additions & 0 deletions google/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@ func TestConfigLoadAndValidate_accountFileJSONInvalid(t *testing.T) {
t.Fatalf("expected error, but got nil")
}
}

func TestConfigLoadValidate_accessToken(t *testing.T) {
accessToken := getTestAccessTokenFromEnv(t)

config := Config{
AccessToken: accessToken,
Project: "my-gce-project",
Region: "us-central1",
}

err := config.loadAndValidate()
if err != nil {
t.Fatalf("error: %v", err)
}
}
30 changes: 21 additions & 9 deletions google/provider.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package google

import (
"encoding/json"
"context"
"fmt"
"os"

"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 @@ -28,6 +30,12 @@ func Provider() terraform.ResourceProvider {
ValidateFunc: validateCredentials,
},

"access_token": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"credentials"},
},

"project": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -244,12 +252,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 @@ -268,10 +281,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
9 changes: 9 additions & 0 deletions google/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ var billingAccountEnvVars = []string{
"GOOGLE_BILLING_ACCOUNT",
}

var accessTokenEnvVars = []string{
"GOOGLE_OAUTH2_ACCESS_TOKEN",
}

func init() {
testAccProvider = Provider().(*schema.Provider)
testAccRandomProvider = random.Provider().(*schema.Provider)
Expand Down Expand Up @@ -202,6 +206,11 @@ func getTestServiceAccountFromEnv(t *testing.T) string {
return multiEnvSearch(serviceAccountEnvVars)
}

func getTestAccessTokenFromEnv(t *testing.T) string {
skipIfEnvNotSet(t, accessTokenEnvVars...)
return multiEnvSearch(accessTokenEnvVars)
}

func multiEnvSearch(ks []string) string {
for _, k := range ks {
if v := os.Getenv(k); v != "" {
Expand Down

0 comments on commit b9cedc6

Please sign in to comment.