Skip to content

Commit

Permalink
feat: Downscope OAuth2 token included in ephemeral certificate (#332)
Browse files Browse the repository at this point in the history
Fixes #328.
  • Loading branch information
enocom committed Oct 7, 2022
1 parent 29d8784 commit d13dd6f
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 18 deletions.
19 changes: 13 additions & 6 deletions dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const (
defaultTCPKeepAlive = 30 * time.Second
// serverProxyPort is the port the server-side proxy receives connections on.
serverProxyPort = "3307"
// iamLoginScope is the OAuth2 scope used for tokens embedded in the ephemeral
// certificate.
iamLoginScope = "https://www.googleapis.com/auth/sqlservice.login"
)

var (
Expand Down Expand Up @@ -121,13 +124,17 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) {
// If callers have not provided a token source, either explicitly with
// WithTokenSource or implicitly with WithCredentialsJSON etc, then use the
// default token source.
if cfg.tokenSource == nil {
if cfg.iamLoginTokenSource == nil {
ts, err := google.DefaultTokenSource(ctx, sqladmin.SqlserviceAdminScope)
if err != nil {
return nil, fmt.Errorf("failed to create token source: %v", err)
}
cfg.tokenSource = ts
cfg.sqladminOpts = append(cfg.sqladminOpts, option.WithTokenSource(ts))
scoped, err := google.DefaultTokenSource(ctx, iamLoginScope)
if err != nil {
return nil, fmt.Errorf("failed to create scoped token source: %v", err)
}
cfg.iamLoginTokenSource = scoped
}

if cfg.rsaKey == nil {
Expand All @@ -143,15 +150,15 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) {
return nil, fmt.Errorf("failed to create sqladmin client: %v", err)
}

dialCfg := dialCfg{
dc := dialCfg{
ipType: cloudsql.PublicIP,
tcpKeepAlive: defaultTCPKeepAlive,
refreshCfg: cloudsql.RefreshCfg{
UseIAMAuthN: cfg.useIAMAuthN,
},
}
for _, opt := range cfg.dialOpts {
opt(&dialCfg)
opt(&dc)
}

if err := trace.InitMetrics(); err != nil {
Expand All @@ -162,9 +169,9 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) {
key: cfg.rsaKey,
refreshTimeout: cfg.refreshTimeout,
sqladmin: client,
defaultDialCfg: dialCfg,
defaultDialCfg: dc,
dialerID: uuid.New().String(),
iamTokenSource: cfg.tokenSource,
iamTokenSource: cfg.iamLoginTokenSource,
dialFunc: cfg.dialFunc,
}
return d, nil
Expand Down
56 changes: 44 additions & 12 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ import (
type Option func(d *dialerConfig)

type dialerConfig struct {
rsaKey *rsa.PrivateKey
sqladminOpts []apiopt.ClientOption
dialOpts []DialOption
dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
refreshTimeout time.Duration
useIAMAuthN bool
tokenSource oauth2.TokenSource
useragents []string
rsaKey *rsa.PrivateKey
sqladminOpts []apiopt.ClientOption
dialOpts []DialOption
dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
refreshTimeout time.Duration
useIAMAuthN bool
iamLoginTokenSource oauth2.TokenSource
useragents []string
// err tracks any dialer options that may have failed.
err error
}
Expand Down Expand Up @@ -79,8 +79,15 @@ func WithCredentialsJSON(b []byte) Option {
d.err = errtype.NewConfigError(err.Error(), "n/a")
return
}
d.tokenSource = c.TokenSource
d.sqladminOpts = append(d.sqladminOpts, apiopt.WithCredentials(c))

// Create another set of credentials scoped to login only
scoped, err := google.CredentialsFromJSON(context.Background(), b, iamLoginScope)
if err != nil {
d.err = errtype.NewConfigError(err.Error(), "n/a")
return
}
d.iamLoginTokenSource = scoped.TokenSource
}
}

Expand All @@ -99,15 +106,40 @@ func WithDefaultDialOptions(opts ...DialOption) Option {
}
}

// WithTokenSource returns an Option that specifies an OAuth2 token source
// to be used as the basis for authentication.
// WithTokenSource returns an Option that specifies an OAuth2 token source to be
// used as the basis for authentication.
//
// When Auth IAM AuthN is enabled, use WithIAMAuthNTokenSources to set the token
// source for login tokens separately from the API client token source.
// WithTokenSource should not be used with WithIAMAuthNTokenSources.
func WithTokenSource(s oauth2.TokenSource) Option {
return func(d *dialerConfig) {
d.tokenSource = s
d.iamLoginTokenSource = s
d.sqladminOpts = append(d.sqladminOpts, apiopt.WithTokenSource(s))
}
}

// WithIAMAuthNTokenSources sets the oauth2.TokenSource for the API client and a
// second token source for IAM AuthN login tokens. The API client token source
// should have the following scopes:
//
// 1. https://www.googleapis.com/auth/sqlservice.admin, and
// 2. https://www.googleapis.com/auth/cloud-platform
//
// The IAM AuthN token source on the other hand should only have:
//
// 1. https://www.googleapis.com/auth/sqlservice.login.
//
// Prefer this option over WithTokenSource when using IAM AuthN which does not
// distinguish between the two token sources. WithIAMAuthNTokenSources should
// not be used with WithTokenSource.
func WithIAMAuthNTokenSources(apiTS, iamLoginTS oauth2.TokenSource) Option {
return func(d *dialerConfig) {
d.iamLoginTokenSource = iamLoginTS
d.sqladminOpts = append(d.sqladminOpts, apiopt.WithTokenSource(apiTS))
}
}

// WithRSAKey returns an Option that specifies a rsa.PrivateKey used to represent the client.
func WithRSAKey(k *rsa.PrivateKey) Option {
return func(d *dialerConfig) {
Expand Down

0 comments on commit d13dd6f

Please sign in to comment.