| // Copyright 2015 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package google | |
| import ( | |
| "bufio" | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "os" | |
| "os/user" | |
| "path/filepath" | |
| "runtime" | |
| "strings" | |
| "time" | |
| "golang.org/x/oauth2" | |
| ) | |
| type sdkCredentials struct { | |
| Data []struct { | |
| Credential struct { | |
| ClientID string `json:"client_id"` | |
| ClientSecret string `json:"client_secret"` | |
| AccessToken string `json:"access_token"` | |
| RefreshToken string `json:"refresh_token"` | |
| TokenExpiry *time.Time `json:"token_expiry"` | |
| } `json:"credential"` | |
| Key struct { | |
| Account string `json:"account"` | |
| Scope string `json:"scope"` | |
| } `json:"key"` | |
| } | |
| } | |
| // An SDKConfig provides access to tokens from an account already | |
| // authorized via the Google Cloud SDK. | |
| type SDKConfig struct { | |
| conf oauth2.Config | |
| initialToken *oauth2.Token | |
| } | |
| // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK | |
| // account. If account is empty, the account currently active in | |
| // Google Cloud SDK properties is used. | |
| // Google Cloud SDK credentials must be created by running `gcloud auth` | |
| // before using this function. | |
| // The Google Cloud SDK is available at https://cloud.google.com/sdk/. | |
| func NewSDKConfig(account string) (*SDKConfig, error) { | |
| configPath, err := sdkConfigPath() | |
| if err != nil { | |
| return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err) | |
| } | |
| credentialsPath := filepath.Join(configPath, "credentials") | |
| f, err := os.Open(credentialsPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err) | |
| } | |
| defer f.Close() | |
| var c sdkCredentials | |
| if err := json.NewDecoder(f).Decode(&c); err != nil { | |
| return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err) | |
| } | |
| if len(c.Data) == 0 { | |
| return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath) | |
| } | |
| if account == "" { | |
| propertiesPath := filepath.Join(configPath, "properties") | |
| f, err := os.Open(propertiesPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err) | |
| } | |
| defer f.Close() | |
| ini, err := parseINI(f) | |
| if err != nil { | |
| return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err) | |
| } | |
| core, ok := ini["core"] | |
| if !ok { | |
| return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini) | |
| } | |
| active, ok := core["account"] | |
| if !ok { | |
| return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core) | |
| } | |
| account = active | |
| } | |
| for _, d := range c.Data { | |
| if account == "" || d.Key.Account == account { | |
| if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" { | |
| return nil, fmt.Errorf("oauth2/google: no token available for account %q", account) | |
| } | |
| var expiry time.Time | |
| if d.Credential.TokenExpiry != nil { | |
| expiry = *d.Credential.TokenExpiry | |
| } | |
| return &SDKConfig{ | |
| conf: oauth2.Config{ | |
| ClientID: d.Credential.ClientID, | |
| ClientSecret: d.Credential.ClientSecret, | |
| Scopes: strings.Split(d.Key.Scope, " "), | |
| Endpoint: Endpoint, | |
| RedirectURL: "oob", | |
| }, | |
| initialToken: &oauth2.Token{ | |
| AccessToken: d.Credential.AccessToken, | |
| RefreshToken: d.Credential.RefreshToken, | |
| Expiry: expiry, | |
| }, | |
| }, nil | |
| } | |
| } | |
| return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account) | |
| } | |
| // Client returns an HTTP client using Google Cloud SDK credentials to | |
| // authorize requests. The token will auto-refresh as necessary. The | |
| // underlying http.RoundTripper will be obtained using the provided | |
| // context. The returned client and its Transport should not be | |
| // modified. | |
| func (c *SDKConfig) Client(ctx context.Context) *http.Client { | |
| return &http.Client{ | |
| Transport: &oauth2.Transport{ | |
| Source: c.TokenSource(ctx), | |
| }, | |
| } | |
| } | |
| // TokenSource returns an oauth2.TokenSource that retrieve tokens from | |
| // Google Cloud SDK credentials using the provided context. | |
| // It will returns the current access token stored in the credentials, | |
| // and refresh it when it expires, but it won't update the credentials | |
| // with the new access token. | |
| func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { | |
| return c.conf.TokenSource(ctx, c.initialToken) | |
| } | |
| // Scopes are the OAuth 2.0 scopes the current account is authorized for. | |
| func (c *SDKConfig) Scopes() []string { | |
| return c.conf.Scopes | |
| } | |
| func parseINI(ini io.Reader) (map[string]map[string]string, error) { | |
| result := map[string]map[string]string{ | |
| "": {}, // root section | |
| } | |
| scanner := bufio.NewScanner(ini) | |
| currentSection := "" | |
| for scanner.Scan() { | |
| line := strings.TrimSpace(scanner.Text()) | |
| if strings.HasPrefix(line, ";") { | |
| // comment. | |
| continue | |
| } | |
| if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { | |
| currentSection = strings.TrimSpace(line[1 : len(line)-1]) | |
| result[currentSection] = map[string]string{} | |
| continue | |
| } | |
| parts := strings.SplitN(line, "=", 2) | |
| if len(parts) == 2 && parts[0] != "" { | |
| result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) | |
| } | |
| } | |
| if err := scanner.Err(); err != nil { | |
| return nil, fmt.Errorf("error scanning ini: %v", err) | |
| } | |
| return result, nil | |
| } | |
| // sdkConfigPath tries to guess where the gcloud config is located. | |
| // It can be overridden during tests. | |
| var sdkConfigPath = func() (string, error) { | |
| if runtime.GOOS == "windows" { | |
| return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil | |
| } | |
| homeDir := guessUnixHomeDir() | |
| if homeDir == "" { | |
| return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") | |
| } | |
| return filepath.Join(homeDir, ".config", "gcloud"), nil | |
| } | |
| func guessUnixHomeDir() string { | |
| // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 | |
| if v := os.Getenv("HOME"); v != "" { | |
| return v | |
| } | |
| // Else, fall back to user.Current: | |
| if u, err := user.Current(); err == nil { | |
| return u.HomeDir | |
| } | |
| return "" | |
| } |