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