MSSQL: Add Windows AD/Kerberos auth (#84742)

* mssql: Add Kerberos/Windows AD auth

* need username for cache file

* account for no port in cc file

* add tests around constring

* remove un-needed port

* add docs

* remove comments

* move defer to same locale as where it begins

* fix linting and spelling

* fix gosec linter

* note lack of grafana cloud support
pull/84825/head
Adam Simpson 1 year ago committed by GitHub
parent 04c9f459ec
commit 311aa94fab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      docs/sources/datasources/mssql/_index.md
  2. 48
      docs/sources/getting-started/get-started-grafana-ms-sql-server.md
  3. 7
      go.mod
  4. 15
      go.sum
  5. 106
      pkg/tsdb/mssql/kerberos/kerberos.go
  6. 24
      pkg/tsdb/mssql/mssql.go
  7. 111
      pkg/tsdb/mssql/mssql_test.go
  8. 50
      public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx
  9. 203
      public/app/plugins/datasource/mssql/configuration/Kerberos.tsx
  10. 10
      public/app/plugins/datasource/mssql/types.ts

@ -52,7 +52,7 @@ To configure basic settings for the data source, complete the following steps:
| **Default** | Sets the data source that's pre-selected for new panels. |
| **Host** | Sets the IP address/hostname and optional port of your MS SQL instance. Default port is 0, the driver default. You can specify multiple connection properties, such as `ApplicationIntent`, by separating each property with a semicolon (`;`). |
| **Database** | Sets the name of your MS SQL database. |
| **Authentication** | Sets the authentication mode, either using SQL Server Authentication or Windows Authentication (single sign-on for Windows users). |
| **Authentication** | Sets the authentication mode, either using SQL Server authentication, Windows authentication (single sign-on for Windows users), Azure Active Directory authentication, or various forms of Windows Active Directory authentication. |
| **User** | Defines the database user's username. |
| **Password** | Defines the database user's password. |
| **Encrypt** | Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. Options include: `disable` - data sent between client and server is not encrypted; `false` - data sent between client and server is not encrypted beyond the login packet; `true` - data sent between client and server is encrypted. Default is `false`. |
@ -89,6 +89,18 @@ You can also override this setting in a dashboard panel under its data source op
The **Connection timeout** setting defines the maximum number of seconds to wait for a connection to the database before timing out. Default is 0 for no timeout.
### UDP Preference Limit
The **UDP Preference Limit** setting defines the maximum size packet that the Kerberos libraries will attempt to send over a UDP connection before retrying with TCP. Default is 1 which means always use TCP.
### DNS Lookup KDC
The **DNS Lookup KDC** setting controls whether to [lookup KDC in DNS](https://web.mit.edu/kerberos/krb5-latest/doc/admin/realm_config.html#mapping-hostnames-onto-kerberos-realms). Default is true.
### KRB5 config file path
The **KRB5 config file path** stores the location of the `krb5` config file. Default is `/etc/krb5.conf`
### Database user permissions
Grafana doesn't validate that a query is safe, and could include any SQL statement.

@ -28,18 +28,60 @@ If you are on a Windows host but want to use Grafana and MS SQL data source on a
#### Add the MS SQL data source
There are several ways to authenticate in MSSQL. Start by:
1. Click **Connections** in the left-side menu and filter by `mssql`.
1. Select the **Microsoft SQL Server** option.
1. Click **Create a Microsoft SQL Server data source** in the top right corner to open the configuration page.
1. Enter the information specified in the table below, then click **Save & test**.
1. Select the desired authentication method and fill in the right information as detailed below.
1. Click **Save & test**.
##### General configuration
| Name | Description |
| ---------- | --------------------------------------------------------------------------------------------------------------------- |
| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
| `Host` | The IP address/hostname and optional port of your MS SQL instance. If port is omitted, the default 1433 will be used. |
| `Database` | Name of your MS SQL database. |
| `User` | Database user's login/username. |
| `Password` | Database user's password. |
##### SQL Server Authentication
| Name | Description |
| ---------- | ------------------------------- |
| `User` | Database user's login/username. |
| `Password` | Database user's password. |
##### Windows Active Directory (Kerberos)
Below are the four possible ways to authenticate via Windows Active Directory/Kerberos.
{{< admonition type="note" >}}
Windows Active Directory (Kerberos) authentication is not supported in Grafana Cloud at the moment.
{{< /admonition >}}
| Method | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Username + password** | Enter the domain user and password |
| **Keytab file** | Specify the path to a valid keytab file to use that for authentication. |
| **Credential cache** | Log in on the host via `kinit` and pass the path to the credential cache. The cache path can be found by running `klist` on the host in question. |
| **Credential cache file** | This option allows multiple valid configurations to be present and matching is performed on host, database, and user. See the example JSON below this table. |
```json
[
{
"user": "grot@GF.LAB",
"database": "dbone",
"address": "mysql1.mydomain.com:3306",
"credentialCache": "/tmp/krb5cc_1000"
},
{
"user": "grot@GF.LAB",
"database": "dbtwo",
"address": "mysql2.gf.lab",
"credentialCache": "/tmp/krb5cc_1000"
}
]
```
For installations from the [grafana/grafana](https://github.com/grafana/grafana/tree/main) repository, `gdev-mssql` data source is available. Once you add this data source, you can use the `Datasource tests - MSSQL` dashboard with three panels showing metrics generated from a test database.

@ -489,10 +489,17 @@ require github.com/jackc/pgx/v5 v5.5.5 // @grafana/oss-big-tent
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
)

@ -2165,6 +2165,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -2291,6 +2293,7 @@ github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3ly
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
@ -2418,6 +2421,18 @@ github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=

@ -0,0 +1,106 @@
package kerberos
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
)
type KerberosLookup struct {
User string `json:"user"`
DBName string `json:"database"`
Address string `json:"address"`
CredentialCacheFilename string `json:"credentialCache"`
}
type KerberosAuth struct {
KeytabFilePath string
CredentialCache string
CredentialCacheLookupFile string
ConfigFilePath string
UDPConnectionLimit string
EnableDNSLookupKDC string
}
func GetKerberosSettings(settings backend.DataSourceInstanceSettings) (kerberosAuth KerberosAuth, err error) {
err = json.Unmarshal(settings.JSONData, &kerberosAuth)
return kerberosAuth, err
}
func Krb5ParseAuthCredentials(host string, port string, db string, user string, pass string, kerberosAuth KerberosAuth) string {
//params for driver conn str
//More details: https://github.com/microsoft/go-mssqldb#kerberos-active-directory-authentication-outside-windows
krb5CCLookupFile := kerberosAuth.CredentialCacheLookupFile
krb5CacheCredsFile := kerberosAuth.CredentialCache
// if there is a lookup file specified, use it to find the correct credential cache file and overwrite var
// getCredentialCacheFromLookup implementation taken from mysql kerberos solution - https://github.com/grafana/mysql/commit/b5e73c8d536150c054d310123643683d3b18f0da
if krb5CCLookupFile != "" {
krb5CacheCredsFile = getCredentialCacheFromLookup(krb5CCLookupFile, host, port, db, user)
if krb5CacheCredsFile == "" {
logger.Error("No valid credential cache file found in lookup.")
return ""
}
}
krb5DriverParams := fmt.Sprintf("authenticator=krb5;krb5-configfile=%s;", kerberosAuth.ConfigFilePath)
// There are 3 main connection types:
// - credentials cache
// - user, realm, keytab
// - realm, user, pass
if krb5CacheCredsFile != "" {
krb5DriverParams += fmt.Sprintf("server=%s;database=%s;krb5-credcachefile=%s;", host, db, krb5CacheCredsFile)
} else if kerberosAuth.KeytabFilePath != "" {
krb5DriverParams += fmt.Sprintf("server=%s;database=%s;user id=%s;krb5-keytabfile=%s;", host, db, user, kerberosAuth.KeytabFilePath)
} else if kerberosAuth.KeytabFilePath == "" {
krb5DriverParams += fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;", host, db, user, pass)
} else {
logger.Error("invalid kerberos configuration")
return ""
}
if kerberosAuth.UDPConnectionLimit != "" {
krb5DriverParams += "krb5-udppreferencelimit=" + kerberosAuth.UDPConnectionLimit + ";"
}
if kerberosAuth.EnableDNSLookupKDC != "" {
krb5DriverParams += "krb5-dnslookupkdc=" + kerberosAuth.EnableDNSLookupKDC + ";"
}
logger.Info(fmt.Sprintf("final krb connstr: %s", krb5DriverParams))
return krb5DriverParams
}
func getCredentialCacheFromLookup(lookupFile string, host string, port string, dbName string, user string) string {
logger.Info(fmt.Sprintf("reading credential cache lookup: %s", lookupFile))
content, err := os.ReadFile(filepath.Clean(lookupFile))
if err != nil {
logger.Error(fmt.Sprintf("error reading: %s, %v", lookupFile, err))
return ""
}
var lookups []KerberosLookup
err = json.Unmarshal(content, &lookups)
if err != nil {
logger.Error(fmt.Sprintf("error parsing: %s, %v", lookupFile, err))
return ""
}
// find cache file
for _, item := range lookups {
if port == "0" {
item.Address = host + ":0"
}
if item.Address == host+":"+port && item.DBName == dbName && item.User == user {
logger.Info(fmt.Sprintf("matched: %+v", item))
return item.CredentialCacheFilename
}
}
logger.Error(fmt.Sprintf("no match found for %s", host+":"+port))
return ""
}

@ -20,9 +20,11 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
mssql "github.com/microsoft/go-mssqldb"
_ "github.com/microsoft/go-mssqldb/azuread"
_ "github.com/microsoft/go-mssqldb/integratedauth/krb5"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/mssql/kerberos"
"github.com/grafana/grafana/pkg/tsdb/mssql/utils"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
"github.com/grafana/grafana/pkg/util"
@ -34,9 +36,13 @@ type Service struct {
}
const (
azureAuthentication = "Azure AD Authentication"
windowsAuthentication = "Windows Authentication"
sqlServerAuthentication = "SQL Server Authentication"
azureAuthentication = "Azure AD Authentication"
windowsAuthentication = "Windows Authentication"
sqlServerAuthentication = "SQL Server Authentication"
kerberosRaw = "Windows AD: Username + password"
kerberosKeytab = "Windows AD: Keytab"
kerberosCredentialCache = "Windows AD: Credential cache" // #nosec G101
kerberosCredentialCacheFile = "Windows AD: Credential cache file" // #nosec G101
)
func ProvideService(cfg *setting.Cfg) *Service {
@ -78,6 +84,12 @@ func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc
if err != nil {
return nil, fmt.Errorf("error reading azure credentials")
}
kerberosAuth, err := kerberos.GetKerberosSettings(settings)
if err != nil {
return nil, fmt.Errorf("error getting kerberos settings: %w", err)
}
err = json.Unmarshal(settings.JSONData, &jsonData)
if err != nil {
return nil, fmt.Errorf("error reading settings: %w", err)
@ -98,7 +110,7 @@ func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc
UID: settings.UID,
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
}
cnnstr, err := generateConnectionString(dsInfo, cfg, azureCredentials, logger)
cnnstr, err := generateConnectionString(dsInfo, cfg, azureCredentials, kerberosAuth, logger)
if err != nil {
return nil, err
}
@ -184,7 +196,7 @@ func ParseURL(u string, logger DebugOnlyLogger) (*url.URL, error) {
}, nil
}
func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, azureCredentials azcredentials.AzureCredentials, logger log.Logger) (string, error) {
func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) {
const dfltPort = "0"
var addr util.NetworkAddress
if dsInfo.URL != "" {
@ -229,6 +241,8 @@ func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, az
connStr += azureCredentialDSNFragment
case windowsAuthentication:
// No user id or password. We're using windows single sign on.
case kerberosRaw, kerberosKeytab, kerberosCredentialCacheFile, kerberosCredentialCache:
connStr = kerberos.Krb5ParseAuthCredentials(addr.Host, addr.Port, dsInfo.Database, dsInfo.User, dsInfo.DecryptedSecureJSONData["password"], kerberosAuth)
default:
connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"])
}

@ -3,6 +3,7 @@ package mssql
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/rand"
"os"
@ -15,6 +16,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/tsdb/mssql/kerberos"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
)
@ -1331,11 +1333,94 @@ func TestTransformQueryError(t *testing.T) {
}
func TestGenerateConnectionString(t *testing.T) {
kerberosLookup := []kerberos.KerberosLookup{
{
Address: "example.host",
DBName: "testDB",
User: "testUser",
CredentialCacheFilename: "/tmp/cache",
},
}
tmpFile := genTempCacheFile(t, kerberosLookup)
defer func() {
err := os.Remove(tmpFile)
if err != nil {
t.Log(err)
}
}()
testCases := []struct {
desc string
dataSource sqleng.DataSourceInfo
expConnStr string
desc string
kerberosCfg kerberos.KerberosAuth
dataSource sqleng.DataSourceInfo
expConnStr string
}{
{
desc: "Use Kerberos Credential Cache",
kerberosCfg: kerberos.KerberosAuth{
CredentialCache: "/tmp/krb5cc_1000",
ConfigFilePath: "/etc/krb5.conf",
},
dataSource: sqleng.DataSourceInfo{
URL: "localhost",
Database: "database",
JsonData: sqleng.JsonData{
AuthenticationType: "Windows AD: Credential cache",
},
},
expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;krb5-credcachefile=/tmp/krb5cc_1000;",
},
{
desc: "Use Kerberos Credential Cache File path",
kerberosCfg: kerberos.KerberosAuth{
CredentialCacheLookupFile: tmpFile,
ConfigFilePath: "/etc/krb5.conf",
},
dataSource: sqleng.DataSourceInfo{
URL: "example.host",
Database: "testDB",
User: "testUser",
JsonData: sqleng.JsonData{
AuthenticationType: "Windows AD: Credential cache file",
},
},
expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=example.host;database=testDB;krb5-credcachefile=/tmp/cache;",
},
{
desc: "Use Kerberos Keytab",
kerberosCfg: kerberos.KerberosAuth{
KeytabFilePath: "/foo/bar.keytab",
ConfigFilePath: "/etc/krb5.conf",
},
dataSource: sqleng.DataSourceInfo{
URL: "localhost",
Database: "database",
User: "foo@test.lab",
JsonData: sqleng.JsonData{
AuthenticationType: "Windows AD: Keytab",
},
},
expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;krb5-keytabfile=/foo/bar.keytab;",
},
{
desc: "Use Kerberos Username and Password",
kerberosCfg: kerberos.KerberosAuth{
ConfigFilePath: "/etc/krb5.conf",
},
dataSource: sqleng.DataSourceInfo{
URL: "localhost",
Database: "database",
User: "foo@test.lab",
DecryptedSecureJSONData: map[string]string{
"password": "foo",
},
JsonData: sqleng.JsonData{
AuthenticationType: "Windows AD: Username + password",
},
},
expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;password=foo;",
},
{
desc: "From URL w/ port",
dataSource: sqleng.DataSourceInfo{
@ -1463,7 +1548,7 @@ func TestGenerateConnectionString(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
connStr, err := generateConnectionString(tc.dataSource, nil, nil, logger)
connStr, err := generateConnectionString(tc.dataSource, nil, nil, tc.kerberosCfg, logger)
require.NoError(t, err)
assert.Equal(t, tc.expConnStr, connStr)
})
@ -1504,3 +1589,21 @@ func genTimeRangeByInterval(from time.Time, duration time.Duration, interval tim
return timeRange
}
func genTempCacheFile(t *testing.T, lookups []kerberos.KerberosLookup) string {
content, err := json.Marshal(lookups)
if err != nil {
t.Fatalf("Unable to marshall json for temp lookup: %v", err)
}
tmpFile, err := os.CreateTemp("", "lookup*.json")
if err != nil {
t.Fatalf("Unable to create temporary file for temp lookup: %v", err)
}
if _, err := tmpFile.Write(content); err != nil {
t.Fatalf("Unable to write to temporary file for temp lookup: %v", err)
}
return tmpFile.Name()
}

@ -37,6 +37,8 @@ import {
MssqlSecureOptions,
} from '../types';
import { KerberosConfig, KerberosAdvancedSettings, UsernameMessage } from './Kerberos';
const LONG_WIDTH = 40;
export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<MssqlOptions, MssqlSecureOptions>) => {
@ -74,7 +76,14 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
onOptionsChange({
...dsSettings,
...{
jsonData: { ...jsonData, ...{ authenticationType: value.value }, azureCredentials: undefined },
jsonData: {
...jsonData,
...{ authenticationType: value.value },
azureCredentials: undefined,
keytabFilePath: undefined,
credentialCache: undefined,
credentialCacheLookupFile: undefined,
},
secureJsonData: { ...dsSettings.secureJsonData, ...{ password: '' } },
secureJsonFields: { ...dsSettings.secureJsonFields, ...{ password: false } },
user: '',
@ -90,6 +99,10 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
const basicAuthenticationOptions: Array<SelectableValue<MSSQLAuthenticationType>> = [
{ value: MSSQLAuthenticationType.sqlAuth, label: 'SQL Server Authentication' },
{ value: MSSQLAuthenticationType.windowsAuth, label: 'Windows Authentication' },
{ value: MSSQLAuthenticationType.kerberosRaw, label: 'Windows AD: Username + password' },
{ value: MSSQLAuthenticationType.kerberosKeytab, label: 'Windows AD: Keytab file' },
{ value: MSSQLAuthenticationType.kerberosCredentialCache, label: 'Windows AD: Credential cache' },
{ value: MSSQLAuthenticationType.kerberosCredentialCacheLookupFile, label: 'Windows AD: Credential cache file' },
];
if (azureAuthIsSupported) {
@ -238,6 +251,21 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
Azure AD credentials - Managed Service Identity and Client Secret Credentials are supported.
</li>
)}
<li>
<i>Windows AD: Username + password</i> Windows Active Directory - Sign on for domain user via
username/password.
</li>
<li>
<i>Windows AD: Keytab</i> Windows Active Directory - Sign on for domain user via keytab file.
</li>
<li>
<i>Windows AD: Credential cache</i> Windows Active Directory - Sign on for domain user via credential
cache.
</li>
<li>
<i>Windows AD: Credential cache file</i> Windows Active Directory - Sign on for domain user via
credential cache file.
</li>
</ul>
}
>
@ -251,14 +279,27 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
/>
</Field>
<KerberosConfig {...props} />
{/* Basic SQL auth. Render if authType === MSSQLAuthenticationType.sqlAuth OR
authType === MSSQLAuthenticationType.kerberosRaw OR
if no authType exists, which will be the case when creating a new data source */}
{(jsonData.authenticationType === MSSQLAuthenticationType.sqlAuth || !jsonData.authenticationType) && (
{(jsonData.authenticationType === MSSQLAuthenticationType.sqlAuth ||
jsonData.authenticationType === MSSQLAuthenticationType.kerberosRaw ||
!jsonData.authenticationType) && (
<>
<Field label="Username" required invalid={!dsSettings.user} error={'Username is required'}>
<Field
label="Username"
required
invalid={!dsSettings.user}
error={'Username is required'}
description={jsonData.authenticationType === MSSQLAuthenticationType.kerberosRaw ? UsernameMessage : ''}
>
<Input
value={dsSettings.user || ''}
placeholder="user"
placeholder={
jsonData.authenticationType === MSSQLAuthenticationType.kerberosRaw ? 'name@EXAMPLE.COM' : 'user'
}
onChange={onDSOptionChanged('user')}
width={LONG_WIDTH}
/>
@ -335,6 +376,7 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
{config.secureSocksDSProxyEnabled && (
<SecureSocksProxySettings options={dsSettings} onOptionsChange={onOptionsChange} />
)}
<KerberosAdvancedSettings {...props} />
</ConfigSection>
</>
);

@ -0,0 +1,203 @@
import React, { SyntheticEvent } from 'react';
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { ConfigSubSection } from '@grafana/experimental';
import { FieldSet, Input, Field } from '@grafana/ui';
import { MSSQLAuthenticationType, MssqlOptions } from '../types';
export const UsernameMessage = (
<span>
Use the format <code>user@EXAMPLE.COM</code>. Realm is derived from the username.
</span>
);
export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps<MssqlOptions>) => {
const { options: settings, onOptionsChange } = props;
const jsonData = settings.jsonData;
const LONG_WIDTH = 40;
const keytabFilePath = jsonData?.keytabFilePath;
const credentialCache = jsonData?.credentialCache;
const credentialCacheLookupFile = jsonData?.credentialCacheLookupFile;
const onKeytabFileChanged = (event: SyntheticEvent<HTMLInputElement>) => {
updateDatasourcePluginJsonDataOption(props, 'keytabFilePath', event.currentTarget.value);
};
const onCredentialCacheChanged = (event: SyntheticEvent<HTMLInputElement>) => {
updateDatasourcePluginJsonDataOption(props, 'credentialCache', event.currentTarget.value);
};
const onCredentialCacheFileChanged = (event: SyntheticEvent<HTMLInputElement>) => {
updateDatasourcePluginJsonDataOption(props, 'credentialCacheLookupFile', event.currentTarget.value);
};
return (
<>
{jsonData.authenticationType === MSSQLAuthenticationType.kerberosKeytab && (
<FieldSet label="Windows AD: Keytab">
<Field
label="Username"
required
invalid={!settings.user}
error={'Username is required'}
description={UsernameMessage}
>
<Input
value={settings.user || ''}
placeholder="name@EXAMPLE.COM"
onChange={(e) => onOptionsChange({ ...settings, ...{ ['user']: e.currentTarget.value } })}
width={LONG_WIDTH}
/>
</Field>
<Field label="Keytab file path" required invalid={!keytabFilePath} error={'Keytab file path is required'}>
<Input
placeholder="/home/grot/grot.keytab"
onChange={onKeytabFileChanged}
width={LONG_WIDTH}
required
value={keytabFilePath || ''}
/>
</Field>
</FieldSet>
)}
{jsonData.authenticationType === MSSQLAuthenticationType.kerberosCredentialCache && (
<FieldSet label="Windows AD: Credential cache">
<Field
label="Credential cache path"
required
invalid={!credentialCache}
error={'Credential cache path is required'}
>
<Input
placeholder="/tmp/krb5cc_1000"
onChange={onCredentialCacheChanged}
width={LONG_WIDTH}
value={credentialCache || ''}
required
/>
</Field>
</FieldSet>
)}
{jsonData.authenticationType === MSSQLAuthenticationType.kerberosCredentialCacheLookupFile && (
<FieldSet label="Windows AD: Credential cache file">
<Field
label="Username"
required
invalid={!settings.user}
error={'Username is required'}
description={UsernameMessage}
>
<Input
value={settings.user || ''}
placeholder="name@EXAMPLE.COM"
onChange={(e) => onOptionsChange({ ...settings, ...{ ['user']: e.currentTarget.value } })}
width={LONG_WIDTH}
/>
</Field>
<Field
label="Credential cache file path"
required
invalid={!credentialCacheLookupFile}
error={'Credential cache file path is required'}
>
<Input
placeholder="/home/grot/cache.json"
onChange={onCredentialCacheFileChanged}
width={LONG_WIDTH}
value={credentialCacheLookupFile || ''}
required
/>
</Field>
</FieldSet>
)}
</>
);
};
export const KerberosAdvancedSettings = (props: DataSourcePluginOptionsEditorProps<MssqlOptions>) => {
const { options: settings } = props;
const jsonData = settings.jsonData;
const configFilePath = jsonData?.configFilePath;
const LONG_WIDTH = 40;
const onUDPLimitChanged = (val: number) => {
updateDatasourcePluginJsonDataOption(props, 'UDPConnectionLimit', val);
};
const onDNSLookupKDCChanged = (event: SyntheticEvent<HTMLInputElement>) => {
updateDatasourcePluginJsonDataOption(props, 'enableDNSLookupKDC', event.currentTarget.value);
};
const onKrbConfigChanged = (event: SyntheticEvent<HTMLInputElement>) => {
updateDatasourcePluginJsonDataOption(props, 'configFilePath', event.currentTarget.value);
};
return (
<>
<ConfigSubSection title="Windows AD: Advanced Settings">
<FieldSet>
<Field
label="UDP Preference Limit"
description={
<span>
The default is <code>1</code> and means always use TCP and is optional.
</span>
}
>
<Input
type="text"
width={LONG_WIDTH}
placeholder="0"
defaultValue={jsonData.UDPConnectionLimit}
onChange={(e) => {
const val = Number(e.currentTarget.value);
if (!Number.isNaN(val)) {
onUDPLimitChanged(val);
}
}}
/>
</Field>
<Field
label="DNS Lookup KDC"
description={
<span>
Indicate whether DNS `SRV` records should be used to locate the KDCs and other servers for a realm. The
default is <code>true</code>.
</span>
}
>
<Input
type="text"
width={LONG_WIDTH}
placeholder="true"
defaultValue={jsonData.enableDNSLookupKDC}
onChange={onDNSLookupKDCChanged}
/>
</Field>
<Field
label="krb5 config file path"
description={
<span>
The path to the configuration file for the{' '}
<a href="https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html">
MIT krb5 package
</a>
. The default is <code>/etc/krb5.conf</code>.
</span>
}
>
<Input
onChange={onKrbConfigChanged}
width={LONG_WIDTH}
required
value={configFilePath || '/etc/krb5.conf'}
/>
</Field>
</FieldSet>
</ConfigSubSection>
</>
);
};

@ -6,6 +6,10 @@ export enum MSSQLAuthenticationType {
sqlAuth = 'SQL Server Authentication',
windowsAuth = 'Windows Authentication',
azureAuth = 'Azure AD Authentication',
kerberosRaw = 'Windows AD: Username + password',
kerberosKeytab = 'Windows AD: Keytab',
kerberosCredentialCache = 'Windows AD: Credential cache',
kerberosCredentialCacheLookupFile = 'Windows AD: Credential cache file',
}
export enum MSSQLEncryptOptions {
@ -41,6 +45,12 @@ export interface MssqlOptions extends SQLOptions {
serverName?: string;
connectionTimeout?: number;
azureCredentials?: AzureCredentialsType;
keytabFilePath?: string;
credentialCache?: string;
credentialCacheLookupFile?: string;
configFilePath?: string;
UDPConnectionLimit?: number;
enableDNSLookupKDC?: string;
}
export interface MssqlSecureOptions {

Loading…
Cancel
Save