diff --git a/cmd/root.go b/cmd/root.go index 30a34b25c..20f707123 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -211,6 +211,8 @@ func NewCommand(opts ...Option) *Command { "Use bearer token as a source of IAM credentials.") cmd.PersistentFlags().StringVarP(&c.conf.CredentialsFile, "credentials-file", "c", "", "Use service account key file as a source of IAM credentials.") + 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 credentials as a source of IAM credentials.") cmd.PersistentFlags().BoolVarP(&c.conf.StructuredLogs, "structured-logs", "l", false, @@ -309,6 +311,15 @@ 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("http-port") && !userHasSet("prometheus") && !userHasSet("health-check") { cmd.logger.Infof("Ignoring --http-port because --prometheus or --health-check was not set") diff --git a/cmd/root_test.go b/cmd/root_test.go index bd96e730f..e9d1937f1 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -147,6 +147,20 @@ func TestNewCommandArguments(t *testing.T) { CredentialsFile: "/path/to/file", }), }, + { + desc: "using the JSON credentials", + args: []string{"--json-credentials", `{"json":"goes-here"}`, "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + CredentialsJSON: `{"json":"goes-here"}`, + }), + }, + { + desc: "using the (short) JSON credentials", + args: []string{"-j", `{"json":"goes-here"}`, "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + CredentialsJSON: `{"json":"goes-here"}`, + }), + }, { desc: "using the gcloud auth flag", args: []string{"--gcloud-auth", "proj:region:inst"}, @@ -493,7 +507,25 @@ func TestNewCommandWithErrors(t *testing.T) { 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", "proj:region:inst"}, + }, + { + desc: "when both token and credentials JSON are set", + args: []string{ + "--token", "a-token", + "--json-credentials", `{"json":"here"}`, "proj:region:inst"}, + }, + { + desc: "when both credentials file and credentials JSON are set", + args: []string{ + "--credentials-file", "/a/file", + "--json-credentials", `{"json":"here"}`, "proj:region:inst"}, + }, + { + desc: "when both gcloud auth and credentials JSON are set", + args: []string{ + "--gcloud-auth", + "--json-credentials", `{"json":"here"}`, "proj:region:inst"}, }, { desc: "when the unix socket query param contains multiple values", diff --git a/go.mod b/go.mod index 4a339f7a6..567bf4a1f 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( go.uber.org/zap v1.23.0 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 - google.golang.org/api v0.96.0 + google.golang.org/api v0.97.0 ) require ( diff --git a/go.sum b/go.sum index d8539b397..7cddd9acb 100644 --- a/go.sum +++ b/go.sum @@ -1705,8 +1705,8 @@ google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6r google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index a1b592136..4262e96ef 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -103,6 +103,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 @@ -201,6 +204,11 @@ func (c *Config) DialerOptions(l cloudsql.Logger) ([]cloudsqlconn.Option, error) opts = append(opts, cloudsqlconn.WithCredentialsFile( c.CredentialsFile, )) + case c.CredentialsJSON != "": + l.Infof("Authorizing with JSON credentials environment variable") + opts = append(opts, cloudsqlconn.WithCredentialsJSON( + []byte(c.CredentialsJSON), + )) case c.GcloudAuth: l.Infof("Authorizing with gcloud user credentials") ts, err := gcloud.TokenSource() diff --git a/tests/connection_test.go b/tests/connection_test.go index 61740baf3..f0aafa4e4 100644 --- a/tests/connection_test.go +++ b/tests/connection_test.go @@ -17,6 +17,7 @@ package tests import ( "context" "database/sql" + "io/ioutil" "net/http" "net/http/httputil" "os" @@ -54,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) diff --git a/tests/mysql_test.go b/tests/mysql_test.go index 9470523c5..9a290b9e2 100644 --- a/tests/mysql_test.go +++ b/tests/mysql_test.go @@ -124,6 +124,24 @@ func TestMySQLAuthWithCredentialsFile(t *testing.T) { "mysql", cfg.FormatDSN()) } -func TestMySQLHealthCheck(t *testing.T) { - testHealthCheck(t, *mysqlConnName) +func TestMySQLAuthWithCredentialsJSON(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMySQLVars(t) + creds := keyfile(t) + _, _, cleanup := removeAuthEnvVar(t) + defer cleanup() + + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + DBName: *mysqlDB, + AllowNativePasswords: true, + Addr: "127.0.0.1:3306", + Net: "tcp", + } + proxyConnTest(t, + []string{"--json-credentials", creds, *mysqlConnName}, + "mysql", cfg.FormatDSN()) } diff --git a/tests/postgres_test.go b/tests/postgres_test.go index f0e292441..9ba9692c9 100644 --- a/tests/postgres_test.go +++ b/tests/postgres_test.go @@ -119,6 +119,22 @@ func TestPostgresAuthWithCredentialsFile(t *testing.T) { "pgx", dsn) } +func TestPostgresAuthWithCredentialsJSON(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + creds := keyfile(t) + _, _, cleanup := removeAuthEnvVar(t) + defer cleanup() + + dsn := fmt.Sprintf("host=localhost user=%s password=%s database=%s sslmode=disable", + *postgresUser, *postgresPass, *postgresDB) + proxyConnTest(t, + []string{"--json-credentials", string(creds), *postgresConnName}, + "pgx", dsn) +} + func TestAuthWithGcloudAuth(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") diff --git a/tests/sqlserver_test.go b/tests/sqlserver_test.go index e243ff4e8..3090649c1 100644 --- a/tests/sqlserver_test.go +++ b/tests/sqlserver_test.go @@ -85,6 +85,22 @@ func TestSQLServerAuthWithCredentialsFile(t *testing.T) { "sqlserver", dsn) } +func TestSQLServerAuthWithCredentialsJSON(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSQLServerVars(t) + creds := keyfile(t) + _, _, cleanup := removeAuthEnvVar(t) + defer cleanup() + + dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", + *sqlserverUser, *sqlserverPass, *sqlserverDB) + proxyConnTest(t, + []string{"--json-credentials", creds, *sqlserverConnName}, + "sqlserver", dsn) +} + func TestSQLServerHealthCheck(t *testing.T) { testHealthCheck(t, *sqlserverConnName) }