Plugins: Add sql support for the secure socks proxy (#64630)

This commit is contained in:
Stephanie Hingtgen 2023-03-16 07:54:19 -05:00 committed by GitHub
parent 68e38aad6a
commit 10db808ea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 651 additions and 104 deletions

1
.github/CODEOWNERS vendored
View File

@ -81,6 +81,7 @@
/pkg/infra/metrics/ @grafana/backend-platform
/pkg/infra/network/ @grafana/backend-platform
/pkg/infra/process/ @grafana/backend-platform
/pkg/infra/proxy/ @grafana/hosted-grafana-team
/pkg/infra/remotecache/ @grafana/backend-platform
/pkg/infra/serverlock/ @grafana/backend-platform
/pkg/infra/slugify/ @grafana/backend-platform

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/infra/proxy"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/validations"
@ -55,8 +56,8 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer
}
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagSecureSocksDatasourceProxy) &&
cfg.SecureSocksDSProxy.Enabled && secureSocksProxyEnabledOnDS(opts) {
err = newSecureSocksProxy(&cfg.SecureSocksDSProxy, transport)
cfg.SecureSocksDSProxy.Enabled && proxy.SecureSocksProxyEnabledOnDS(opts) {
err = proxy.NewSecureSocksHTTPProxy(&cfg.SecureSocksDSProxy, transport)
if err != nil {
logger.Error("Failed to enable secure socks proxy", "error", err.Error(), "datasource", datasourceName)
}

View File

@ -1,4 +1,4 @@
package httpclientprovider
package proxyutil
import (
"crypto/rand"
@ -7,30 +7,20 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewSecureSocksProxy(t *testing.T) {
func SetupTestSecureSocksProxySettings(t *testing.T) *setting.SecureSocksDSProxySettings {
proxyAddress := "localhost:3000"
serverName := "localhost"
tempDir := t.TempDir()
// create empty file for testing invalid configs
tempEmptyFile := filepath.Join(tempDir, "emptyfile.txt")
// nolint:gosec
// The gosec G304 warning can be ignored because all values come from the test
_, err := os.Create(tempEmptyFile)
require.NoError(t, err)
// generate test rootCA
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
@ -98,80 +88,11 @@ func TestNewSecureSocksProxy(t *testing.T) {
})
require.NoError(t, err)
settings := &setting.SecureSocksDSProxySettings{
return &setting.SecureSocksDSProxySettings{
ClientCert: clientCert,
ClientKey: clientKey,
RootCA: rootCACert,
ServerName: serverName,
ProxyAddress: proxyAddress,
}
t.Run("New socks proxy should be properly configured when all settings are valid", func(t *testing.T) {
require.NoError(t, newSecureSocksProxy(settings, &http.Transport{}))
})
t.Run("Client cert must be valid", func(t *testing.T) {
settings.ClientCert = tempEmptyFile
t.Cleanup(func() {
settings.ClientCert = clientCert
})
require.Error(t, newSecureSocksProxy(settings, &http.Transport{}))
})
t.Run("Client key must be valid", func(t *testing.T) {
settings.ClientKey = tempEmptyFile
t.Cleanup(func() {
settings.ClientKey = clientKey
})
require.Error(t, newSecureSocksProxy(settings, &http.Transport{}))
})
t.Run("Root CA must be valid", func(t *testing.T) {
settings.RootCA = tempEmptyFile
t.Cleanup(func() {
settings.RootCA = rootCACert
})
require.Error(t, newSecureSocksProxy(settings, &http.Transport{}))
})
}
func TestSecureSocksProxyEnabledOnDS(t *testing.T) {
t.Run("Secure socks proxy should only be enabled when the json data contains enableSecureSocksProxy=true", func(t *testing.T) {
tests := []struct {
instanceSettings *backend.AppInstanceSettings
enabled bool
}{
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{}"),
},
enabled: false,
},
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{ \"enableSecureSocksProxy\": \"nonbool\" }"),
},
enabled: false,
},
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{ \"enableSecureSocksProxy\": false }"),
},
enabled: false,
},
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{ \"enableSecureSocksProxy\": true }"),
},
enabled: true,
},
}
for _, tt := range tests {
opts, err := tt.instanceSettings.HTTPClientOptions()
assert.NoError(t, err)
assert.Equal(t, tt.enabled, secureSocksProxyEnabledOnDS(opts))
}
})
}

View File

@ -1,4 +1,4 @@
package httpclientprovider
package proxy
import (
"crypto/tls"
@ -14,24 +14,40 @@ import (
"golang.org/x/net/proxy"
)
// newSecureSocksProxy takes a http.DefaultTransport and wraps it in a socks5 proxy with TLS
func newSecureSocksProxy(cfg *setting.SecureSocksDSProxySettings, transport *http.Transport) error {
// NewSecureSocksHTTPProxy takes a http.DefaultTransport and wraps it in a socks5 proxy with TLS
func NewSecureSocksHTTPProxy(cfg *setting.SecureSocksDSProxySettings, transport *http.Transport) error {
dialSocksProxy, err := NewSecureSocksProxyContextDialer(cfg)
if err != nil {
return err
}
contextDialer, ok := dialSocksProxy.(proxy.ContextDialer)
if !ok {
return errors.New("unable to cast socks proxy dialer to context proxy dialer")
}
transport.DialContext = contextDialer.DialContext
return nil
}
// NewSecureSocksProxyContextDialer returns a proxy context dialer that will wrap connections in a secure socks proxy
func NewSecureSocksProxyContextDialer(cfg *setting.SecureSocksDSProxySettings) (proxy.Dialer, error) {
certPool := x509.NewCertPool()
for _, rootCAFile := range strings.Split(cfg.RootCA, " ") {
// nolint:gosec
// The gosec G304 warning can be ignored because `rootCAFile` comes from config ini.
pem, err := os.ReadFile(rootCAFile)
if err != nil {
return err
return nil, err
}
if !certPool.AppendCertsFromPEM(pem) {
return errors.New("failed to append CA certificate " + rootCAFile)
return nil, errors.New("failed to append CA certificate " + rootCAFile)
}
}
cert, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey)
if err != nil {
return err
return nil, err
}
tlsDialer := &tls.Dialer{
@ -43,21 +59,14 @@ func newSecureSocksProxy(cfg *setting.SecureSocksDSProxySettings, transport *htt
}
dialSocksProxy, err := proxy.SOCKS5("tcp", cfg.ProxyAddress, nil, tlsDialer)
if err != nil {
return err
return nil, err
}
contextDialer, ok := dialSocksProxy.(proxy.ContextDialer)
if !ok {
return errors.New("unable to cast socks proxy dialer to context proxy dialer")
}
transport.DialContext = contextDialer.DialContext
return nil
return dialSocksProxy, nil
}
// secureSocksProxyEnabledOnDS checks the datasource json data to see if the secure socks proxy is enabled on it
func secureSocksProxyEnabledOnDS(opts sdkhttpclient.Options) bool {
// SecureSocksProxyEnabledOnDS checks the datasource json data to see if the secure socks proxy is enabled on it
func SecureSocksProxyEnabledOnDS(opts sdkhttpclient.Options) bool {
jsonData := backend.JSONDataFromHTTPClientOptions(opts)
res, enabled := jsonData["enableSecureSocksProxy"]
if !enabled {

View File

@ -0,0 +1,97 @@
package proxy
import (
"net/http"
"os"
"path/filepath"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewSecureSocksProxy(t *testing.T) {
settings := proxyutil.SetupTestSecureSocksProxySettings(t)
// create empty file for testing invalid configs
tempDir := t.TempDir()
tempEmptyFile := filepath.Join(tempDir, "emptyfile.txt")
// nolint:gosec
// The gosec G304 warning can be ignored because all values come from the test
_, err := os.Create(tempEmptyFile)
require.NoError(t, err)
t.Run("New socks proxy should be properly configured when all settings are valid", func(t *testing.T) {
require.NoError(t, NewSecureSocksHTTPProxy(settings, &http.Transport{}))
})
t.Run("Client cert must be valid", func(t *testing.T) {
clientCertBefore := settings.ClientCert
settings.ClientCert = tempEmptyFile
t.Cleanup(func() {
settings.ClientCert = clientCertBefore
})
require.Error(t, NewSecureSocksHTTPProxy(settings, &http.Transport{}))
})
t.Run("Client key must be valid", func(t *testing.T) {
clientKeyBefore := settings.ClientKey
settings.ClientKey = tempEmptyFile
t.Cleanup(func() {
settings.ClientKey = clientKeyBefore
})
require.Error(t, NewSecureSocksHTTPProxy(settings, &http.Transport{}))
})
t.Run("Root CA must be valid", func(t *testing.T) {
rootCABefore := settings.RootCA
settings.RootCA = tempEmptyFile
t.Cleanup(func() {
settings.RootCA = rootCABefore
})
require.Error(t, NewSecureSocksHTTPProxy(settings, &http.Transport{}))
})
}
func TestSecureSocksProxyEnabledOnDS(t *testing.T) {
t.Run("Secure socks proxy should only be enabled when the json data contains enableSecureSocksProxy=true", func(t *testing.T) {
tests := []struct {
instanceSettings *backend.AppInstanceSettings
enabled bool
}{
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{}"),
},
enabled: false,
},
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{ \"enableSecureSocksProxy\": \"nonbool\" }"),
},
enabled: false,
},
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{ \"enableSecureSocksProxy\": false }"),
},
enabled: false,
},
{
instanceSettings: &backend.AppInstanceSettings{
JSONData: []byte("{ \"enableSecureSocksProxy\": true }"),
},
enabled: true,
},
}
for _, tt := range tests {
opts, err := tt.instanceSettings.HTTPClientOptions()
assert.NoError(t, err)
assert.Equal(t, tt.enabled, SecureSocksProxyEnabledOnDS(opts))
}
})
}

View File

@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
"github.com/grafana/grafana/pkg/util"
@ -60,6 +61,7 @@ func newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
ConnMaxLifetime: 14400,
Encrypt: "false",
ConnectionTimeout: 0,
SecureDSProxy: false,
}
err := json.Unmarshal(settings.JSONData, &jsonData)
@ -90,8 +92,18 @@ func newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
if cfg.Env == setting.Dev {
logger.Debug("GetEngine", "connection", cnnstr)
}
driverName := "mssql"
// register a new proxy driver if the secure socks proxy is enabled
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagSecureSocksDatasourceProxy) && cfg.SecureSocksDSProxy.Enabled && jsonData.SecureDSProxy {
driverName, err = createMSSQLProxyDriver(&cfg.SecureSocksDSProxy, cnnstr)
if err != nil {
return nil, err
}
}
config := sqleng.DataPluginConfiguration{
DriverName: "mssql",
DriverName: driverName,
ConnectionString: cnnstr,
DSInfo: dsInfo,
MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"},

91
pkg/tsdb/mssql/proxy.go Normal file
View File

@ -0,0 +1,91 @@
package mssql
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
mssql "github.com/denisenkom/go-mssqldb"
iproxy "github.com/grafana/grafana/pkg/infra/proxy"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
"github.com/grafana/grafana/pkg/util"
"golang.org/x/net/proxy"
"xorm.io/core"
)
// createMSSQLProxyDriver creates and registers a new sql driver that uses a mssql connector and updates the dialer to
// route connections through the secure socks proxy
func createMSSQLProxyDriver(settings *setting.SecureSocksDSProxySettings, cnnstr string) (string, error) {
sqleng.XormDriverMu.Lock()
defer sqleng.XormDriverMu.Unlock()
// create a unique driver per connection string
hash, err := util.Md5SumString(cnnstr)
if err != nil {
return "", err
}
driverName := "mssql-proxy-" + hash
// only register the driver once
if core.QueryDriver(driverName) == nil {
connector, err := mssql.NewConnector(cnnstr)
if err != nil {
return "", err
}
driver, err := newMSSQLProxyDriver(settings, connector)
if err != nil {
return "", err
}
sql.Register(driverName, driver)
core.RegisterDriver(driverName, driver)
}
return driverName, nil
}
// mssqlProxyDriver is a regular mssql driver with an updated dialer.
// This is needed because there is no way to save a dialer to the mssql driver in xorm
type mssqlProxyDriver struct {
c *mssql.Connector
}
var _ driver.DriverContext = (*mssqlProxyDriver)(nil)
var _ core.Driver = (*mssqlProxyDriver)(nil)
// newMSSQLProxyDriver updates the dialer for a mssql connector with a dialer that proxys connections through the secure socks proxy
// and returns a new mssql driver to register
func newMSSQLProxyDriver(cfg *setting.SecureSocksDSProxySettings, connector *mssql.Connector) (*mssqlProxyDriver, error) {
dialer, err := iproxy.NewSecureSocksProxyContextDialer(cfg)
if err != nil {
return nil, err
}
contextDialer, ok := dialer.(proxy.ContextDialer)
if !ok {
return nil, errors.New("unable to cast socks proxy dialer to context proxy dialer")
}
connector.Dialer = contextDialer
return &mssqlProxyDriver{c: connector}, nil
}
// Parse uses the xorm mssql dialect for the driver (this has to be implemented to register the driver with xorm)
func (d *mssqlProxyDriver) Parse(a string, b string) (*core.Uri, error) {
sqleng.XormDriverMu.RLock()
defer sqleng.XormDriverMu.RUnlock()
return core.QueryDriver("mssql").Parse(a, b)
}
// OpenConnector returns the normal mssql connector that has the updated dialer context
func (d *mssqlProxyDriver) OpenConnector(name string) (driver.Connector, error) {
return d.c, nil
}
// Open uses the connector with the updated dialer context to open a new connection
func (d *mssqlProxyDriver) Open(dsn string) (driver.Conn, error) {
return d.c.Connect(context.Background())
}

View File

@ -0,0 +1,66 @@
package mssql
import (
"context"
"fmt"
"testing"
mssql "github.com/denisenkom/go-mssqldb"
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil"
"github.com/stretchr/testify/require"
"xorm.io/core"
)
func TestMSSQLProxyDriver(t *testing.T) {
settings := proxyutil.SetupTestSecureSocksProxySettings(t)
dialect := "mssql"
cnnstr := "server=127.0.0.1;port=1433;user id=sa;password=yourStrong(!)Password;database=db"
driverName, err := createMSSQLProxyDriver(settings, cnnstr)
require.NoError(t, err)
t.Run("Driver should not be registered more than once", func(t *testing.T) {
testDriver, err := createMSSQLProxyDriver(settings, cnnstr)
require.NoError(t, err)
require.Equal(t, driverName, testDriver)
})
t.Run("A new driver should be created for a new connection string", func(t *testing.T) {
testDriver, err := createMSSQLProxyDriver(settings, "server=localhost;user id=sa;password=yourStrong(!)Password;database=db2")
require.NoError(t, err)
require.NotEqual(t, driverName, testDriver)
})
t.Run("Parse should have the same result as xorm mssql parse", func(t *testing.T) {
xormDriver := core.QueryDriver(dialect)
xormResult, err := xormDriver.Parse(dialect, cnnstr)
require.NoError(t, err)
xormNewDriver := core.QueryDriver(driverName)
xormNewResult, err := xormNewDriver.Parse(dialect, cnnstr)
require.NoError(t, err)
require.Equal(t, xormResult, xormNewResult)
})
t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) {
connector, err := mssql.NewConnector(cnnstr)
require.NoError(t, err)
driver, err := newMSSQLProxyDriver(settings, connector)
require.NoError(t, err)
conn, err := driver.OpenConnector(cnnstr)
require.NoError(t, err)
_, err = conn.Connect(context.Background())
require.Contains(t, err.Error(), fmt.Sprintf("socks connect tcp %s->127.0.0.1:1433", settings.ProxyAddress))
})
t.Run("Open should use the connector that routes through the socks proxy to db", func(t *testing.T) {
connector, err := mssql.NewConnector(cnnstr)
require.NoError(t, err)
driver, err := newMSSQLProxyDriver(settings, connector)
require.NoError(t, err)
_, err = driver.Open(cnnstr)
require.Contains(t, err.Error(), fmt.Sprintf("socks connect tcp %s->127.0.0.1:1433", settings.ProxyAddress))
})
}

View File

@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
)
@ -54,6 +55,7 @@ func newInstanceSettings(cfg *setting.Cfg, httpClientProvider httpclient.Provide
MaxOpenConns: 0,
MaxIdleConns: 2,
ConnMaxLifetime: 14400,
SecureDSProxy: false,
}
err := json.Unmarshal(settings.JSONData, &jsonData)
@ -82,6 +84,16 @@ func newInstanceSettings(cfg *setting.Cfg, httpClientProvider httpclient.Provide
protocol = "unix"
}
// register the secure socks proxy dialer context, if enabled
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagSecureSocksDatasourceProxy) && cfg.SecureSocksDSProxy.Enabled && jsonData.SecureDSProxy {
// UID is only unique per org, the only way to ensure uniqueness is to do it by connection information
uniqueIdentifier := dsInfo.User + dsInfo.DecryptedSecureJSONData["password"] + dsInfo.URL + dsInfo.Database
protocol, err = registerProxyDialerContext(&cfg.SecureSocksDSProxy, protocol, uniqueIdentifier)
if err != nil {
return nil, err
}
}
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
characterEscape(dsInfo.User, ":"),
dsInfo.DecryptedSecureJSONData["password"],

57
pkg/tsdb/mysql/proxy.go Normal file
View File

@ -0,0 +1,57 @@
package mysql
import (
"context"
"net"
"github.com/go-sql-driver/mysql"
iproxy "github.com/grafana/grafana/pkg/infra/proxy"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"golang.org/x/net/proxy"
)
// registerProxyDialerContext registers a new dialer context to be used by mysql when the proxy network is
// specified in the connection string
func registerProxyDialerContext(settings *setting.SecureSocksDSProxySettings, protocol, cnnstr string) (string, error) {
// the dialer contains the true network used behind the scenes
dialer, err := getProxyDialerContext(settings, protocol)
if err != nil {
return "", err
}
// the dialer context can be updated everytime the datasource is updated
// have a unique network per connection string
hash, err := util.Md5SumString(cnnstr)
if err != nil {
return "", err
}
network := "proxy-" + hash
mysql.RegisterDialContext(network, dialer.DialContext)
return network, nil
}
// mySQLContextDialer turns a golang proxy driver into a MySQL proxy driver
type mySQLContextDialer struct {
dialer proxy.ContextDialer
network string
}
// getProxyDialerContext returns a context dialer that will send the request through to the secure socks proxy
func getProxyDialerContext(cfg *setting.SecureSocksDSProxySettings, actualNetwork string) (*mySQLContextDialer, error) {
dialer, err := iproxy.NewSecureSocksProxyContextDialer(cfg)
if err != nil {
return nil, err
}
contextDialer, ok := dialer.(proxy.ContextDialer)
if !ok {
return nil, err
}
return &mySQLContextDialer{dialer: contextDialer, network: actualNetwork}, nil
}
// DialContext implements the MySQL requirements for a proxy driver, and uses the underlying golang proxy driver with the assigned network
func (d *mySQLContextDialer) DialContext(ctx context.Context, addr string) (net.Conn, error) {
return d.dialer.DialContext(ctx, d.network, addr)
}

View File

@ -0,0 +1,51 @@
package mysql
import (
"context"
"fmt"
"testing"
"github.com/go-sql-driver/mysql"
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil"
"github.com/stretchr/testify/require"
)
func TestMySQLProxyDialer(t *testing.T) {
settings := proxyutil.SetupTestSecureSocksProxySettings(t)
protocol := "tcp"
network, err := registerProxyDialerContext(settings, protocol, "1")
require.NoError(t, err)
driver := mysql.MySQLDriver{}
dbURL := "localhost:5432"
cnnstr := fmt.Sprintf("test:test@%s(%s)/db",
network,
dbURL,
)
t.Run("Network is available", func(t *testing.T) {
_, err = driver.OpenConnector(cnnstr)
require.NoError(t, err)
})
t.Run("Multiple networks can be created", func(t *testing.T) {
network, err := registerProxyDialerContext(settings, protocol, "2")
require.NoError(t, err)
cnnstr2 := fmt.Sprintf("test:test@%s(%s)/db",
network,
dbURL,
)
// both networks should exist
_, err = driver.OpenConnector(cnnstr)
require.NoError(t, err)
_, err = driver.OpenConnector(cnnstr2)
require.NoError(t, err)
})
t.Run("Connection should be routed through socks proxy to db", func(t *testing.T) {
conn, err := driver.OpenConnector(cnnstr)
require.NoError(t, err)
_, err = conn.Connect(context.Background())
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("socks connect %s %s->%s", protocol, settings.ProxyAddress, dbURL))
})
}

View File

@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
)
@ -60,6 +61,7 @@ func (s *Service) newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFacto
ConnMaxLifetime: 14400,
Timescaledb: false,
ConfigurationMethod: "file-path",
SecureDSProxy: false,
}
err := json.Unmarshal(settings.JSONData, &jsonData)
@ -92,8 +94,17 @@ func (s *Service) newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFacto
logger.Debug("GetEngine", "connection", cnnstr)
}
driverName := "postgres"
// register a proxy driver if the secure socks proxy is enabled
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagSecureSocksDatasourceProxy) && cfg.SecureSocksDSProxy.Enabled && jsonData.SecureDSProxy {
driverName, err = createPostgresProxyDriver(&cfg.SecureSocksDSProxy, cnnstr)
if err != nil {
return "", nil
}
}
config := sqleng.DataPluginConfiguration{
DriverName: "postgres",
DriverName: driverName,
ConnectionString: cnnstr,
DSInfo: dsInfo,
MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"},

107
pkg/tsdb/postgres/proxy.go Normal file
View File

@ -0,0 +1,107 @@
package postgres
import (
"context"
"database/sql"
"database/sql/driver"
"net"
"time"
iproxy "github.com/grafana/grafana/pkg/infra/proxy"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
"github.com/grafana/grafana/pkg/util"
"github.com/lib/pq"
"golang.org/x/net/proxy"
"xorm.io/core"
)
// createPostgresProxyDriver creates and registers a new sql driver that uses a postgres connector and updates the dialer to
// route connections through the secure socks proxy
func createPostgresProxyDriver(settings *setting.SecureSocksDSProxySettings, cnnstr string) (string, error) {
sqleng.XormDriverMu.Lock()
defer sqleng.XormDriverMu.Unlock()
// create a unique driver per connection string
hash, err := util.Md5SumString(cnnstr)
if err != nil {
return "", err
}
driverName := "postgres-proxy-" + hash
// only register the driver once
if core.QueryDriver(driverName) == nil {
connector, err := pq.NewConnector(cnnstr)
if err != nil {
return "", err
}
driver, err := newPostgresProxyDriver(settings, connector)
if err != nil {
return "", err
}
sql.Register(driverName, driver)
core.RegisterDriver(driverName, driver)
}
return driverName, nil
}
// postgresProxyDriver is a regular postgres driver with an updated dialer.
// This is done because there is no way to save a dialer to the postgres driver in xorm
type postgresProxyDriver struct {
c *pq.Connector
}
var _ driver.DriverContext = (*postgresProxyDriver)(nil)
var _ core.Driver = (*postgresProxyDriver)(nil)
// newPostgresProxyDriver updates the dialer for a postgres connector with a dialer that proxys connections through the secure socks proxy
// and returns a new postgres driver to register
func newPostgresProxyDriver(cfg *setting.SecureSocksDSProxySettings, connector *pq.Connector) (*postgresProxyDriver, error) {
dialer, err := iproxy.NewSecureSocksProxyContextDialer(cfg)
if err != nil {
return nil, err
}
// update the postgres dialer with the proxy dialer
connector.Dialer(&postgresProxyDialer{d: dialer})
return &postgresProxyDriver{connector}, nil
}
// postgresProxyDialer implements the postgres dialer using a proxy dialer, as their functions differ slightly
type postgresProxyDialer struct {
d proxy.Dialer
}
// Dial uses the normal proxy dial function with the updated dialer
func (p *postgresProxyDialer) Dial(network, addr string) (c net.Conn, err error) {
return p.d.Dial(network, addr)
}
// DialTimeout uses the normal postgres dial timeout function with the updated dialer
func (p *postgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return p.d.(proxy.ContextDialer).DialContext(ctx, network, address)
}
// Parse uses the xorm postgres dialect for the driver (this has to be implemented to register the driver with xorm)
func (d *postgresProxyDriver) Parse(a string, b string) (*core.Uri, error) {
sqleng.XormDriverMu.RLock()
defer sqleng.XormDriverMu.RUnlock()
return core.QueryDriver("postgres").Parse(a, b)
}
// OpenConnector returns the normal postgres connector that has the updated dialer context
func (d *postgresProxyDriver) OpenConnector(name string) (driver.Connector, error) {
return d.c, nil
}
// Open uses the connector with the updated dialer to open a new connection
func (d *postgresProxyDriver) Open(dsn string) (driver.Conn, error) {
return d.c.Connect(context.Background())
}

View File

@ -0,0 +1,70 @@
package postgres
import (
"context"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
"xorm.io/core"
)
func TestPostgresProxyDriver(t *testing.T) {
dialect := "postgres"
settings := proxyutil.SetupTestSecureSocksProxySettings(t)
dbURL := "localhost:5432"
cnnstr := fmt.Sprintf("postgres://auser:password@%s/db?sslmode=disable", dbURL)
driverName, err := createPostgresProxyDriver(settings, cnnstr)
require.NoError(t, err)
t.Run("Driver should not be registered more than once", func(t *testing.T) {
testDriver, err := createPostgresProxyDriver(settings, cnnstr)
require.NoError(t, err)
require.Equal(t, driverName, testDriver)
})
t.Run("A new driver should be created for a new connection string", func(t *testing.T) {
testDriver, err := createPostgresProxyDriver(settings, "server=localhost;user id=sa;password=yourStrong(!)Password;database=db2")
require.NoError(t, err)
require.NotEqual(t, driverName, testDriver)
})
t.Run("Parse should have the same result as xorm mssql parse", func(t *testing.T) {
xormDriver := core.QueryDriver(dialect)
xormResult, err := xormDriver.Parse(dialect, cnnstr)
require.NoError(t, err)
xormNewDriver := core.QueryDriver(driverName)
xormNewResult, err := xormNewDriver.Parse(dialect, cnnstr)
require.NoError(t, err)
require.Equal(t, xormResult, xormNewResult)
})
t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) {
connector, err := pq.NewConnector(cnnstr)
require.NoError(t, err)
driver, err := newPostgresProxyDriver(settings, connector)
require.NoError(t, err)
conn, err := driver.OpenConnector(cnnstr)
require.NoError(t, err)
_, err = conn.Connect(context.Background())
require.Contains(t, err.Error(), fmt.Sprintf("socks connect %s %s->%s", "tcp", settings.ProxyAddress, dbURL))
})
t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) {
connector, err := pq.NewConnector(cnnstr)
require.NoError(t, err)
driver, err := newPostgresProxyDriver(settings, connector)
require.NoError(t, err)
conn, err := driver.OpenConnector(cnnstr)
require.NoError(t, err)
_, err = conn.Connect(context.Background())
require.Contains(t, err.Error(), fmt.Sprintf("socks connect %s %s->%s", "tcp", settings.ProxyAddress, dbURL))
})
}

View File

@ -23,6 +23,9 @@ import (
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
)
// XormDriverMu is used to allow safe concurrent registering and querying of drivers in xorm
var XormDriverMu sync.RWMutex
// MetaKeyExecutedQueryString is the key where the executed query should get stored
const MetaKeyExecutedQueryString = "executedQueryString"
@ -67,6 +70,7 @@ type JsonData struct {
Servername string `json:"servername"`
TimeInterval string `json:"timeInterval"`
Database string `json:"database"`
SecureDSProxy bool `json:"enableSecureSocksProxy"`
}
type DataSourceInfo struct {

View File

@ -21,8 +21,10 @@ import {
SecretInput,
Select,
useStyles2,
SecureSocksProxySettings,
} from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { config } from 'app/core/config';
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits';
import { useMigrateDatabaseField } from 'app/features/plugins/sql/components/configuration/useMigrateDatabaseField';
@ -154,6 +156,10 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
)}
</FieldSet>
{config.featureToggles.secureSocksDatasourceProxy && (
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
)}
<FieldSet label="TLS/SSL Auth">
<InlineField
labelWidth={labelWidthSSL}

View File

@ -7,7 +7,18 @@ import {
updateDatasourcePluginJsonDataOption,
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { Alert, FieldSet, InlineField, InlineFieldRow, InlineSwitch, Input, Link, SecretInput } from '@grafana/ui';
import {
Alert,
FieldSet,
InlineField,
InlineFieldRow,
InlineSwitch,
Input,
Link,
SecretInput,
SecureSocksProxySettings,
} from '@grafana/ui';
import { config } from 'app/core/config';
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits';
import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig';
import { useMigrateDatabaseField } from 'app/features/plugins/sql/components/configuration/useMigrateDatabaseField';
@ -141,6 +152,9 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<My
</InlineField>
</FieldSet>
{config.featureToggles.secureSocksDatasourceProxy && (
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
)}
{jsonData.tlsAuth || jsonData.tlsAuthWithCACert ? (
<FieldSet label="TLS/SSL Auth Details">
<TLSSecretsConfig

View File

@ -19,6 +19,7 @@ import {
SecretInput,
Link,
} from '@grafana/ui';
import { config } from 'app/core/config';
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits';
import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig';
import { useMigrateDatabaseField } from 'app/features/plugins/sql/components/configuration/useMigrateDatabaseField';
@ -166,6 +167,21 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P
) : null}
</FieldSet>
{config.featureToggles.secureSocksDatasourceProxy && (
<FieldSet label="Secure Socks Proxy">
<InlineField labelWidth={26} label="Enabled" tooltip="Connect to this datasource via the secure socks proxy.">
<InlineSwitch
value={options.jsonData.enableSecureSocksProxy ?? false}
onChange={(event) =>
onOptionsChange({
...options,
jsonData: { ...options.jsonData, enableSecureSocksProxy: event!.currentTarget.checked },
})
}
/>
</InlineField>
</FieldSet>
)}
{jsonData.sslmode !== PostgresTLSModes.disable ? (
<FieldSet label="TLS/SSL Auth Details">
{jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? (

View File

@ -19,6 +19,7 @@ export interface PostgresOptions extends SQLOptions {
sslKeyFile?: string;
postgresVersion?: number;
timescaledb?: boolean;
enableSecureSocksProxy?: boolean;
}
export interface SecureJsonData {