mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
Plugins: Add sql support for the secure socks proxy (#64630)
This commit is contained in:
parent
68e38aad6a
commit
10db808ea1
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
@ -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 {
|
97
pkg/infra/proxy/secure_socks_proxy_test.go
Normal file
97
pkg/infra/proxy/secure_socks_proxy_test.go
Normal 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))
|
||||
}
|
||||
})
|
||||
}
|
@ -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
91
pkg/tsdb/mssql/proxy.go
Normal 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())
|
||||
}
|
66
pkg/tsdb/mssql/proxy_test.go
Normal file
66
pkg/tsdb/mssql/proxy_test.go
Normal 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))
|
||||
})
|
||||
}
|
@ -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
57
pkg/tsdb/mysql/proxy.go
Normal 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)
|
||||
}
|
51
pkg/tsdb/mysql/proxy_test.go
Normal file
51
pkg/tsdb/mysql/proxy_test.go
Normal 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))
|
||||
})
|
||||
}
|
@ -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
107
pkg/tsdb/postgres/proxy.go
Normal 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())
|
||||
}
|
70
pkg/tsdb/postgres/proxy_test.go
Normal file
70
pkg/tsdb/postgres/proxy_test.go
Normal 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))
|
||||
})
|
||||
}
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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 ? (
|
||||
|
@ -19,6 +19,7 @@ export interface PostgresOptions extends SQLOptions {
|
||||
sslKeyFile?: string;
|
||||
postgresVersion?: number;
|
||||
timescaledb?: boolean;
|
||||
enableSecureSocksProxy?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureJsonData {
|
||||
|
Loading…
Reference in New Issue
Block a user