Skip to content

Commit 4bfa258

Browse files
authored
feat: add gcloud-auth flag (#43)
This commit is based on: GoogleCloudPlatform/cloud-sql-proxy@08d827c
1 parent b140b88 commit 4bfa258

File tree

7 files changed

+236
-23
lines changed

7 files changed

+236
-23
lines changed

cmd/root.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import (
2929

3030
"cloud.google.com/go/alloydbconn"
3131
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/alloydb"
32+
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/gcloud"
3233
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/proxy"
3334
"github.com/spf13/cobra"
35+
"golang.org/x/oauth2"
3436
)
3537

3638
var (
@@ -122,6 +124,8 @@ without having to manage any client SSL certificates.`,
122124
"Bearer token used for authorization.")
123125
cmd.PersistentFlags().StringVarP(&c.conf.CredentialsFile, "credentials-file", "c", "",
124126
"Path to a service account key to use for authentication.")
127+
cmd.PersistentFlags().BoolVarP(&c.conf.GcloudAuth, "gcloud-auth", "g", false,
128+
"Use gcloud's user configuration to retrieve a token for authentication.")
125129

126130
// Global and per instance flags
127131
cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
@@ -154,19 +158,41 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
154158
return newBadCommandError(fmt.Sprintf("not a valid IP address: %q", conf.Addr))
155159
}
156160

157-
// If both token and credentials file were set, error.
161+
// If more than one auth method is set, error.
158162
if conf.Token != "" && conf.CredentialsFile != "" {
159-
return newBadCommandError("Cannot specify --token and --credentials-file flags at the same time")
163+
return newBadCommandError("cannot specify --token and --credentials-file flags at the same time")
164+
}
165+
if conf.Token != "" && conf.GcloudAuth {
166+
return newBadCommandError("cannot specify --token and --gcloud-auth flags at the same time")
167+
}
168+
if conf.CredentialsFile != "" && conf.GcloudAuth {
169+
return newBadCommandError("cannot specify --credentials-file and --gcloud-auth flags at the same time")
170+
}
171+
opts := []alloydbconn.Option{
172+
alloydbconn.WithUserAgent(userAgent),
160173
}
161-
162174
switch {
163175
case conf.Token != "":
164176
cmd.Printf("Authorizing with the -token flag\n")
177+
opts = append(opts, alloydbconn.WithTokenSource(
178+
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: conf.Token}),
179+
))
165180
case conf.CredentialsFile != "":
166181
cmd.Printf("Authorizing with the credentials file at %q\n", conf.CredentialsFile)
182+
opts = append(opts, alloydbconn.WithCredentialsFile(
183+
conf.CredentialsFile,
184+
))
185+
case conf.GcloudAuth:
186+
cmd.Println("Authorizing with gcloud user credentials")
187+
ts, err := gcloud.TokenSource()
188+
if err != nil {
189+
return err
190+
}
191+
opts = append(opts, alloydbconn.WithTokenSource(ts))
167192
default:
168193
cmd.Println("Authorizing with Application Default Credentials")
169194
}
195+
conf.DialerOpts = opts
170196

171197
var ics []proxy.InstanceConnConfig
172198
for _, a := range args {
@@ -269,9 +295,8 @@ func runSignalWrapper(cmd *Command) error {
269295
// Otherwise, initialize a new one.
270296
d := cmd.conf.Dialer
271297
if d == nil {
272-
opts := append(cmd.conf.DialerOpts(), alloydbconn.WithUserAgent(userAgent))
273298
var err error
274-
d, err = alloydbconn.NewDialer(ctx, opts...)
299+
d, err = alloydbconn.NewDialer(ctx, cmd.conf.DialerOpts...)
275300
if err != nil {
276301
shutdownCh <- fmt.Errorf("error initializing dialer: %v", err)
277302
return

cmd/root_test.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,58 @@ func TestNewCommandArguments(t *testing.T) {
179179
t.Fatalf("want error = nil, got = %v", err)
180180
}
181181

182-
if got := c.conf; !cmp.Equal(tc.want, got, cmpopts.IgnoreUnexported(proxy.Config{})) {
182+
opts := cmpopts.IgnoreFields(proxy.Config{}, "DialerOpts")
183+
if got := c.conf; !cmp.Equal(tc.want, got, opts) {
183184
t.Fatalf("want = %#v\ngot = %#v\ndiff = %v", tc.want, got, cmp.Diff(tc.want, got))
184185
}
185186
})
186187
}
187188
}
188189

190+
func TestNewCommandWithGcloudAuth(t *testing.T) {
191+
if testing.Short() {
192+
t.Skip("skipping Gcloud auth test")
193+
}
194+
tcs := []struct {
195+
desc string
196+
args []string
197+
want bool
198+
}{
199+
{
200+
desc: "using the gcloud auth flag",
201+
args: []string{"--gcloud-auth", "/projects/proj/locations/region/clusters/clust/instances/inst"},
202+
want: true,
203+
},
204+
{
205+
desc: "using the (short) gcloud auth flag",
206+
args: []string{"-g", "/projects/proj/locations/region/clusters/clust/instances/inst"},
207+
want: true,
208+
},
209+
}
210+
for _, tc := range tcs {
211+
t.Run(tc.desc, func(t *testing.T) {
212+
c := NewCommand()
213+
// Keep the test output quiet
214+
c.SilenceUsage = true
215+
c.SilenceErrors = true
216+
// Disable execute behavior
217+
c.RunE = func(*cobra.Command, []string) error {
218+
return nil
219+
}
220+
c.SetArgs(tc.args)
221+
222+
err := c.Execute()
223+
if err != nil {
224+
t.Fatalf("want error = nil, got = %v", err)
225+
}
226+
227+
if got := c.conf.GcloudAuth; got != tc.want {
228+
t.Fatalf("want = %v, got = %v", tc.want, got)
229+
}
230+
})
231+
}
232+
}
233+
189234
func TestNewCommandWithErrors(t *testing.T) {
190235
tcs := []struct {
191236
desc string
@@ -228,11 +273,23 @@ func TestNewCommandWithErrors(t *testing.T) {
228273
args: []string{"/projects/proj/locations/region/clusters/clust/instances/inst?port=hi"},
229274
},
230275
{
231-
desc: "when both token and credentials file is set",
276+
desc: "when both token and credentials file are set",
232277
args: []string{
233278
"--token", "my-token",
234279
"--credentials-file", "/path/to/file", "/projects/proj/locations/region/clusters/clust/instances/inst"},
235280
},
281+
{
282+
desc: "when both token and gcloud auth are set",
283+
args: []string{
284+
"--token", "my-token",
285+
"--gcloud-auth", "proj:region:inst"},
286+
},
287+
{
288+
desc: "when both gcloud auth and credentials file are set",
289+
args: []string{
290+
"--gcloud-auth",
291+
"--credential-file", "/path/to/file", "proj:region:inst"},
292+
},
236293
{
237294
desc: "when the unix socket query param contains multiple values",
238295
args: []string{"/projects/proj/locations/region/clusters/clust/instances/inst?unix-socket=/one&unix-socket=/two"},

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/spf13/cobra v1.4.0
1111
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect
1212
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
13-
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464 // indirect
13+
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464
1414
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
1515
google.golang.org/api v0.80.0 // indirect
1616
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect

internal/gcloud/gcloud.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2022 Google LLC
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 gcloud
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"fmt"
21+
"runtime"
22+
"time"
23+
24+
"golang.org/x/oauth2"
25+
exec "golang.org/x/sys/execabs"
26+
)
27+
28+
// config represents the credentials returned by `gcloud config config-helper`.
29+
type config struct {
30+
Credential struct {
31+
AccessToken string `json:"access_token"`
32+
TokenExpiry time.Time `json:"token_expiry"`
33+
}
34+
}
35+
36+
func (c *config) Token() *oauth2.Token {
37+
return &oauth2.Token{
38+
AccessToken: c.Credential.AccessToken,
39+
Expiry: c.Credential.TokenExpiry,
40+
}
41+
}
42+
43+
// Path returns the absolute path to the gcloud command. If the command is not
44+
// found it returns an error.
45+
func Path() (string, error) {
46+
g := "gcloud"
47+
if runtime.GOOS == "windows" {
48+
g = g + ".cmd"
49+
}
50+
return exec.LookPath(g)
51+
}
52+
53+
// configHelper implements oauth2.TokenSource via the `gcloud config config-helper` command.
54+
type configHelper struct{}
55+
56+
// Token helps gcloudTokenSource implement oauth2.TokenSource.
57+
func (configHelper) Token() (*oauth2.Token, error) {
58+
gcloudCmd, err := Path()
59+
if err != nil {
60+
return nil, err
61+
}
62+
buf, errbuf := new(bytes.Buffer), new(bytes.Buffer)
63+
cmd := exec.Command(gcloudCmd, "--format", "json", "config", "config-helper", "--min-expiry", "1h")
64+
cmd.Stdout = buf
65+
cmd.Stderr = errbuf
66+
67+
if err := cmd.Run(); err != nil {
68+
err = fmt.Errorf("error reading config: %v; stderr was:\n%v", err, errbuf)
69+
return nil, err
70+
}
71+
72+
c := &config{}
73+
if err := json.Unmarshal(buf.Bytes(), c); err != nil {
74+
return nil, err
75+
}
76+
return c.Token(), nil
77+
}
78+
79+
// TokenSource returns an oauth2.TokenSource backed by the gcloud CLI.
80+
func TokenSource() (oauth2.TokenSource, error) {
81+
h := configHelper{}
82+
tok, err := h.Token()
83+
if err != nil {
84+
return nil, err
85+
}
86+
return oauth2.ReuseTokenSource(tok, h), nil
87+
}

internal/gcloud/gcloud_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2022 Google LLC
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 gcloud_test
16+
17+
import (
18+
"testing"
19+
20+
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/gcloud"
21+
)
22+
23+
func TestGcloud(t *testing.T) {
24+
if testing.Short() {
25+
t.Skip("skipping gcloud integration tests")
26+
}
27+
28+
// gcloud is now configured. Try to obtain a token from gcloud config
29+
// helper.
30+
ts, err := gcloud.TokenSource()
31+
if err != nil {
32+
t.Fatalf("failed to get token source: %v", err)
33+
}
34+
35+
_, err = ts.Token()
36+
if err != nil {
37+
t.Fatalf("failed to get token: %v", err)
38+
}
39+
}

internal/proxy/proxy.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
"cloud.google.com/go/alloydbconn"
3030
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/alloydb"
3131
"github.com/spf13/cobra"
32-
"golang.org/x/oauth2"
3332
)
3433

3534
// InstanceConnConfig holds the configuration for an individual instance
@@ -55,6 +54,10 @@ type Config struct {
5554
// CredentialsFile is the path to a service account key.
5655
CredentialsFile string
5756

57+
// GcloudAuth set whether to use Gcloud's config helper to retrieve a
58+
// token for authentication.
59+
GcloudAuth bool
60+
5861
// Addr is the address on which to bind all instances.
5962
Addr string
6063

@@ -73,21 +76,10 @@ type Config struct {
7376
// Dialer specifies the dialer to use when connecting to AlloyDB
7477
// instances.
7578
Dialer alloydb.Dialer
76-
}
7779

78-
func (c *Config) DialerOpts() []alloydbconn.Option {
79-
var opts []alloydbconn.Option
80-
switch {
81-
case c.Token != "":
82-
opts = append(opts, alloydbconn.WithTokenSource(
83-
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}),
84-
))
85-
case c.CredentialsFile != "":
86-
opts = append(opts, alloydbconn.WithCredentialsFile(
87-
c.CredentialsFile,
88-
))
89-
}
90-
return opts
80+
// DialerOpts specifies the opts to use when creating a new dialer. This
81+
// value is ignored when a Dialer has been set.
82+
DialerOpts []alloydbconn.Option
9183
}
9284

9385
type portConfig struct {

tests/alloydb_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,16 @@ func TestPostgresAuthWithCredentialsFile(t *testing.T) {
141141
[]string{"--credentials-file", path, *alloydbConnName},
142142
"alloydb3", dsn)
143143
}
144+
145+
func TestAuthWithGcloudAuth(t *testing.T) {
146+
if testing.Short() {
147+
t.Skip("skipping Postgres integration tests")
148+
}
149+
requirePostgresVars(t)
150+
151+
dsn := fmt.Sprintf("host=localhost user=%s password=%s database=%s sslmode=disable",
152+
*alloydbUser, *alloydbPass, *alloydbDB)
153+
proxyConnTest(t,
154+
[]string{"--gcloud-auth", *alloydbConnName},
155+
"pgx", dsn)
156+
}

0 commit comments

Comments
 (0)