Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add gcloud-auth flag #43

Merged
merged 6 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 All @@ -143,19 +147,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 @@ -239,9 +265,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
35 changes: 33 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ import (

"cloud.google.com/go/alloydbconn"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/proxy"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/testutil"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra"
)

func TestNewCommandArguments(t *testing.T) {
cleanup := testutil.ConfigureGcloud(t)
defer cleanup()

withDefaults := func(c *proxy.Config) *proxy.Config {
if c.Addr == "" {
c.Addr = "127.0.0.1"
Expand Down Expand Up @@ -137,6 +141,20 @@ func TestNewCommandArguments(t *testing.T) {
CredentialsFile: "/path/to/file",
}),
},
{
desc: "using the gcloud auth flag",
args: []string{"--gcloud-auth", "proj:region:inst"},
want: withDefaults(&proxy.Config{
GcloudAuth: true,
}),
},
{
desc: "using the (short) gcloud auth flag",
args: []string{"-g", "proj:region:inst"},
want: withDefaults(&proxy.Config{
GcloudAuth: true,
}),
},
}

for _, tc := range tcs {
Expand All @@ -156,7 +174,8 @@ 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))
}
})
Expand Down Expand Up @@ -205,11 +224,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"},
},
}

for _, tc := range tcs {
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
}
43 changes: 43 additions & 0 deletions internal/gcloud/gcloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/testutil"
)

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

cleanup := testutil.ConfigureGcloud(t)
defer cleanup()

// 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 @@ -25,7 +25,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 @@ -47,6 +46,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 @@ -61,21 +64,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
63 changes: 63 additions & 0 deletions internal/testutil/testutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 testutil

import (
"bytes"
"io/ioutil"
"os"
"os/exec"
"testing"

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

// ConfigureGcloud configures gcloud using only GOOGLE_APPLICATION_CREDENTIALS
// and stores the resulting configuration in a temporary directory as set by
// CLOUDSDK_CONFIG, which changes the gcloud config directory from the
// default. We use a temporary directory to avoid trampling on any existing
// gcloud config.
func ConfigureGcloud(t *testing.T) func() {
dir, err := ioutil.TempDir("", "cloudsdk*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
os.Setenv("CLOUDSDK_CONFIG", dir)

gcloudCmd, err := gcloud.Path()
if err != nil {
t.Fatal(err)
}

keyFile, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS")
if !ok {
t.Fatal("GOOGLE_APPLICATION_CREDENTIALS is not set in the environment")
}
os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")

buf := &bytes.Buffer{}
cmd := exec.Command(gcloudCmd, "auth", "activate-service-account", "--key-file", keyFile)
cmd.Stdout = buf

if err := cmd.Run(); err != nil {
t.Fatalf("failed to active service account. err = %v, message = %v", err, buf.String())
}

return func() {
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", keyFile)
os.Unsetenv("CLOUDSDK_CONFIG")
}

}
Loading