Skip to content

Commit

Permalink
feat: add gcloud-auth flag (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
enocom authored Jun 27, 2022
1 parent b140b88 commit 4bfa258
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 23 deletions.
35 changes: 30 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import (

"cloud.google.com/go/alloydbconn"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/alloydb"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/gcloud"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/proxy"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)

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

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

// If both token and credentials file were set, error.
// If more than one auth method is set, error.
if conf.Token != "" && conf.CredentialsFile != "" {
return newBadCommandError("Cannot specify --token and --credentials-file flags at the same time")
return newBadCommandError("cannot specify --token and --credentials-file flags at the same time")
}
if conf.Token != "" && conf.GcloudAuth {
return newBadCommandError("cannot specify --token and --gcloud-auth flags at the same time")
}
if conf.CredentialsFile != "" && conf.GcloudAuth {
return newBadCommandError("cannot specify --credentials-file and --gcloud-auth flags at the same time")
}
opts := []alloydbconn.Option{
alloydbconn.WithUserAgent(userAgent),
}

switch {
case conf.Token != "":
cmd.Printf("Authorizing with the -token flag\n")
opts = append(opts, alloydbconn.WithTokenSource(
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: conf.Token}),
))
case conf.CredentialsFile != "":
cmd.Printf("Authorizing with the credentials file at %q\n", conf.CredentialsFile)
opts = append(opts, alloydbconn.WithCredentialsFile(
conf.CredentialsFile,
))
case conf.GcloudAuth:
cmd.Println("Authorizing with gcloud user credentials")
ts, err := gcloud.TokenSource()
if err != nil {
return err
}
opts = append(opts, alloydbconn.WithTokenSource(ts))
default:
cmd.Println("Authorizing with Application Default Credentials")
}
conf.DialerOpts = opts

var ics []proxy.InstanceConnConfig
for _, a := range args {
Expand Down Expand Up @@ -269,9 +295,8 @@ func runSignalWrapper(cmd *Command) error {
// Otherwise, initialize a new one.
d := cmd.conf.Dialer
if d == nil {
opts := append(cmd.conf.DialerOpts(), alloydbconn.WithUserAgent(userAgent))
var err error
d, err = alloydbconn.NewDialer(ctx, opts...)
d, err = alloydbconn.NewDialer(ctx, cmd.conf.DialerOpts...)
if err != nil {
shutdownCh <- fmt.Errorf("error initializing dialer: %v", err)
return
Expand Down
61 changes: 59 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,58 @@ func TestNewCommandArguments(t *testing.T) {
t.Fatalf("want error = nil, got = %v", err)
}

if got := c.conf; !cmp.Equal(tc.want, got, cmpopts.IgnoreUnexported(proxy.Config{})) {
opts := cmpopts.IgnoreFields(proxy.Config{}, "DialerOpts")
if got := c.conf; !cmp.Equal(tc.want, got, opts) {
t.Fatalf("want = %#v\ngot = %#v\ndiff = %v", tc.want, got, cmp.Diff(tc.want, got))
}
})
}
}

func TestNewCommandWithGcloudAuth(t *testing.T) {
if testing.Short() {
t.Skip("skipping Gcloud auth test")
}
tcs := []struct {
desc string
args []string
want bool
}{
{
desc: "using the gcloud auth flag",
args: []string{"--gcloud-auth", "/projects/proj/locations/region/clusters/clust/instances/inst"},
want: true,
},
{
desc: "using the (short) gcloud auth flag",
args: []string{"-g", "/projects/proj/locations/region/clusters/clust/instances/inst"},
want: true,
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
c := NewCommand()
// Keep the test output quiet
c.SilenceUsage = true
c.SilenceErrors = true
// Disable execute behavior
c.RunE = func(*cobra.Command, []string) error {
return nil
}
c.SetArgs(tc.args)

err := c.Execute()
if err != nil {
t.Fatalf("want error = nil, got = %v", err)
}

if got := c.conf.GcloudAuth; got != tc.want {
t.Fatalf("want = %v, got = %v", tc.want, got)
}
})
}
}

func TestNewCommandWithErrors(t *testing.T) {
tcs := []struct {
desc string
Expand Down Expand Up @@ -228,11 +273,23 @@ func TestNewCommandWithErrors(t *testing.T) {
args: []string{"/projects/proj/locations/region/clusters/clust/instances/inst?port=hi"},
},
{
desc: "when both token and credentials file is set",
desc: "when both token and credentials file are set",
args: []string{
"--token", "my-token",
"--credentials-file", "/path/to/file", "/projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "when both token and gcloud auth are set",
args: []string{
"--token", "my-token",
"--gcloud-auth", "proj:region:inst"},
},
{
desc: "when both gcloud auth and credentials file are set",
args: []string{
"--gcloud-auth",
"--credential-file", "/path/to/file", "proj:region:inst"},
},
{
desc: "when the unix socket query param contains multiple values",
args: []string{"/projects/proj/locations/region/clusters/clust/instances/inst?unix-socket=/one&unix-socket=/two"},
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/spf13/cobra v1.4.0
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464 // indirect
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
google.golang.org/api v0.80.0 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
Expand Down
87 changes: 87 additions & 0 deletions internal/gcloud/gcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcloud

import (
"bytes"
"encoding/json"
"fmt"
"runtime"
"time"

"golang.org/x/oauth2"
exec "golang.org/x/sys/execabs"
)

// config represents the credentials returned by `gcloud config config-helper`.
type config struct {
Credential struct {
AccessToken string `json:"access_token"`
TokenExpiry time.Time `json:"token_expiry"`
}
}

func (c *config) Token() *oauth2.Token {
return &oauth2.Token{
AccessToken: c.Credential.AccessToken,
Expiry: c.Credential.TokenExpiry,
}
}

// Path returns the absolute path to the gcloud command. If the command is not
// found it returns an error.
func Path() (string, error) {
g := "gcloud"
if runtime.GOOS == "windows" {
g = g + ".cmd"
}
return exec.LookPath(g)
}

// configHelper implements oauth2.TokenSource via the `gcloud config config-helper` command.
type configHelper struct{}

// Token helps gcloudTokenSource implement oauth2.TokenSource.
func (configHelper) Token() (*oauth2.Token, error) {
gcloudCmd, err := Path()
if err != nil {
return nil, err
}
buf, errbuf := new(bytes.Buffer), new(bytes.Buffer)
cmd := exec.Command(gcloudCmd, "--format", "json", "config", "config-helper", "--min-expiry", "1h")
cmd.Stdout = buf
cmd.Stderr = errbuf

if err := cmd.Run(); err != nil {
err = fmt.Errorf("error reading config: %v; stderr was:\n%v", err, errbuf)
return nil, err
}

c := &config{}
if err := json.Unmarshal(buf.Bytes(), c); err != nil {
return nil, err
}
return c.Token(), nil
}

// TokenSource returns an oauth2.TokenSource backed by the gcloud CLI.
func TokenSource() (oauth2.TokenSource, error) {
h := configHelper{}
tok, err := h.Token()
if err != nil {
return nil, err
}
return oauth2.ReuseTokenSource(tok, h), nil
}
39 changes: 39 additions & 0 deletions internal/gcloud/gcloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcloud_test

import (
"testing"

"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/gcloud"
)

func TestGcloud(t *testing.T) {
if testing.Short() {
t.Skip("skipping gcloud integration tests")
}

// gcloud is now configured. Try to obtain a token from gcloud config
// helper.
ts, err := gcloud.TokenSource()
if err != nil {
t.Fatalf("failed to get token source: %v", err)
}

_, err = ts.Token()
if err != nil {
t.Fatalf("failed to get token: %v", err)
}
}
22 changes: 7 additions & 15 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
"cloud.google.com/go/alloydbconn"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/alloydb"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)

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

// GcloudAuth set whether to use Gcloud's config helper to retrieve a
// token for authentication.
GcloudAuth bool

// Addr is the address on which to bind all instances.
Addr string

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

func (c *Config) DialerOpts() []alloydbconn.Option {
var opts []alloydbconn.Option
switch {
case c.Token != "":
opts = append(opts, alloydbconn.WithTokenSource(
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}),
))
case c.CredentialsFile != "":
opts = append(opts, alloydbconn.WithCredentialsFile(
c.CredentialsFile,
))
}
return opts
// DialerOpts specifies the opts to use when creating a new dialer. This
// value is ignored when a Dialer has been set.
DialerOpts []alloydbconn.Option
}

type portConfig struct {
Expand Down
13 changes: 13 additions & 0 deletions tests/alloydb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,16 @@ func TestPostgresAuthWithCredentialsFile(t *testing.T) {
[]string{"--credentials-file", path, *alloydbConnName},
"alloydb3", dsn)
}

func TestAuthWithGcloudAuth(t *testing.T) {
if testing.Short() {
t.Skip("skipping Postgres integration tests")
}
requirePostgresVars(t)

dsn := fmt.Sprintf("host=localhost user=%s password=%s database=%s sslmode=disable",
*alloydbUser, *alloydbPass, *alloydbDB)
proxyConnTest(t,
[]string{"--gcloud-auth", *alloydbConnName},
"pgx", dsn)
}

0 comments on commit 4bfa258

Please sign in to comment.