Skip to content

Commit

Permalink
Enable credential provider authentication for self-hosted mysql DB
Browse files Browse the repository at this point in the history
refactored New function to NewClient

using opentelemetry http wrapper and using http client's in-built timeout

Addressed some of the review comments

Renamed token, tokenConfig, tokenClient to tokensource, tokenSourceConfig, tokenSourceClient
Using go-resty instead of default http client
AuthInjector can inject more types of authentications like mTLS
Added default values and corresponding tests for token source config

Addressed some of the review comments

Calling CheckAndSetDefaults in NewClient
Moved url substitution to a new function

deleted old file
  • Loading branch information
saisandeep-flipkart committed Oct 4, 2023
1 parent 9f946de commit 0422d61
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 2 deletions.
4 changes: 4 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,10 @@ const (
// ConnectMyComputerNodeOwnerLabel is a label used to control access to the node managed by
// Teleport Connect as part of Connect My Computer. See [teleterm.connectmycomputer.RoleSetup].
ConnectMyComputerNodeOwnerLabel = TeleportNamespace + "/connect-my-computer/owner"

// TokenAuthEnabledLabel is used identify whether enable token based authentication instead of regular
// certificate based authentication
TokenAuthEnabledLabel = TeleportNamespace + "/token-auth-enabled"
)

var (
Expand Down
6 changes: 6 additions & 0 deletions api/types/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ type Database interface {
// GetCloud gets the cloud this database is running on, or an empty string if it
// isn't running on a cloud provider.
GetCloud() string
// IsTokenAuthEnabled returns true if for a certain db token auth should be used instead of certificate auth
IsTokenAuthEnabled() bool
}

// NewDatabaseV3 creates a new database resource.
Expand All @@ -156,6 +158,10 @@ func (d *DatabaseV3) GetVersion() string {
return d.Version
}

func (d *DatabaseV3) IsTokenAuthEnabled() bool {
return d.Metadata.IsTokenAuthEnabled()
}

// GetKind returns the database resource kind.
func (d *DatabaseV3) GetKind() string {
return d.Kind
Expand Down
8 changes: 8 additions & 0 deletions api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,14 @@ func (m *Metadata) SetOrigin(origin string) {
m.Labels[OriginLabel] = origin
}

func (m *Metadata) IsTokenAuthEnabled() bool {
if m.Labels == nil {
return false
}
tokenAuthEnabled, _ := utils.ParseBool(m.Labels[TokenAuthEnabledLabel])
return tokenAuthEnabled
}

// CheckAndSetDefaults checks validity of all parameters and sets defaults
func (m *Metadata) CheckAndSetDefaults() error {
if m.Name == "" {
Expand Down
20 changes: 20 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ package config
import (
"crypto/x509"
"errors"
"github.com/gravitational/teleport/lib/tokensource"
"golang.org/x/exp/maps"
"io"
stdlog "log"
"net"
Expand All @@ -34,6 +36,7 @@ import (
"runtime"
"strconv"
"strings"
"text/template"
"time"
"unicode"

Expand Down Expand Up @@ -1506,6 +1509,23 @@ func applyKubeConfig(fc *FileConfig, cfg *servicecfg.Config) error {
// applyDatabasesConfig applies file configuration for the "db_service" section.
func applyDatabasesConfig(fc *FileConfig, cfg *servicecfg.Config) error {
cfg.Databases.Enabled = true
tokenConfig := fc.Databases.TokenSourceConfig
if tokenConfig.Enabled.Value {
var err error
cfg.Databases.TokenSourceConfig.Enabled = tokenConfig.Enabled.Value
if tokenConfig.UrlTemplate == "" {
return trace.Errorf("token source url template cannot be empty")
}
cfg.Databases.TokenSourceConfig.UrlTemplate, err = template.New("url").Parse(tokenConfig.UrlTemplate)
if err != nil {
return trace.Wrap(err)
}
cfg.Databases.TokenSourceConfig.Timeout = tokenConfig.Timeout
if tokenConfig.TokenSourceAuthConfig.Scheme != "" && tokensource.AuthCreators[tokenConfig.TokenSourceAuthConfig.Scheme] == nil {
return trace.Errorf("no known token source authentication scheme %s, valid values are %v", tokenConfig.TokenSourceAuthConfig.Scheme, maps.Keys(tokensource.AuthCreators))
}
cfg.Databases.TokenSourceConfig.AuthConfig.Scheme = tokenConfig.TokenSourceAuthConfig.Scheme
}
for _, matcher := range fc.Databases.ResourceMatchers {
cfg.Databases.ResourceMatchers = append(cfg.Databases.ResourceMatchers,
services.ResourceMatcher{
Expand Down
64 changes: 64 additions & 0 deletions lib/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4197,3 +4197,67 @@ func TestGetInstallerProxyAddr(t *testing.T) {
})
}
}

func TestApplyFileConfig_DbTokenAuth_errors(t *testing.T) {
tests := []struct {
name string
url string
scheme string
error string
}{
{
name: "empty url template",
url: "",
scheme: "",
error: "url template cannot be empty",
},
{
name: "invalid url template",
url: "{{User}}",
scheme: "",
error: "\"User\" not defined",
},
{
name: "invalid auth scheme",
url: "{{.User}}",
scheme: "abcd",
error: "no known token source authentication scheme abcd",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
defaultCfg := servicecfg.MakeDefaultConfig()
err := ApplyFileConfig(&FileConfig{
Databases: Databases{
Service: Service{
EnabledFlag: "yes",
},
TokenSourceConfig: TokenSourceConfig{
Enabled: types.BoolOption{Value: true},
UrlTemplate: test.url,
TokenSourceAuthConfig: TokenSourceAuthConfig{
Scheme: test.scheme,
},
},
},
}, defaultCfg)
require.ErrorContains(t, err, test.error)
})
}
}

func TestApplyFileConfig_DbTokenAuth(t *testing.T) {
defaultCfg := servicecfg.MakeDefaultConfig()
err := ApplyFileConfig(&FileConfig{
Databases: Databases{
Service: Service{
EnabledFlag: "yes",
},
TokenSourceConfig: TokenSourceConfig{
Enabled: types.BoolOption{Value: true},
UrlTemplate: "http://localhost/{{.Database}}",
},
},
}, defaultCfg)
require.Nil(t, err)
}
13 changes: 13 additions & 0 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,19 @@ type Databases struct {
AWSMatchers []AWSMatcher `yaml:"aws,omitempty"`
// AzureMatchers match Azure hosted databases.
AzureMatchers []AzureMatcher `yaml:"azure,omitempty"`
// TokenSourceConfig has http and authentication config for token exchange service
TokenSourceConfig TokenSourceConfig `yaml:"token_source,omitempty"`
}

type TokenSourceConfig struct {
Enabled types.BoolOption `yaml:"enabled"`
UrlTemplate string `yaml:"url_template"`
Timeout time.Duration `yaml:"timeout"`
TokenSourceAuthConfig TokenSourceAuthConfig `yaml:"authentication"`
}

type TokenSourceAuthConfig struct {
Scheme string `yaml:"scheme,omitempty"`
}

// ResourceMatcher matches cluster resources.
Expand Down
1 change: 1 addition & 0 deletions lib/service/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) {
OnHeartbeat: process.OnHeartbeat(teleport.ComponentDatabase),
ConnectionMonitor: connMonitor,
ConnectedProxyGetter: proxyGetter,
TokenSourceConfig: process.Config.Databases.TokenSourceConfig,
})
if err != nil {
return trace.Wrap(err)
Expand Down
3 changes: 3 additions & 0 deletions lib/service/servicecfg/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package servicecfg

import (
"github.com/gravitational/teleport/lib/tokensource"
"strings"

"github.com/gravitational/trace"
Expand Down Expand Up @@ -42,6 +43,8 @@ type DatabasesConfig struct {
AzureMatchers []types.AzureMatcher
// Limiter limits the connection and request rates.
Limiter limiter.Config
// TokenConfig has http and authentication config for token exchange service
TokenSourceConfig tokensource.ClientConfig
}

// Database represents a single database that's being proxied.
Expand Down
21 changes: 21 additions & 0 deletions lib/srv/db/common/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"github.com/gravitational/teleport/lib/tokensource"
"io"
"net/http"
"net/url"
Expand Down Expand Up @@ -93,6 +94,9 @@ type Auth interface {
// GetAWSIAMCreds returns the AWS IAM credentials, including access key,
// secret access key and session token.
GetAWSIAMCreds(ctx context.Context, sessionCtx *Session) (string, string, string, error)

IsTokenAuthEnabled() bool
GetTokenAuthCredentials(ctx context.Context, sessionCtx *Session) (string, string, error)
// Closer releases all resources used by authenticator.
io.Closer
}
Expand All @@ -113,6 +117,8 @@ type AuthConfig struct {
AuthClient AuthClient
// Clients provides interface for obtaining cloud provider clients.
Clients cloud.Clients
// TokenSourceClient
TokenSourceClient *tokensource.Client
// Clock is the clock implementation.
Clock clockwork.Clock
// Log is used for logging.
Expand Down Expand Up @@ -551,6 +557,12 @@ func (a *dbAuth) getTLSConfigVerifyFull(ctx context.Context, sessionCtx *Session
return tlsConfig, nil
}

// When connecting to onprem database but if token auth is enabled
// auth happens via token so don't generate client cert
if a.IsTokenAuthEnabled() && sessionCtx.Database.IsTokenAuthEnabled() {
return tlsConfig, nil
}

// Otherwise, when connecting to an onprem database, generate a client
// certificate. The database instance should be configured with
// Teleport's CA obtained with 'tctl auth sign --type=db'.
Expand Down Expand Up @@ -904,6 +916,15 @@ func (a *dbAuth) GetAWSIAMCreds(ctx context.Context, sessionCtx *Session) (strin
return creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken, nil
}

func (a *dbAuth) IsTokenAuthEnabled() bool {
return a.cfg.TokenSourceClient != nil
}

// GetTokenAuthCredentials exchanges username/token and exchanges it with actual username and password
func (a *dbAuth) GetTokenAuthCredentials(ctx context.Context, sessionCtx *Session) (string, string, error) {
return a.cfg.TokenSourceClient.GetCredentials(ctx, sessionCtx.Database.GetMetadata().Name, sessionCtx.Identity.Username, sessionCtx.DatabaseUser)
}

// Close releases all resources used by authenticator.
func (a *dbAuth) Close() error {
return a.cfg.Clients.Close()
Expand Down
5 changes: 5 additions & 0 deletions lib/srv/db/mysql/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ func (e *Engine) connect(ctx context.Context, sessionCtx *common.Session) (*clie
return nil, trace.Wrap(err)
}
user = services.MakeAzureDatabaseLoginUsername(sessionCtx.Database, user)
case e.Auth.IsTokenAuthEnabled() && sessionCtx.Database.IsTokenAuthEnabled():
user, password, err = e.Auth.GetTokenAuthCredentials(ctx, sessionCtx)
if err != nil {
return nil, trace.Wrap(err)
}
}

// Use default net dialer unless it is already initialized.
Expand Down
8 changes: 6 additions & 2 deletions lib/srv/db/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package db
import (
"context"
"crypto/tls"
"github.com/gravitational/teleport/lib/tokensource"
"net"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -141,6 +142,8 @@ type Config struct {
// discoveryResourceChecker performs some pre-checks when creating databases
// discovered by the discovery service.
discoveryResourceChecker cloud.DiscoveryResourceChecker

TokenSourceConfig tokensource.ClientConfig
}

// NewAuditFn defines a function that creates an audit logger.
Expand Down Expand Up @@ -169,8 +172,9 @@ func (c *Config) CheckAndSetDefaults(ctx context.Context) (err error) {
}
if c.Auth == nil {
c.Auth, err = common.NewAuth(common.AuthConfig{
AuthClient: c.AuthClient,
Clock: c.Clock,
AuthClient: c.AuthClient,
Clock: c.Clock,
TokenSourceClient: tokensource.NewClient(c.TokenSourceConfig),
})
if err != nil {
return trace.Wrap(err)
Expand Down
33 changes: 33 additions & 0 deletions lib/tokensource/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tokensource

import (
"github.com/go-resty/resty/v2"
)

const (
NoneAuthScheme = "NONE"
)

const DefaultAuthScheme = NoneAuthScheme

type AuthCreator func(config AuthConfig) AuthInjector

var AuthCreators = make(map[string]AuthCreator)

func init() {
AuthCreators[NoneAuthScheme] = newNoAuthInjector
}

type AuthInjector interface {
injectAuth(c *resty.Client)
}

type NoAuthInjector struct {
}

func (i NoAuthInjector) injectAuth(c *resty.Client) {
}

func newNoAuthInjector(config AuthConfig) AuthInjector {
return NoAuthInjector{}
}
Loading

0 comments on commit 0422d61

Please sign in to comment.