sdk.go raw

   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