Skip to content

Commit ec8f5ac

Browse files
authored
Use gcloud oauth token before trying application default credentials (GoogleCloudPlatform#198)
This changes the oauth token source to first try to retrieve auth tokens using `gcloud config config-helper` first before trying application default credentials. This behavior will only exist when the proxy is invoked as a binary, but not when it is used as a library. The intent here is that its likely reasonable for individuals to run the proxy, but if they are using the proxy as a library, we do still want to encourage them to use a service account, since that is more reliable for production configurations
1 parent 9660231 commit ec8f5ac

File tree

3 files changed

+152
-58
lines changed

3 files changed

+152
-58
lines changed

cmd/cloud_sql_proxy/cloud_sql_proxy.go

+33-52
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,13 @@
2020
package main
2121

2222
import (
23-
"bytes"
24-
"encoding/json"
2523
"errors"
2624
"flag"
2725
"fmt"
2826
"io/ioutil"
2927
"log"
3028
"net/http"
3129
"os"
32-
"os/exec"
3330
"path/filepath"
3431
"strings"
3532
"sync"
@@ -40,6 +37,7 @@ import (
4037
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/fuse"
4138
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/limits"
4239
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
40+
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util"
4341

4442
"cloud.google.com/go/compute/metadata"
4543
"golang.org/x/net/context"
@@ -85,25 +83,13 @@ can be removed automatically by this program.`)
8583
You may set the GOOGLE_APPLICATION_CREDENTIALS environment variable for the same effect.`)
8684
ipAddressTypes = flag.String("ip_address_types", "PRIMARY", "Default to be 'PRIMARY'. Options: a list of strings separated by ',', e.g. 'PRIMARY, PRIVATE' ")
8785

88-
// Set to non-default value when gcloud execution failed.
89-
gcloudStatus gcloudStatusCode
90-
9186
// Setting to choose what API to connect to
9287
host = flag.String("host", "https://www.googleapis.com/sql/v1beta4/", "When set, the proxy uses this host as the base API path.")
9388
)
9489

95-
type gcloudStatusCode int
96-
97-
const (
98-
gcloudOk gcloudStatusCode = iota
99-
gcloudNotFound
100-
// generic execution failure error not specified above.
101-
gcloudExecErr
102-
)
103-
10490
const (
10591
minimumRefreshCfgThrottle = time.Second
106-
92+
10793
port = 3307
10894
)
10995

@@ -286,7 +272,17 @@ func authenticatedClient(ctx context.Context) (*http.Client, error) {
286272
return oauth2.NewClient(ctx, src), nil
287273
}
288274

289-
return goauth.DefaultClient(ctx, proxy.SQLScope)
275+
// If flags don't specify an auth source, try either gcloud or application default
276+
// credentials.
277+
src, err := util.GcloudTokenSource(ctx)
278+
if err != nil {
279+
src, err = goauth.DefaultTokenSource(ctx, proxy.SQLScope)
280+
}
281+
if err != nil {
282+
return nil, err
283+
}
284+
285+
return oauth2.NewClient(ctx, src), nil
290286
}
291287

292288
func stringList(s string) []string {
@@ -343,38 +339,15 @@ func listInstances(ctx context.Context, cl *http.Client, projects []string) ([]s
343339
return ret, nil
344340
}
345341

346-
func gcloudProject() []string {
347-
buf := new(bytes.Buffer)
348-
cmd := exec.Command("gcloud", "--format", "json", "config", "list", "core/project")
349-
cmd.Stdout = buf
350-
351-
if err := cmd.Run(); err != nil {
352-
if strings.Contains(err.Error(), "executable file not found") {
353-
// gcloud not found (probably not installed). Ignore the error but record
354-
// the status for later use.
355-
gcloudStatus = gcloudNotFound
356-
return nil
357-
}
358-
gcloudStatus = gcloudExecErr
359-
logging.Errorf("Error detecting gcloud project: %v", err)
360-
return nil
361-
}
362-
363-
var data struct {
364-
Core struct {
365-
Project string
366-
}
342+
func gcloudProject() ([]string, error) {
343+
cfg, err := util.GcloudConfig()
344+
if err != nil {
345+
return nil, err
367346
}
368-
369-
if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
370-
gcloudStatus = gcloudExecErr
371-
logging.Errorf("Failed to unmarshal bytes from gcloud: %v", err)
372-
logging.Errorf(" gcloud returned:\n%s", buf)
373-
return nil
347+
if cfg.Configuration.Properties.Core.Project == "" {
348+
return nil, fmt.Errorf("gcloud has no active project, you can set it by running `gcloud config set project <project>`")
374349
}
375-
376-
logging.Infof("Using gcloud's active project: %v", data.Core.Project)
377-
return []string{data.Core.Project}
350+
return []string{cfg.Configuration.Properties.Core.Project}, nil
378351
}
379352

380353
// Main executes the main function of the proxy, allowing it to be called from tests.
@@ -431,7 +404,15 @@ func main() {
431404
projList := stringList(*projects)
432405
// TODO: it'd be really great to consolidate flag verification in one place.
433406
if len(instList) == 0 && *instanceSrc == "" && len(projList) == 0 && !*useFuse {
434-
projList = gcloudProject()
407+
var err error
408+
projList, err = gcloudProject()
409+
if err == nil {
410+
logging.Infof("Using gcloud's active project: %v", projList)
411+
} else if gErr, ok := err.(*util.GcloudError); ok && gErr.Status == util.GcloudNotFound {
412+
log.Fatalf("gcloud is not in the path and -instances and -projects are empty")
413+
} else {
414+
log.Fatalf("unable to retrieve the active gcloud project and -instances and -projects are empty: %v", err)
415+
}
435416
}
436417

437418
onGCE := onGCE()
@@ -505,10 +486,10 @@ func main() {
505486
Port: port,
506487
MaxConnections: *maxConnections,
507488
Certs: certs.NewCertSourceOpts(client, certs.RemoteOpts{
508-
APIBasePath: *host,
509-
IgnoreRegion: !*checkRegion,
510-
UserAgent: userAgentFromVersionString(),
511-
IPAddrTypeOpts: ipAddrTypeOptsInput,
489+
APIBasePath: *host,
490+
IgnoreRegion: !*checkRegion,
491+
UserAgent: userAgentFromVersionString(),
492+
IPAddrTypeOpts: ipAddrTypeOptsInput,
512493
}),
513494
Conns: connset,
514495
RefreshCfgThrottle: refreshCfgThrottle,

cmd/cloud_sql_proxy/proxy.go

-6
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,6 @@ func CreateInstanceConfigs(dir string, useFuse bool, instances []string, instanc
349349
}
350350

351351
errStr := fmt.Sprintf("no instance selected because none of %s is specified", flags)
352-
switch gcloudStatus {
353-
case gcloudNotFound:
354-
errStr = fmt.Sprintf("%s and gcloud could not be found in the system path", errStr)
355-
case gcloudExecErr:
356-
errStr = fmt.Sprintf("%s and gcloud failed to get the project list", errStr)
357-
}
358352
return nil, errors.New(errStr)
359353
}
360354
return cfgs, nil

proxy/util/gcloudutil.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2018 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package util
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"encoding/json"
21+
"os/exec"
22+
"runtime"
23+
"time"
24+
25+
"github.com/GoogleCloudPlatform/cloudsql-proxy/logging"
26+
"golang.org/x/oauth2"
27+
)
28+
29+
// GcloudConfigData represents the data returned by `gcloud config config-helper`.
30+
type GcloudConfigData struct {
31+
Configuration struct {
32+
Properties struct {
33+
Core struct {
34+
Project string
35+
Account string
36+
}
37+
}
38+
}
39+
Credential struct {
40+
AccessToken string `json:"access_token"`
41+
TokenExpiry time.Time `json:"token_expiry"`
42+
}
43+
}
44+
45+
func (cfg *GcloudConfigData) oauthToken() *oauth2.Token {
46+
return &oauth2.Token{
47+
AccessToken: cfg.Credential.AccessToken,
48+
Expiry: cfg.Credential.TokenExpiry,
49+
}
50+
}
51+
52+
type GcloudStatusCode int
53+
54+
const (
55+
GcloudOk GcloudStatusCode = iota
56+
GcloudNotFound
57+
// generic execution failure error not specified above.
58+
GcloudExecErr
59+
)
60+
61+
type GcloudError struct {
62+
GcloudError error
63+
Status GcloudStatusCode
64+
}
65+
66+
func (e *GcloudError) Error() string {
67+
return e.GcloudError.Error()
68+
}
69+
70+
// GcloudConfig returns a GcloudConfigData object or an error of type *GcloudError.
71+
func GcloudConfig() (*GcloudConfigData, error) {
72+
gcloudCmd := "gcloud"
73+
if runtime.GOOS == "windows" {
74+
gcloudCmd = gcloudCmd + ".cmd"
75+
}
76+
77+
if _, err := exec.LookPath(gcloudCmd); err != nil {
78+
return nil, &GcloudError{err, GcloudNotFound}
79+
}
80+
81+
buf := new(bytes.Buffer)
82+
cmd := exec.Command(gcloudCmd, "--format", "json", "config", "config-helper")
83+
cmd.Stdout = buf
84+
85+
if err := cmd.Run(); err != nil {
86+
logging.Errorf("Error detecting gcloud project: %v", err)
87+
return nil, &GcloudError{err, GcloudExecErr}
88+
}
89+
90+
data := &GcloudConfigData{}
91+
if err := json.Unmarshal(buf.Bytes(), data); err != nil {
92+
logging.Errorf("Failed to unmarshal bytes from gcloud: %v", err)
93+
logging.Errorf(" gcloud returned:\n%s", buf)
94+
return nil, &GcloudError{err, GcloudExecErr}
95+
}
96+
97+
return data, nil
98+
}
99+
100+
// gcloudTokenSource implements oauth2.TokenSource via the `gcloud config config-helper` command.
101+
type gcloudTokenSource struct {
102+
}
103+
104+
// Token helps gcloudTokenSource implement oauth2.TokenSource.
105+
func (src *gcloudTokenSource) Token() (*oauth2.Token, error) {
106+
cfg, err := GcloudConfig()
107+
if err != nil {
108+
return nil, err
109+
}
110+
return cfg.oauthToken(), nil
111+
}
112+
113+
func GcloudTokenSource(ctx context.Context) (oauth2.TokenSource, error) {
114+
cfg, err := GcloudConfig()
115+
if err != nil {
116+
return nil, err
117+
}
118+
return oauth2.ReuseTokenSource(cfg.oauthToken(), &gcloudTokenSource{}), nil
119+
}

0 commit comments

Comments
 (0)