From f6093e37b6cb4092101a298aba5d794eb570757f Mon Sep 17 00:00:00 2001 From: Ross Light Date: Mon, 7 Nov 2016 13:06:47 -0800 Subject: [PATCH] google: add DefaultCredentials function This new function allows reading the project ID from a service account JSON file without an additional disk read. Change-Id: I1f03ca3ca39a2ae3bd6524367c17761b0f08de45 Reviewed-on: https://go-review.googlesource.com/32876 Reviewed-by: Jaana Burcu Dogan --- google/appengine.go | 3 ++ google/appengine_hook.go | 1 + google/appenginevm_hook.go | 1 + google/default.go | 74 ++++++++++++++++++++++---------------- google/google.go | 1 + 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/google/appengine.go b/google/appengine.go index dc993ef..4243f4c 100644 --- a/google/appengine.go +++ b/google/appengine.go @@ -20,6 +20,9 @@ var appengineVM bool // Set at init time by appengine_hook.go. If nil, we're not on App Engine. var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error) +// Set at init time by appengine_hook.go. If nil, we're not on App Engine. +var appengineAppIDFunc func(c context.Context) string + // AppEngineTokenSource returns a token source that fetches tokens // issued to the current App Engine application's service account. // If you are implementing a 3-legged OAuth 2.0 flow on App Engine diff --git a/google/appengine_hook.go b/google/appengine_hook.go index 4f42c8b..6f66411 100644 --- a/google/appengine_hook.go +++ b/google/appengine_hook.go @@ -10,4 +10,5 @@ import "google.golang.org/appengine" func init() { appengineTokenFunc = appengine.AccessToken + appengineAppIDFunc = appengine.AppID } diff --git a/google/appenginevm_hook.go b/google/appenginevm_hook.go index 633611c..1074780 100644 --- a/google/appenginevm_hook.go +++ b/google/appenginevm_hook.go @@ -11,4 +11,5 @@ import "google.golang.org/appengine" func init() { appengineVM = true appengineTokenFunc = appengine.AccessToken + appengineAppIDFunc = appengine.AppID } diff --git a/google/default.go b/google/default.go index c572d1a..b45e796 100644 --- a/google/default.go +++ b/google/default.go @@ -18,16 +18,16 @@ import ( "golang.org/x/oauth2" ) -// DefaultClient returns an HTTP Client that uses the -// DefaultTokenSource to obtain authentication credentials. -// -// This client should be used when developing services -// that run on Google App Engine or Google Compute Engine -// and use "Application Default Credentials." -// +// DefaultCredentials holds "Application Default Credentials". // For more details, see: // https://developers.google.com/accounts/docs/application-default-credentials -// +type DefaultCredentials struct { + ProjectID string // may be empty + TokenSource oauth2.TokenSource +} + +// DefaultClient returns an HTTP Client that uses the +// DefaultTokenSource to obtain authentication credentials. func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { ts, err := DefaultTokenSource(ctx, scope...) if err != nil { @@ -36,8 +36,18 @@ func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { return oauth2.NewClient(ctx, ts), nil } -// DefaultTokenSource is a token source that uses +// DefaultTokenSource returns the token source for // "Application Default Credentials". +// It is a shortcut for FindDefaultCredentials(ctx, scope).TokenSource. +func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) { + creds, err := FindDefaultCredentials(ctx, scope...) + if err != nil { + return nil, err + } + return creds.TokenSource, nil +} + +// FindDefaultCredentials searches for "Application Default Credentials". // // It looks for credentials in the following places, // preferring the first location found: @@ -51,45 +61,40 @@ func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { // 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches // credentials from the metadata server. // (In this final case any provided scopes are ignored.) -// -// For more details, see: -// https://developers.google.com/accounts/docs/application-default-credentials -// -func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) { +func FindDefaultCredentials(ctx context.Context, scope ...string) (*DefaultCredentials, error) { // First, try the environment variable. const envVar = "GOOGLE_APPLICATION_CREDENTIALS" if filename := os.Getenv(envVar); filename != "" { - ts, err := tokenSourceFromFile(ctx, filename, scope) + creds, err := readCredentialsFile(ctx, filename, scope) if err != nil { return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err) } - return ts, nil + return creds, nil } // Second, try a well-known file. filename := wellKnownFile() - _, err := os.Stat(filename) - if err == nil { - ts, err2 := tokenSourceFromFile(ctx, filename, scope) - if err2 == nil { - return ts, nil - } - err = err2 - } else if os.IsNotExist(err) { - err = nil // ignore this error - } - if err != nil { + if creds, err := readCredentialsFile(ctx, filename, scope); err == nil { + return creds, nil + } else if !os.IsNotExist(err) { return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) } // Third, if we're on Google App Engine use those credentials. if appengineTokenFunc != nil && !appengineVM { - return AppEngineTokenSource(ctx, scope...), nil + return &DefaultCredentials{ + ProjectID: appengineAppIDFunc(ctx), + TokenSource: AppEngineTokenSource(ctx, scope...), + }, nil } // Fourth, if we're on Google Compute Engine use the metadata server. if metadata.OnGCE() { - return ComputeTokenSource(""), nil + id, _ := metadata.ProjectID() + return &DefaultCredentials{ + ProjectID: id, + TokenSource: ComputeTokenSource(""), + }, nil } // None are found; return helpful error. @@ -105,7 +110,7 @@ func wellKnownFile() string { return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f) } -func tokenSourceFromFile(ctx context.Context, filename string, scopes []string) (oauth2.TokenSource, error) { +func readCredentialsFile(ctx context.Context, filename string, scopes []string) (*DefaultCredentials, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err @@ -114,5 +119,12 @@ func tokenSourceFromFile(ctx context.Context, filename string, scopes []string) if err := json.Unmarshal(b, &f); err != nil { return nil, err } - return f.tokenSource(ctx, scopes) + ts, err := f.tokenSource(ctx, append([]string(nil), scopes...)) + if err != nil { + return nil, err + } + return &DefaultCredentials{ + ProjectID: f.ProjectID, + TokenSource: ts, + }, nil } diff --git a/google/google.go b/google/google.go index 0704732..66a8b0e 100644 --- a/google/google.go +++ b/google/google.go @@ -112,6 +112,7 @@ type credentialsFile struct { PrivateKeyID string `json:"private_key_id"` PrivateKey string `json:"private_key"` TokenURL string `json:"token_uri"` + ProjectID string `json:"project_id"` // User Credential fields // (These typically come from gcloud auth.)