Skip to content

Commit

Permalink
feat: add support for Auto IAM AuthN
Browse files Browse the repository at this point in the history
  • Loading branch information
enocom committed Sep 29, 2023
1 parent 1219cd5 commit 9bc9c8a
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 12 deletions.
88 changes: 83 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ Overview
hourly basis. Existing client connections are unaffected by the refresh
cycle.
Authenication
The Proxy uses Application Default Credentials by default. Enable these
credentials with gcloud:
gcloud auth application-default login
In Google-run environments, Application Default Credentials are already
available and do not need to be retrieved.
The Proxy will use the environment's IAM principal when authenticating to
the backend. To use a specific set of credentials, use the
--credentials-file flag, e.g.,
./alloydb-auth-proxy --credentials-file /path/to/key.json \
projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE
See the individual flags below, for more options.
Starting the Proxy
To start the proxy, you will need your instance URI, which may be found in
Expand Down Expand Up @@ -184,8 +203,29 @@ Instance Level Configuration
instances, the proxy will ensure that the last path element is
'.s.PGSQL.5432' appending it if necessary. For example,
./cloud-sql-proxy \
'my-project:us-central1:my-db-server?unix-socket-path=/path/to/socket'
./alloydb-aith-proxy \
'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE1?unix-socket-path=/path/to/socket'
(*) indicates a flag that may be used as a query parameter
Automatic IAM Authentication
The Auth Proxy support Automatic IAM Authentication where the Proxy
retrieves the environment's IAM principal's OAuth2 token and supplies it to
the backend. When a client connects to the Proxy, there is no need to supply
a database user password.
To enable the feature, run:
./alloydb-auth-proxy \
--auto-iam-authn \
'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE'
In addition, Auto IAM AuthN may be enabled on a per-instance basis with the
query string syntax described above.
./alloydb-auth-proxy \
'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE?auto-iam-authn=true'
Health checks
Expand Down Expand Up @@ -436,11 +476,13 @@ status code.`)

// Global and per instance flags
pflags.StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
"Address on which to bind AlloyDB instance listeners.")
"(*) Address on which to bind AlloyDB instance listeners.")
pflags.IntVarP(&c.conf.Port, "port", "p", 5432,
"Initial port to use for listeners. Subsequent listeners increment from this value.")
"(*) Initial port to use for listeners. Subsequent listeners increment from this value.")
pflags.StringVarP(&c.conf.UnixSocket, "unix-socket", "u", "",
`Enables Unix sockets for all listeners using the provided directory.`)
`(*) Enables Unix sockets for all listeners using the provided directory.`)
pflags.BoolVarP(&c.conf.AutoIAMAuthN, "auto-iam-authn", "i", false,
"(*) Enables Automatic IAM Authentication for all instances")

v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer("-", "_")))
v.SetEnvPrefix(envPrefix)
Expand Down Expand Up @@ -629,6 +671,11 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
ic.UnixSocket = u[0]

}

ic.AutoIAMAuthN, err = parseBoolOpt(q, "auto-iam-authn")
if err != nil {
return err
}
}
ics = append(ics, ic)
}
Expand All @@ -637,6 +684,37 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
return nil
}

// parseBoolOpt parses a boolean option from the query string.
// True is can be "t", "true" (case-insensitive).
// False can be "f" or "false" (case-insensitive).
func parseBoolOpt(q url.Values, name string) (bool, error) {
v, ok := q[name]
if !ok {
return false, nil
}

if len(v) != 1 {
return false, newBadCommandError(
fmt.Sprintf("%v param should be only one value: %q", name, v),
)
}

switch strings.ToLower(v[0]) {
// if only the key is present (and the value is empty string), accept that
// as true.
case "true", "t", "":
return true, nil
case "false", "f":
return false, nil
default:
// value is not recognized
return false, newBadCommandError(
fmt.Sprintf("%v query param should be true or false, got: %q",
name, v[0],
))
}
}

// runSignalWrapper watches for SIGTERM and SIGINT and interupts execution if necessary.
func runSignalWrapper(cmd *Command) (err error) {
defer cmd.cleanup()
Expand Down
55 changes: 55 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,61 @@ func TestNewCommandArguments(t *testing.T) {
Instances: []proxy.InstanceConnConfig{{Name: "projects/proj/locations/region/clusters/clust/instances/inst"}},
}),
},
{
desc: "Auto IAM AuthN",
args: []string{
"--auto-iam-authn",
"projects/proj/locations/region/clusters/clust/instances/inst",
},
want: withDefaults(&proxy.Config{
AutoIAMAuthN: true,
Instances: []proxy.InstanceConnConfig{{Name: "projects/proj/locations/region/clusters/clust/instances/inst"}},
}),
},
{
desc: "Auto IAM AuthN query param (key only)",
args: []string{
"projects/proj/locations/region/clusters/clust/instances/inst?auto-iam-authn",
},
want: withDefaults(&proxy.Config{
Instances: []proxy.InstanceConnConfig{{
AutoIAMAuthN: true,
Name: "projects/proj/locations/region/clusters/clust/instances/inst",
}},
}),
},
{
desc: "Auto IAM AuthN query param (t & f)",
args: []string{
"projects/proj/locations/region/clusters/clust/instances/inst1?auto-iam-authn=t",
"projects/proj/locations/region/clusters/clust/instances/inst2?auto-iam-authn=f",
},
want: withDefaults(&proxy.Config{
Instances: []proxy.InstanceConnConfig{{
AutoIAMAuthN: true,
Name: "projects/proj/locations/region/clusters/clust/instances/inst1",
}, {
AutoIAMAuthN: false,
Name: "projects/proj/locations/region/clusters/clust/instances/inst2",
}},
}),
},
{
desc: "Auto IAM AuthN query param (true & false)",
args: []string{
"projects/proj/locations/region/clusters/clust/instances/inst1?auto-iam-authn=true",
"projects/proj/locations/region/clusters/clust/instances/inst2?auto-iam-authn=false",
},
want: withDefaults(&proxy.Config{
Instances: []proxy.InstanceConnConfig{{
AutoIAMAuthN: true,
Name: "projects/proj/locations/region/clusters/clust/instances/inst1",
}, {
AutoIAMAuthN: false,
Name: "projects/proj/locations/region/clusters/clust/instances/inst2",
}},
}),
},
{
desc: "using the address flag",
args: []string{"--address", "0.0.0.0", "projects/proj/locations/region/clusters/clust/instances/inst"},
Expand Down
2 changes: 1 addition & 1 deletion cmd/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.0
1.4.1-auto-iam-authn-preview.0
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/GoogleCloudPlatform/alloydb-auth-proxy
go 1.20

require (
cloud.google.com/go/alloydbconn v1.4.0
cloud.google.com/go/alloydbconn v1.4.1-0.20230929162911-57a3439f4e94
contrib.go.opencensus.io/exporter/prometheus v0.4.2
contrib.go.opencensus.io/exporter/stackdriver v0.13.14
github.com/coreos/go-systemd/v22 v22.5.0
Expand All @@ -23,7 +23,7 @@ require (

require (
cloud.google.com/go v0.110.7 // indirect
cloud.google.com/go/alloydb v1.4.0 // indirect
cloud.google.com/go/alloydb v1.5.0 // indirect
cloud.google.com/go/compute v1.23.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/longrunning v0.5.1 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2Z
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
cloud.google.com/go/alloydb v1.4.0 h1:+T0fH8UynA6L90XCEYUkeMEbL+kS7h8vdBTfxJ5XeYw=
cloud.google.com/go/alloydb v1.4.0/go.mod h1:rE8WUOYC3NeZyLz/U/UDNY5jLUQKjUu8PzWtdvx+4Dk=
cloud.google.com/go/alloydbconn v1.4.0 h1:vXKKCzF3q9k3vL3FgBvzdkAiIasELc5Qgi40ZC6B1OA=
cloud.google.com/go/alloydbconn v1.4.0/go.mod h1:35ygofr1nt0os6k4E3ZXaDaKZAnq7ChXgwUrhFiMduQ=
cloud.google.com/go/alloydb v1.5.0 h1:Ab+rt0F7mMYjAG9q9FFRFQL8VM3ju2CjIFT3I7fBCNc=
cloud.google.com/go/alloydb v1.5.0/go.mod h1:/hPjP/H+sU/PhA2Ifpbd80POfQCfwtCWacxbOf6zCkI=
cloud.google.com/go/alloydbconn v1.4.1-0.20230929162911-57a3439f4e94 h1:368qnYKDuabKvEOEkxoL+nwIuNVi8d21jbR//xebu10=
cloud.google.com/go/alloydbconn v1.4.1-0.20230929162911-57a3439f4e94/go.mod h1:hctr9euJZxx9ly6aSzSrrQew10ifGWoIaI2CmFj/8i0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
Expand Down
13 changes: 13 additions & 0 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ type InstanceConnConfig struct {
// necessary. If set, UnixSocketPath takes precedence over UnixSocket, Addr
// and Port.
UnixSocketPath string

// AutoIAMAuthN enables automatic IAM authentication on the instance only.
// See Config.AutoIAMAuthN for more details.
AutoIAMAuthN bool
}

// Config contains all the configuration provided by the caller.
Expand All @@ -64,6 +68,11 @@ type Config struct {
// API.
UserAgent string

// AutoIAMAuthN enabled automatic IAM authentication which results in the
// Proxy sending the IAM principal's OAuth2 token to the backend to enable
// a passwordless login for callers.
AutoIAMAuthN bool

// Token is the Bearer token used for authorization.
Token string

Expand Down Expand Up @@ -272,6 +281,10 @@ func (c *Config) DialerOptions(l alloydb.Logger) ([]alloydbconn.Option, error) {
opts = append(opts, alloydbconn.WithAdminAPIEndpoint(c.APIEndpointURL))
}

if c.AutoIAMAuthN {
opts = append(opts, alloydbconn.WithIAMAuthN())
}

return opts, nil
}

Expand Down
18 changes: 18 additions & 0 deletions tests/alloydb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>`,
os.Getenv("ALLOYDB_USER"),
"Name of database user.",
)
alloydbIAMUser = flag.String(
"alloydb_iam_user",
os.Getenv("ALLOYDB_IAM_USER"),
"Name of database user.",
)
alloydbPass = flag.String(
"alloydb_pass",
os.Getenv("ALLOYDB_PASS"),
Expand All @@ -54,6 +59,8 @@ func requirePostgresVars(t *testing.T) {
t.Fatal("'alloydb_conn_name' not set")
case *alloydbUser:
t.Fatal("'alloydb_user' not set")
case *alloydbIAMUser:
t.Fatal("'alloydb_iam_user' not set")
case *alloydbPass:
t.Fatal("'alloydb_pass' not set")
case *alloydbDB:
Expand All @@ -72,6 +79,17 @@ func TestPostgresTCP(t *testing.T) {
proxyConnTest(t, []string{*alloydbInstanceName}, "pgx", dsn)
}

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

dsn := fmt.Sprintf("host=127.0.0.1 user=%v password=%v database=%v sslmode=disable",
*alloydbIAMUser, *alloydbPass, *alloydbDB)
proxyConnTest(t, []string{*alloydbInstanceName, "--auto-iam-authn"}, "pgx", dsn)
}

func createTempDir(t *testing.T) (string, func()) {
testDir, err := os.MkdirTemp("", "*")
if err != nil {
Expand Down

0 comments on commit 9bc9c8a

Please sign in to comment.