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

[Terraform] Add more OAuth2 credential options. #1201

Merged
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
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 {
emilymye marked this conversation as resolved.
Show resolved Hide resolved
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