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 support for JSON credentials #188

Merged
merged 4 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export ALLOYDB_CONNECTION_NAME="project:region:instance"
export ALLOYDB_USER="postgres-user"
export ALLOYDB_PASS="postgres-password"
export ALLOYDB_DB="postgres-db-name"

export GOOGLE_APPLICATION_CREDENTIALS="path/to/credentials"
12 changes: 12 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,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().StringVarP(&c.conf.CredentialsJSON, "json-credentials", "j", "",
"Use service account key JSON as a source of IAM credentials.")
cmd.PersistentFlags().BoolVarP(&c.conf.GcloudAuth, "gcloud-auth", "g", false,
"Use gcloud's user configuration to retrieve a token for authentication.")
cmd.PersistentFlags().BoolVarP(&c.conf.StructuredLogs, "structured-logs", "l", false,
Expand Down Expand Up @@ -259,6 +261,16 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
if conf.CredentialsFile != "" && conf.GcloudAuth {
return newBadCommandError("cannot specify --credentials-file and --gcloud-auth flags at the same time")
}
if conf.CredentialsJSON != "" && conf.Token != "" {
return newBadCommandError("cannot specify --json-credentials and --token flags at the same time")
}
if conf.CredentialsJSON != "" && conf.CredentialsFile != "" {
return newBadCommandError("cannot specify --json-credentials and --credentials-file flags at the same time")
}
if conf.CredentialsJSON != "" && conf.GcloudAuth {
return newBadCommandError("cannot specify --json-credentials and --gcloud-auth flags at the same time")
}

if userHasSet("alloydbadmin-api-endpoint") {
_, err := url.Parse(conf.APIEndpointURL)
if err != nil {
Expand Down
43 changes: 40 additions & 3 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,19 @@ func TestNewCommandArguments(t *testing.T) {
APIEndpointURL: "https://test.googleapis.com",
}),
},
{
desc: "using the JSON credentials",
args: []string{"--json-credentials", `{"json":"goes-here"}`, "projects/proj/locations/region/clusters/clust/instances/inst"}, want: withDefaults(&proxy.Config{
CredentialsJSON: `{"json":"goes-here"}`,
}),
},
{
desc: "using the (short) JSON credentials",
args: []string{"-j", `{"json":"goes-here"}`, "projects/proj/locations/region/clusters/clust/instances/inst"},
want: withDefaults(&proxy.Config{
CredentialsJSON: `{"json":"goes-here"}`,
}),
},
}

for _, tc := range tcs {
Expand Down Expand Up @@ -329,13 +342,36 @@ func TestNewCommandWithErrors(t *testing.T) {
desc: "when both token and gcloud auth are set",
args: []string{
"--token", "my-token",
"--gcloud-auth", "proj:region:inst"},
"--gcloud-auth",
"projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "when both gcloud auth and credentials file are set",
args: []string{
"--gcloud-auth",
"--credential-file", "/path/to/file", "proj:region:inst"},
"--credentials-file", "/path/to/file",
"projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "when both token and credentials JSON are set",
args: []string{
"--token", "a-token",
"--json-credentials", `{"json":"here"}`,
"projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "when both credentials file and credentials JSON are set",
args: []string{
"--credentials-file", "/a/file",
"--json-credentials", `{"json":"here"}`,
"projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "when both gcloud auth and credentials JSON are set",
args: []string{
"--gcloud-auth",
"--json-credentials", `{"json":"here"}`,
"projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "when the unix socket query param contains multiple values",
Expand All @@ -359,7 +395,8 @@ func TestNewCommandWithErrors(t *testing.T) {
},
{
desc: "using an invalid url for host flag",
args: []string{"--host", "https://invalid:url[/]", "proj:region:inst"},
args: []string{"--host", "https://invalid:url[/]",
"projects/proj/locations/region/clusters/clust/instances/inst"},
},
{
desc: "using fuse-tmp-dir without fuse",
Expand Down
8 changes: 8 additions & 0 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type Config struct {
// CredentialsFile is the path to a service account key.
CredentialsFile string

// CredentialsJSON is a JSON representation of the service account key.
CredentialsJSON string

// GcloudAuth set whether to use Gcloud's config helper to retrieve a
// token for authentication.
GcloudAuth bool
Expand Down Expand Up @@ -131,6 +134,11 @@ func (c *Config) DialerOptions(l alloydb.Logger) ([]alloydbconn.Option, error) {
return nil, err
}
opts = append(opts, alloydbconn.WithTokenSource(ts))
case c.CredentialsJSON != "":
l.Infof("Authorizing with JSON credentials environment variable")
opts = append(opts, alloydbconn.WithCredentialsJSON(
[]byte(c.CredentialsJSON),
))
default:
l.Infof("Authorizing with Application Default Credentials")
}
Expand Down
1 change: 1 addition & 0 deletions internal/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func TestClientInitialization(t *testing.T) {
}
_, isFlex := os.LookupEnv("FLEX")
if !isFlex {
// App Engine Flex doesn't support IPv6.
tcs = append(tcs, testCase{
desc: "IPv6 support",
in: &proxy.Config{
Expand Down
26 changes: 19 additions & 7 deletions tests/alloydb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ func TestPostgresAuthWithToken(t *testing.T) {
}
_, isFlex := os.LookupEnv("FLEX")
if isFlex {
t.Skip("disabling until we migrate tests to Kokoro")
t.Skip("App Engine Flex doesn't support retrieving OAuth2 tokens")
}
requirePostgresVars(t)
cleanup, err := pgxv4.RegisterDriver("alloydb2")
if err != nil {
t.Fatalf("failed to register driver: %v", err)
}
defer cleanup()
tok, _, cleanup2 := removeAuthEnvVar(t)
tok, _, cleanup2 := removeAuthEnvVar(t, true)
defer cleanup2()

dsn := fmt.Sprintf("host=%v user=%v password=%v database=%v sslmode=disable",
Expand All @@ -122,17 +122,13 @@ func TestPostgresAuthWithCredentialsFile(t *testing.T) {
if testing.Short() {
t.Skip("skipping Postgres integration tests")
}
_, isFlex := os.LookupEnv("FLEX")
if isFlex {
t.Skip("disabling until we migrate tests to Kokoro")
}
requirePostgresVars(t)
cleanup, err := pgxv4.RegisterDriver("alloydb3")
if err != nil {
t.Fatalf("failed to register driver: %v", err)
}
defer cleanup()
_, path, cleanup2 := removeAuthEnvVar(t)
_, path, cleanup2 := removeAuthEnvVar(t, false)
defer cleanup2()

dsn := fmt.Sprintf("host=%v user=%v password=%v database=%v sslmode=disable",
Expand All @@ -142,6 +138,22 @@ func TestPostgresAuthWithCredentialsFile(t *testing.T) {
"alloydb3", dsn)
}

func TestPostgresAuthWithCredentialsJSON(t *testing.T) {
if testing.Short() {
t.Skip("skipping Postgres integration tests")
}
requirePostgresVars(t)
creds := keyfile(t)
_, _, cleanup := removeAuthEnvVar(t, false)
defer cleanup()

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

func TestAuthWithGcloudAuth(t *testing.T) {
if testing.Short() {
t.Skip("skipping Postgres integration tests")
Expand Down
32 changes: 24 additions & 8 deletions tests/connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package tests
import (
"context"
"database/sql"
"io/ioutil"
"os"
"testing"
"time"
Expand All @@ -30,14 +31,17 @@ const connTestTimeout = time.Minute
// removeAuthEnvVar retrieves an OAuth2 token and a path to a service account key
// and then unsets GOOGLE_APPLICATION_CREDENTIALS. It returns a cleanup function
// that restores the original setup.
func removeAuthEnvVar(t *testing.T) (*oauth2.Token, string, func()) {
ts, err := google.DefaultTokenSource(context.Background())
if err != nil {
t.Errorf("failed to resolve token source: %v", err)
}
tok, err := ts.Token()
if err != nil {
t.Errorf("failed to get token: %v", err)
func removeAuthEnvVar(t *testing.T, wantToken bool) (*oauth2.Token, string, func()) {
var tok *oauth2.Token
if wantToken {
ts, err := google.DefaultTokenSource(context.Background())
if err != nil {
t.Errorf("failed to resolve token source: %v", err)
}
tok, err = ts.Token()
if err != nil {
t.Errorf("failed to get token: %v", err)
}
}
path, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS")
if !ok {
Expand All @@ -51,6 +55,18 @@ func removeAuthEnvVar(t *testing.T) (*oauth2.Token, string, func()) {
}
}

func keyfile(t *testing.T) string {
path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
if path == "" {
t.Fatal("GOOGLE_APPLICATION_CREDENTIALS not set")
}
creds, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("io.ReadAll(): %v", err)
}
return string(creds)
}

// proxyConnTest is a test helper to verify the proxy works with a basic connectivity test.
func proxyConnTest(t *testing.T, args []string, driver, dsn string) {
ctx, cancel := context.WithTimeout(context.Background(), connTestTimeout)
Expand Down