diff --git a/dialer.go b/dialer.go index 3af8bd6a..ed03d7a0 100644 --- a/dialer.go +++ b/dialer.go @@ -137,6 +137,11 @@ type Dialer struct { // ahead cache assumes a background goroutine may run consistently. lazyRefresh bool + // disableMetadataExchange is a temporary addition to help clients who + // cannot use the metadata exchange yet. In future versions, this field + // should be removed. + disableMetadataExchange bool + staticConnInfo io.Reader client *alloydbadmin.AlloyDBAdminClient @@ -183,6 +188,10 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) { return nil, cfg.err } } + if cfg.disableMetadataExchange && cfg.useIAMAuthN { + return nil, errors.New("incompatible options: WithOptOutOfAdvancedConnection " + + "check cannot be used with WithIAMAuthN") + } userAgent := strings.Join(cfg.userAgents, " ") // Add this to the end to make sure it's not overridden cfg.adminOpts = append(cfg.adminOpts, option.WithUserAgent(userAgent)) @@ -221,21 +230,22 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) { return nil, err } d := &Dialer{ - closed: make(chan struct{}), - cache: make(map[alloydb.InstanceURI]monitoredCache), - lazyRefresh: cfg.lazyRefresh, - staticConnInfo: cfg.staticConnInfo, - keyGenerator: g, - refreshTimeout: cfg.refreshTimeout, - client: client, - logger: cfg.logger, - defaultDialCfg: dialCfg, - dialerID: uuid.New().String(), - dialFunc: cfg.dialFunc, - useIAMAuthN: cfg.useIAMAuthN, - iamTokenSource: ts, - userAgent: userAgent, - buffer: newBuffer(), + closed: make(chan struct{}), + cache: make(map[alloydb.InstanceURI]monitoredCache), + lazyRefresh: cfg.lazyRefresh, + disableMetadataExchange: cfg.disableMetadataExchange, + staticConnInfo: cfg.staticConnInfo, + keyGenerator: g, + refreshTimeout: cfg.refreshTimeout, + client: client, + logger: cfg.logger, + defaultDialCfg: dialCfg, + dialerID: uuid.New().String(), + dialFunc: cfg.dialFunc, + useIAMAuthN: cfg.useIAMAuthN, + iamTokenSource: ts, + userAgent: userAgent, + buffer: newBuffer(), } return d, nil } @@ -351,12 +361,14 @@ func (d *Dialer) Dial(ctx context.Context, instance string, opts ...DialOption) return nil, errtype.NewDialError("handshake failed", inst.String(), err) } - // The metadata exchange must occur after the TLS connection is established - // to avoid leaking sensitive information. - err = d.metadataExchange(tlsConn) - if err != nil { - _ = tlsConn.Close() // best effort close attempt - return nil, err + if !d.disableMetadataExchange { + // The metadata exchange must occur after the TLS connection is established + // to avoid leaking sensitive information. + err = d.metadataExchange(tlsConn) + if err != nil { + _ = tlsConn.Close() // best effort close attempt + return nil, err + } } latency := time.Since(startTime).Milliseconds() @@ -598,6 +610,7 @@ func (d *Dialer) connectionInfoCache( d.logger, d.client, k, d.refreshTimeout, d.dialerID, + d.disableMetadataExchange, ) case d.staticConnInfo != nil: var err error @@ -615,6 +628,7 @@ func (d *Dialer) connectionInfoCache( d.logger, d.client, k, d.refreshTimeout, d.dialerID, + d.disableMetadataExchange, ) } var open uint64 diff --git a/dialer_test.go b/dialer_test.go index f2f5fdaa..051b7526 100644 --- a/dialer_test.go +++ b/dialer_test.go @@ -49,6 +49,27 @@ func (stubTokenSource) Token() (*oauth2.Token, error) { return &oauth2.Token{}, nil } +func TestDialerIncompatibleOptions(t *testing.T) { + tcs := []struct { + desc string + opts []Option + }{ + { + desc: "opt out connection check doesn't work with IAM authn", + opts: []Option{WithOptOutOfAdvancedConnectionCheck(), WithIAMAuthN()}, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, err := NewDialer(context.Background(), tc.opts...) + if err == nil { + t.Fatalf("got = %v, want no error", err) + } + }) + } +} + func TestDialerCanConnectToInstance(t *testing.T) { ctx := context.Background() inst := mock.NewFakeInstance( diff --git a/e2e_test.go b/e2e_test.go index d29bfa4f..1fd27196 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -106,6 +106,16 @@ func TestPgxConnect(t *testing.T) { ) }, }, + { + desc: "metadata exchange disabled", + f: func(ctx context.Context) (*pgxpool.Pool, func() error, error) { + return connectPgx( + ctx, alloydbInstanceName, + alloydbUser, alloydbPass, alloydbDB, + alloydbconn.WithOptOutOfAdvancedConnectionCheck(), + ) + }, + }, } for _, tc := range tcs { diff --git a/internal/alloydb/instance.go b/internal/alloydb/instance.go index af9318e0..2b4ff478 100644 --- a/internal/alloydb/instance.go +++ b/internal/alloydb/instance.go @@ -169,13 +169,14 @@ func NewRefreshAheadCache( key *rsa.PrivateKey, refreshTimeout time.Duration, dialerID string, + disableMetadataExchange bool, ) *RefreshAheadCache { ctx, cancel := context.WithCancel(context.Background()) i := &RefreshAheadCache{ instanceURI: instance, logger: l, l: rate.NewLimiter(rate.Every(refreshInterval), refreshBurst), - r: newAdminAPIClient(client, key, dialerID), + r: newAdminAPIClient(client, key, dialerID, disableMetadataExchange), refreshTimeout: refreshTimeout, ctx: ctx, cancel: cancel, diff --git a/internal/alloydb/instance_test.go b/internal/alloydb/instance_test.go index 5dad9e68..7b883416 100644 --- a/internal/alloydb/instance_test.go +++ b/internal/alloydb/instance_test.go @@ -158,6 +158,7 @@ func TestConnectionInfo(t *testing.T) { testInstanceURI(), nullLogger{}, c, rsaKey, 30*time.Second, "dialer-id", + false, ) if err != nil { t.Fatalf("failed to create mock instance: %v", err) @@ -210,6 +211,7 @@ func TestConnectInfoErrors(t *testing.T) { testInstanceURI(), nullLogger{}, c, rsaKey, 0, "dialer-id", + false, ) if err != nil { t.Fatalf("failed to initialize Instance: %v", err) @@ -243,6 +245,7 @@ func TestClose(t *testing.T) { testInstanceURI(), nullLogger{}, c, rsaKey, 30, "dialer-ider", + false, ) if err != nil { t.Fatalf("failed to initialize Instance: %v", err) diff --git a/internal/alloydb/lazy.go b/internal/alloydb/lazy.go index 0f2e1985..8d06ea1c 100644 --- a/internal/alloydb/lazy.go +++ b/internal/alloydb/lazy.go @@ -43,15 +43,12 @@ func NewLazyRefreshCache( key *rsa.PrivateKey, _ time.Duration, dialerID string, + disableMetadataExchange bool, ) *LazyRefreshCache { return &LazyRefreshCache{ uri: uri, logger: l, - r: newAdminAPIClient( - client, - key, - dialerID, - ), + r: newAdminAPIClient(client, key, dialerID, disableMetadataExchange), } } diff --git a/internal/alloydb/lazy_test.go b/internal/alloydb/lazy_test.go index b108e051..9666da3a 100644 --- a/internal/alloydb/lazy_test.go +++ b/internal/alloydb/lazy_test.go @@ -49,6 +49,7 @@ func TestLazyRefreshCacheConnectionInfo(t *testing.T) { cache := NewLazyRefreshCache( testInstanceURI(), nullLogger{}, c, rsaKey, 30*time.Second, "", + false, ) ci, err := cache.ConnectionInfo(context.Background()) @@ -91,6 +92,7 @@ func TestLazyRefreshCacheForceRefresh(t *testing.T) { cache := NewLazyRefreshCache( testInstanceURI(), nullLogger{}, c, rsaKey, 30*time.Second, "", + false, ) _, err = cache.ConnectionInfo(context.Background()) diff --git a/internal/alloydb/refresh.go b/internal/alloydb/refresh.go index 2bd922d1..e3b97223 100644 --- a/internal/alloydb/refresh.go +++ b/internal/alloydb/refresh.go @@ -121,6 +121,7 @@ func fetchClientCertificate( cl *alloydbadmin.AlloyDBAdminClient, inst InstanceURI, key *rsa.PrivateKey, + disableMetadataExchange bool, ) (cc *clientCertificate, err error) { var end trace.EndSpanFunc ctx, end = trace.StartSpan(ctx, "cloud.google.com/go/alloydbconn/internal.FetchEphemeralCert") @@ -138,7 +139,7 @@ func fetchClientCertificate( ), PublicKey: buf.String(), CertDuration: durationpb.New(time.Second * 3600), - UseMetadataExchange: true, + UseMetadataExchange: !disableMetadataExchange, } resp, err := cl.GenerateClientCertificate(ctx, req) if err != nil { @@ -225,11 +226,13 @@ func newAdminAPIClient( client *alloydbadmin.AlloyDBAdminClient, key *rsa.PrivateKey, dialerID string, + disableMetadataExchange bool, ) adminAPIClient { return adminAPIClient{ - client: client, - key: key, - dialerID: dialerID, + client: client, + key: key, + dialerID: dialerID, + disableMetadataExchange: disableMetadataExchange, } } @@ -242,6 +245,9 @@ type adminAPIClient struct { key *rsa.PrivateKey // dialerID is the unique ID of the associated dialer. dialerID string + // disableMetadataExchange is a temporary addition to ease the migration to + // when the metadata exchange is required. + disableMetadataExchange bool } // ConnectionInfo holds all the data necessary to connect to an instance. @@ -286,7 +292,7 @@ func (c adminAPIClient) connectionInfo( certCh := make(chan certRes, 1) go func() { defer close(certCh) - cc, err := fetchClientCertificate(ctx, c.client, i, c.key) + cc, err := fetchClientCertificate(ctx, c.client, i, c.key, c.disableMetadataExchange) certCh <- certRes{cc: cc, err: err} }() diff --git a/internal/alloydb/refresh_test.go b/internal/alloydb/refresh_test.go index 9be270ae..af321360 100644 --- a/internal/alloydb/refresh_test.go +++ b/internal/alloydb/refresh_test.go @@ -62,7 +62,7 @@ func TestRefresh(t *testing.T) { if err != nil { t.Fatalf("admin API client error: %v", err) } - r := newAdminAPIClient(cl, rsaKey, testDialerID) + r := newAdminAPIClient(cl, rsaKey, testDialerID, false) res, err := r.connectionInfo(context.Background(), cn) if err != nil { t.Fatalf("performRefresh unexpectedly failed with error: %v", err) @@ -124,7 +124,7 @@ func TestRefreshFailsFast(t *testing.T) { if err != nil { t.Fatalf("admin API client error: %v", err) } - r := newAdminAPIClient(cl, rsaKey, testDialerID) + r := newAdminAPIClient(cl, rsaKey, testDialerID, false) _, err = r.connectionInfo(context.Background(), cn) if err != nil { diff --git a/options.go b/options.go index 47c26cd5..e23f9335 100644 --- a/options.go +++ b/options.go @@ -49,6 +49,10 @@ type dialerConfig struct { logger debug.ContextLogger lazyRefresh bool + // disableMetadataExchange is a temporary addition and will be removed in + // future versions. + disableMetadataExchange bool + staticConnInfo io.Reader // err tracks any dialer options that may have failed. err error @@ -241,6 +245,24 @@ func WithStaticConnectionInfo(r io.Reader) Option { } } +// WithOptOutOfAdvancedConnectionCheck disables the dataplane permission check. +// It is intended only for clients who are running in an environment where the +// workload's IP address is otherwise unknown and cannot be allow-listed in a +// VPC Service Control security perimeter. This option is incompatible with IAM +// Authentication. +// +// NOTE: This option is for internal usage only and is meant to ease the +// migration when the advanced check will be required on the server. In future +// versions this will revert to a no-op and should not be used. If you think +// you need this option, open an issue on +// https://github.com/GoogleCloudPlatform/alloydb-go-connector for design +// advice. +func WithOptOutOfAdvancedConnectionCheck() Option { + return func(d *dialerConfig) { + d.disableMetadataExchange = true + } +} + // A DialOption is an option for configuring how a Dialer's Dial call is // executed. type DialOption func(d *dialCfg) diff --git a/pgxpool_test.go b/pgxpool_test.go index 3a8a28fc..7c03b0cd 100644 --- a/pgxpool_test.go +++ b/pgxpool_test.go @@ -42,13 +42,14 @@ import ( // that should be called when you're done with the database connection. func connectPgx( ctx context.Context, instURI, user, pass, dbname string, + opts ...alloydbconn.Option, ) (*pgxpool.Pool, func() error, error) { // First initialize the dialer. alloydbconn.NewDialer accepts additional // options to configure credentials, timeouts, etc. // // For details, see: // https://pkg.go.dev/cloud.google.com/go/alloydbconn#Option - d, err := alloydbconn.NewDialer(ctx) + d, err := alloydbconn.NewDialer(ctx, opts...) if err != nil { noop := func() error { return nil } return nil, noop, fmt.Errorf("failed to init Dialer: %v", err)