From 311aa94fabfab4efe358f2030e28ae5bfe7ee616 Mon Sep 17 00:00:00 2001 From: Adam Simpson Date: Wed, 20 Mar 2024 10:41:57 -0400 Subject: [PATCH] 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 --- docs/sources/datasources/mssql/_index.md | 14 +- .../get-started-grafana-ms-sql-server.md | 48 ++++- go.mod | 7 + go.sum | 15 ++ pkg/tsdb/mssql/kerberos/kerberos.go | 106 +++++++++ pkg/tsdb/mssql/mssql.go | 24 ++- pkg/tsdb/mssql/mssql_test.go | 111 +++++++++- .../configuration/ConfigurationEditor.tsx | 50 ++++- .../mssql/configuration/Kerberos.tsx | 203 ++++++++++++++++++ public/app/plugins/datasource/mssql/types.ts | 10 + 10 files changed, 571 insertions(+), 17 deletions(-) create mode 100644 pkg/tsdb/mssql/kerberos/kerberos.go create mode 100644 public/app/plugins/datasource/mssql/configuration/Kerberos.tsx diff --git a/docs/sources/datasources/mssql/_index.md b/docs/sources/datasources/mssql/_index.md index 9bf5ccf3a00..89403bb3f7b 100644 --- a/docs/sources/datasources/mssql/_index.md +++ b/docs/sources/datasources/mssql/_index.md @@ -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. diff --git a/docs/sources/getting-started/get-started-grafana-ms-sql-server.md b/docs/sources/getting-started/get-started-grafana-ms-sql-server.md index b9f0a95fa47..2ca4dcac2b4 100644 --- a/docs/sources/getting-started/get-started-grafana-ms-sql-server.md +++ b/docs/sources/getting-started/get-started-grafana-ms-sql-server.md @@ -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. diff --git a/go.mod b/go.mod index 51713e1d29b..bc5d53b614c 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 3065ccc8953..3e92878e262 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/tsdb/mssql/kerberos/kerberos.go b/pkg/tsdb/mssql/kerberos/kerberos.go new file mode 100644 index 00000000000..207f324672a --- /dev/null +++ b/pkg/tsdb/mssql/kerberos/kerberos.go @@ -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 "" +} diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index a99064f3e9e..368e0141847 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -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"]) } diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 9c54e213d81..77b2c1f4fbf 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -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() +} diff --git a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx index b949fcbc4ca..be0ed607137 100644 --- a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx @@ -37,6 +37,8 @@ import { MssqlSecureOptions, } from '../types'; +import { KerberosConfig, KerberosAdvancedSettings, UsernameMessage } from './Kerberos'; + const LONG_WIDTH = 40; export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps) => { @@ -74,7 +76,14 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps> = [ { 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 )} +
  • + Windows AD: Username + password Windows Active Directory - Sign on for domain user via + username/password. +
  • +
  • + Windows AD: Keytab Windows Active Directory - Sign on for domain user via keytab file. +
  • +
  • + Windows AD: Credential cache Windows Active Directory - Sign on for domain user via credential + cache. +
  • +
  • + Windows AD: Credential cache file Windows Active Directory - Sign on for domain user via + credential cache file. +
  • } > @@ -251,14 +279,27 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps + + {/* 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) && ( <> - + @@ -335,6 +376,7 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps )} + ); diff --git a/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx b/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx new file mode 100644 index 00000000000..a73b6eb1a19 --- /dev/null +++ b/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx @@ -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 = ( + + Use the format user@EXAMPLE.COM. Realm is derived from the username. + +); + +export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps) => { + 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) => { + updateDatasourcePluginJsonDataOption(props, 'keytabFilePath', event.currentTarget.value); + }; + + const onCredentialCacheChanged = (event: SyntheticEvent) => { + updateDatasourcePluginJsonDataOption(props, 'credentialCache', event.currentTarget.value); + }; + + const onCredentialCacheFileChanged = (event: SyntheticEvent) => { + updateDatasourcePluginJsonDataOption(props, 'credentialCacheLookupFile', event.currentTarget.value); + }; + + return ( + <> + {jsonData.authenticationType === MSSQLAuthenticationType.kerberosKeytab && ( +
    + + onOptionsChange({ ...settings, ...{ ['user']: e.currentTarget.value } })} + width={LONG_WIDTH} + /> + + + + +
    + )} + + {jsonData.authenticationType === MSSQLAuthenticationType.kerberosCredentialCache && ( +
    + + + +
    + )} + + {jsonData.authenticationType === MSSQLAuthenticationType.kerberosCredentialCacheLookupFile && ( +
    + + onOptionsChange({ ...settings, ...{ ['user']: e.currentTarget.value } })} + width={LONG_WIDTH} + /> + + + + +
    + )} + + ); +}; + +export const KerberosAdvancedSettings = (props: DataSourcePluginOptionsEditorProps) => { + 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) => { + updateDatasourcePluginJsonDataOption(props, 'enableDNSLookupKDC', event.currentTarget.value); + }; + + const onKrbConfigChanged = (event: SyntheticEvent) => { + updateDatasourcePluginJsonDataOption(props, 'configFilePath', event.currentTarget.value); + }; + + return ( + <> + +
    + + The default is 1 and means always use TCP and is optional. + + } + > + { + const val = Number(e.currentTarget.value); + if (!Number.isNaN(val)) { + onUDPLimitChanged(val); + } + }} + /> + + + Indicate whether DNS `SRV` records should be used to locate the KDCs and other servers for a realm. The + default is true. + + } + > + + + + The path to the configuration file for the{' '} + + MIT krb5 package + + . The default is /etc/krb5.conf. + + } + > + + +
    +
    + + ); +}; diff --git a/public/app/plugins/datasource/mssql/types.ts b/public/app/plugins/datasource/mssql/types.ts index 40c1914b34b..ca35d492fb3 100644 --- a/public/app/plugins/datasource/mssql/types.ts +++ b/public/app/plugins/datasource/mssql/types.ts @@ -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 {