mirror of https://github.com/grafana/grafana
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 supportpull/84825/head
parent
04c9f459ec
commit
311aa94fab
@ -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 "" |
||||
} |
@ -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> |
||||
</> |
||||
); |
||||
}; |
Loading…
Reference in new issue