|
- // 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 ""
- }
|