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	"bufio"
9	"encoding/json"
10	"errors"
11	"fmt"
12	"io"
13	"net/http"
14	"os"
15	"os/user"
16	"path/filepath"
17	"runtime"
18	"strings"
19	"time"
20
21	"golang.org/x/net/context"
22	"golang.org/x/oauth2"
23)
24
25type 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.
43type 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/.
54func 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.
128func (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.
141func (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.
146func (c *SDKConfig) Scopes() []string {
147	return c.conf.Scopes
148}
149
150func 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.
180var 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
191func 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