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:
Oscar Kilhed
2023-09-06 18:27:19 +02:00
committed by GitHub
co-authored by baldm0mma
parent 3ccfa5620f
commit 579709c7a6
16 changed files with 853 additions and 72 deletions
+63 -10
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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
}
+6 -6
View File
@@ -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)
+28
View File
@@ -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)
}
+1
View File
@@ -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 {