...

Source file src/golang.org/x/oauth2/google/sdk.go

Documentation: golang.org/x/oauth2/google

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package google
     6  
     7  import (
     8  	"bufio"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"os"
    16  	"os/user"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strings"
    20  	"time"
    21  
    22  	"golang.org/x/oauth2"
    23  )
    24  
    25  type sdkCredentials struct {
    26  	Data []struct {
    27  		Credential struct {
    28  			ClientID     string     `json:"client_id"`
    29  			ClientSecret string     `json:"client_secret"`
    30  			AccessToken  string     `json:"access_token"`
    31  			RefreshToken string     `json:"refresh_token"`
    32  			TokenExpiry  *time.Time `json:"token_expiry"`
    33  		} `json:"credential"`
    34  		Key struct {
    35  			Account string `json:"account"`
    36  			Scope   string `json:"scope"`
    37  		} `json:"key"`
    38  	}
    39  }
    40  
    41  // An SDKConfig provides access to tokens from an account already
    42  // authorized via the Google Cloud SDK.
    43  type SDKConfig struct {
    44  	conf         oauth2.Config
    45  	initialToken *oauth2.Token
    46  }
    47  
    48  // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
    49  // account. If account is empty, the account currently active in
    50  // Google Cloud SDK properties is used.
    51  // Google Cloud SDK credentials must be created by running `gcloud auth`
    52  // before using this function.
    53  // The Google Cloud SDK is available at https://cloud.google.com/sdk/.
    54  func NewSDKConfig(account string) (*SDKConfig, error) {
    55  	configPath, err := sdkConfigPath()
    56  	if err != nil {
    57  		return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
    58  	}
    59  	credentialsPath := filepath.Join(configPath, "credentials")
    60  	f, err := os.Open(credentialsPath)
    61  	if err != nil {
    62  		return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
    63  	}
    64  	defer f.Close()
    65  
    66  	var c sdkCredentials
    67  	if err := json.NewDecoder(f).Decode(&c); err != nil {
    68  		return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
    69  	}
    70  	if len(c.Data) == 0 {
    71  		return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
    72  	}
    73  	if account == "" {
    74  		propertiesPath := filepath.Join(configPath, "properties")
    75  		f, err := os.Open(propertiesPath)
    76  		if err != nil {
    77  			return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
    78  		}
    79  		defer f.Close()
    80  		ini, err := parseINI(f)
    81  		if err != nil {
    82  			return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
    83  		}
    84  		core, ok := ini["core"]
    85  		if !ok {
    86  			return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
    87  		}
    88  		active, ok := core["account"]
    89  		if !ok {
    90  			return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
    91  		}
    92  		account = active
    93  	}
    94  
    95  	for _, d := range c.Data {
    96  		if account == "" || d.Key.Account == account {
    97  			if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" {
    98  				return nil, fmt.Errorf("oauth2/google: no token available for account %q", account)
    99  			}
   100  			var expiry time.Time
   101  			if d.Credential.TokenExpiry != nil {
   102  				expiry = *d.Credential.TokenExpiry
   103  			}
   104  			return &SDKConfig{
   105  				conf: oauth2.Config{
   106  					ClientID:     d.Credential.ClientID,
   107  					ClientSecret: d.Credential.ClientSecret,
   108  					Scopes:       strings.Split(d.Key.Scope, " "),
   109  					Endpoint:     Endpoint,
   110  					RedirectURL:  "oob",
   111  				},
   112  				initialToken: &oauth2.Token{
   113  					AccessToken:  d.Credential.AccessToken,
   114  					RefreshToken: d.Credential.RefreshToken,
   115  					Expiry:       expiry,
   116  				},
   117  			}, nil
   118  		}
   119  	}
   120  	return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
   121  }
   122  
   123  // Client returns an HTTP client using Google Cloud SDK credentials to
   124  // authorize requests. The token will auto-refresh as necessary. The
   125  // underlying http.RoundTripper will be obtained using the provided
   126  // context. The returned client and its Transport should not be
   127  // modified.
   128  func (c *SDKConfig) Client(ctx context.Context) *http.Client {
   129  	return &http.Client{
   130  		Transport: &oauth2.Transport{
   131  			Source: c.TokenSource(ctx),
   132  		},
   133  	}
   134  }
   135  
   136  // TokenSource returns an oauth2.TokenSource that retrieve tokens from
   137  // Google Cloud SDK credentials using the provided context.
   138  // It will returns the current access token stored in the credentials,
   139  // and refresh it when it expires, but it won't update the credentials
   140  // with the new access token.
   141  func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource {
   142  	return c.conf.TokenSource(ctx, c.initialToken)
   143  }
   144  
   145  // Scopes are the OAuth 2.0 scopes the current account is authorized for.
   146  func (c *SDKConfig) Scopes() []string {
   147  	return c.conf.Scopes
   148  }
   149  
   150  func parseINI(ini io.Reader) (map[string]map[string]string, error) {
   151  	result := map[string]map[string]string{
   152  		"": {}, // root section
   153  	}
   154  	scanner := bufio.NewScanner(ini)
   155  	currentSection := ""
   156  	for scanner.Scan() {
   157  		line := strings.TrimSpace(scanner.Text())
   158  		if strings.HasPrefix(line, ";") {
   159  			// comment.
   160  			continue
   161  		}
   162  		if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
   163  			currentSection = strings.TrimSpace(line[1 : len(line)-1])
   164  			result[currentSection] = map[string]string{}
   165  			continue
   166  		}
   167  		parts := strings.SplitN(line, "=", 2)
   168  		if len(parts) == 2 && parts[0] != "" {
   169  			result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
   170  		}
   171  	}
   172  	if err := scanner.Err(); err != nil {
   173  		return nil, fmt.Errorf("error scanning ini: %v", err)
   174  	}
   175  	return result, nil
   176  }
   177  
   178  // sdkConfigPath tries to guess where the gcloud config is located.
   179  // It can be overridden during tests.
   180  var sdkConfigPath = func() (string, error) {
   181  	if runtime.GOOS == "windows" {
   182  		return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
   183  	}
   184  	homeDir := guessUnixHomeDir()
   185  	if homeDir == "" {
   186  		return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty")
   187  	}
   188  	return filepath.Join(homeDir, ".config", "gcloud"), nil
   189  }
   190  
   191  func guessUnixHomeDir() string {
   192  	// Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470
   193  	if v := os.Getenv("HOME"); v != "" {
   194  		return v
   195  	}
   196  	// Else, fall back to user.Current:
   197  	if u, err := user.Current(); err == nil {
   198  		return u.HomeDir
   199  	}
   200  	return ""
   201  }
   202  

View as plain text