MSSQL: Add support for MI authentication to MSSQL (#73597)
* Add support for MI authentication to MSSQL This adds support for managed identity authentication for MSSQL managed instances running in Azure. Co-authored-by: baldm0mma <jev.forsberg@grafana.com>
This commit is contained in:
co-authored by
baldm0mma
parent
3ccfa5620f
commit
579709c7a6
+63
-10
@@ -10,16 +10,19 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
mssql "github.com/grafana/go-mssqldb"
|
||||
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
sdkproxy "github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
|
||||
mssql "github.com/microsoft/go-mssqldb"
|
||||
_ "github.com/microsoft/go-mssqldb/azuread"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/mssql/utils"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng/proxyutil"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -31,6 +34,12 @@ type Service struct {
|
||||
im instancemgmt.InstanceManager
|
||||
}
|
||||
|
||||
const (
|
||||
azureAuthentication = "Azure AD Authentication"
|
||||
windowsAuthentication = "Windows Authentication"
|
||||
sqlServerAuthentication = "SQL Server Authentication"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) *Service {
|
||||
return &Service{
|
||||
im: datasource.NewInstanceManager(newInstanceSettings(cfg)),
|
||||
@@ -64,8 +73,11 @@ func newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
ConnectionTimeout: 0,
|
||||
SecureDSProxy: false,
|
||||
}
|
||||
|
||||
err := json.Unmarshal(settings.JSONData, &jsonData)
|
||||
azureCredentials, err := utils.GetAzureCredentials(settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading azure credentials")
|
||||
}
|
||||
err = json.Unmarshal(settings.JSONData, &jsonData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading settings: %w", err)
|
||||
}
|
||||
@@ -85,7 +97,7 @@ func newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
UID: settings.UID,
|
||||
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||
}
|
||||
cnnstr, err := generateConnectionString(dsInfo)
|
||||
cnnstr, err := generateConnectionString(dsInfo, cfg, azureCredentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -93,12 +105,19 @@ func newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
if cfg.Env == setting.Dev {
|
||||
logger.Debug("GetEngine", "connection", cnnstr)
|
||||
}
|
||||
|
||||
driverName := "mssql"
|
||||
if jsonData.AuthenticationType == azureAuthentication {
|
||||
driverName = "azuresql"
|
||||
}
|
||||
|
||||
// register a new proxy driver if the secure socks proxy is enabled
|
||||
proxyOpts := proxyutil.GetSQLProxyOptions(dsInfo)
|
||||
if sdkproxy.Cli.SecureSocksProxyEnabled(proxyOpts) {
|
||||
driverName, err = createMSSQLProxyDriver(cnnstr, proxyOpts)
|
||||
URL, err := ParseURL(dsInfo.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driverName, err = createMSSQLProxyDriver(cnnstr, URL.Hostname(), proxyOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,7 +160,7 @@ func ParseURL(u string) (*url.URL, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) {
|
||||
func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, azureCredentials azcredentials.AzureCredentials) (string, error) {
|
||||
const dfltPort = "0"
|
||||
var addr util.NetworkAddress
|
||||
if dsInfo.URL != "" {
|
||||
@@ -172,12 +191,24 @@ func generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) {
|
||||
tlsSkipVerify := dsInfo.JsonData.TlsSkipVerify
|
||||
hostNameInCertificate := dsInfo.JsonData.Servername
|
||||
certificate := dsInfo.JsonData.RootCertFile
|
||||
connStr := fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;",
|
||||
connStr := fmt.Sprintf("server=%s;database=%s;",
|
||||
addr.Host,
|
||||
dsInfo.Database,
|
||||
dsInfo.User,
|
||||
dsInfo.DecryptedSecureJSONData["password"],
|
||||
)
|
||||
|
||||
switch dsInfo.JsonData.AuthenticationType {
|
||||
case azureAuthentication:
|
||||
azureCredentialDSNFragment, err := getAzureCredentialDSNFragment(azureCredentials, cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
connStr += azureCredentialDSNFragment
|
||||
case windowsAuthentication:
|
||||
// No user id or password. We're using windows single sign on.
|
||||
default:
|
||||
connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"])
|
||||
}
|
||||
|
||||
// Port number 0 means to determine the port automatically, so we can let the driver choose
|
||||
if addr.Port != "0" {
|
||||
connStr += fmt.Sprintf("port=%s;", addr.Port)
|
||||
@@ -202,6 +233,28 @@ func generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) {
|
||||
return connStr, nil
|
||||
}
|
||||
|
||||
func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, cfg *setting.Cfg) (string, error) {
|
||||
connStr := ""
|
||||
switch c := azureCredentials.(type) {
|
||||
case *azcredentials.AzureManagedIdentityCredentials:
|
||||
if cfg.Azure.ManagedIdentityClientId != "" {
|
||||
connStr += fmt.Sprintf("user id=%s;", cfg.Azure.ManagedIdentityClientId)
|
||||
}
|
||||
connStr += fmt.Sprintf("fedauth=%s;",
|
||||
"ActiveDirectoryManagedIdentity")
|
||||
case *azcredentials.AzureClientSecretCredentials:
|
||||
connStr += fmt.Sprintf("user id=%s@%s;password=%s;fedauth=%s;",
|
||||
c.ClientId,
|
||||
c.TenantId,
|
||||
c.ClientSecret,
|
||||
"ActiveDirectoryApplication",
|
||||
)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported azure authentication type")
|
||||
}
|
||||
return connStr, nil
|
||||
}
|
||||
|
||||
type mssqlQueryResultTransformer struct {
|
||||
userError string
|
||||
}
|
||||
|
||||
@@ -1460,7 +1460,7 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
connStr, err := generateConnectionString(tc.dataSource)
|
||||
connStr, err := generateConnectionString(tc.dataSource, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expConnStr, connStr)
|
||||
})
|
||||
|
||||
+19
-5
@@ -5,18 +5,19 @@ import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
mssql "github.com/grafana/go-mssqldb"
|
||||
sdkproxy "github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
mssql "github.com/microsoft/go-mssqldb"
|
||||
"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(cnnstr string, opts *sdkproxy.Options) (string, error) {
|
||||
func createMSSQLProxyDriver(cnnstr string, hostName string, opts *sdkproxy.Options) (string, error) {
|
||||
sqleng.XormDriverMu.Lock()
|
||||
defer sqleng.XormDriverMu.Unlock()
|
||||
|
||||
@@ -34,7 +35,7 @@ func createMSSQLProxyDriver(cnnstr string, opts *sdkproxy.Options) (string, erro
|
||||
return "", err
|
||||
}
|
||||
|
||||
driver, err := newMSSQLProxyDriver(connector, opts)
|
||||
driver, err := newMSSQLProxyDriver(connector, hostName, opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -45,6 +46,19 @@ func createMSSQLProxyDriver(cnnstr string, opts *sdkproxy.Options) (string, erro
|
||||
return driverName, nil
|
||||
}
|
||||
|
||||
type HostTransportDialer struct {
|
||||
Dialer proxy.ContextDialer
|
||||
Host string
|
||||
}
|
||||
|
||||
func (m HostTransportDialer) DialContext(ctx context.Context, network string, addr string) (conn net.Conn, err error) {
|
||||
return m.Dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
func (m HostTransportDialer) HostName() string {
|
||||
return m.Host
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -56,7 +70,7 @@ 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(connector *mssql.Connector, opts *sdkproxy.Options) (*mssqlProxyDriver, error) {
|
||||
func newMSSQLProxyDriver(connector *mssql.Connector, hostName string, opts *sdkproxy.Options) (*mssqlProxyDriver, error) {
|
||||
dialer, err := sdkproxy.Cli.NewSecureSocksProxyContextDialer(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -67,7 +81,7 @@ func newMSSQLProxyDriver(connector *mssql.Connector, opts *sdkproxy.Options) (*m
|
||||
return nil, errors.New("unable to cast socks proxy dialer to context proxy dialer")
|
||||
}
|
||||
|
||||
connector.Dialer = contextDialer
|
||||
connector.Dialer = HostTransportDialer{contextDialer, hostName}
|
||||
return &mssqlProxyDriver{c: connector}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
mssql "github.com/grafana/go-mssqldb"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng/proxyutil"
|
||||
mssql "github.com/microsoft/go-mssqldb"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/core"
|
||||
)
|
||||
@@ -17,17 +17,17 @@ func TestMSSQLProxyDriver(t *testing.T) {
|
||||
dialect := "mssql"
|
||||
opts := proxyutil.GetSQLProxyOptions(sqleng.DataSourceInfo{UID: "1", JsonData: sqleng.JsonData{SecureDSProxy: true}})
|
||||
cnnstr := "server=127.0.0.1;port=1433;user id=sa;password=yourStrong(!)Password;database=db"
|
||||
driverName, err := createMSSQLProxyDriver(cnnstr, opts)
|
||||
driverName, err := createMSSQLProxyDriver(cnnstr, "127.0.0.1", opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Driver should not be registered more than once", func(t *testing.T) {
|
||||
testDriver, err := createMSSQLProxyDriver(cnnstr, opts)
|
||||
testDriver, err := createMSSQLProxyDriver(cnnstr, "127.0.0.1", opts)
|
||||
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("server=localhost;user id=sa;password=yourStrong(!)Password;database=db2", opts)
|
||||
testDriver, err := createMSSQLProxyDriver("server=localhost;user id=sa;password=yourStrong(!)Password;database=db2", "localhost", opts)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, driverName, testDriver)
|
||||
})
|
||||
@@ -46,7 +46,7 @@ func TestMSSQLProxyDriver(t *testing.T) {
|
||||
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(connector, opts)
|
||||
driver, err := newMSSQLProxyDriver(connector, "127.0.0.1", opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := driver.OpenConnector(cnnstr)
|
||||
@@ -59,7 +59,7 @@ func TestMSSQLProxyDriver(t *testing.T) {
|
||||
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(connector, opts)
|
||||
driver, err := newMSSQLProxyDriver(connector, "127.0.0.1", opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = driver.Open(cnnstr)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
)
|
||||
|
||||
// GetJsonData just gets the json in easier to work with type. It's used on multiple places which isn't super effective
|
||||
// but only when creating a client which should not happen often anyway.
|
||||
func getJsonData(settings backend.DataSourceInstanceSettings) (map[string]interface{}, error) {
|
||||
var jsonData map[string]interface{}
|
||||
err := json.Unmarshal(settings.JSONData, &jsonData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling JSONData: %w", err)
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
func GetAzureCredentials(settings backend.DataSourceInstanceSettings) (azcredentials.AzureCredentials, error) {
|
||||
jsonData, err := getJsonData(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return azcredentials.FromDatasourceData(jsonData, settings.DecryptedSecureJSONData)
|
||||
}
|
||||
@@ -74,6 +74,7 @@ type JsonData struct {
|
||||
Database string `json:"database"`
|
||||
SecureDSProxy bool `json:"enableSecureSocksProxy"`
|
||||
AllowCleartextPasswords bool `json:"allowCleartextPasswords"`
|
||||
AuthenticationType string `json:"authenticationType"`
|
||||
}
|
||||
|
||||
type DataSourceInfo struct {
|
||||
|
||||
Reference in New Issue
Block a user