From 0f638853e3369b2b307292d19ae9fcb4f7675e66 Mon Sep 17 00:00:00 2001 From: Pete Bassett <97095825+PeteBassettBet365@users.noreply.github.com> Date: Mon, 13 Mar 2023 15:42:07 +0000 Subject: [PATCH] krb5 authentication provider (#65) * New krb5 authenticator * change the krb-keytabcachefile param name to krb5-credcachefile --- CHANGELOG.md | 12 + README.md | 50 +++- integratedauth/krb5/krb5.go | 409 ++++++++++++++++----------- integratedauth/krb5/krb5_test.go | 470 +++++++++++++++++++++---------- 4 files changed, 602 insertions(+), 339 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0aa649a..dfd2405a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ ### Bug fixes * Fixed uninitialized server name in TLS config ([#93](https://github.com/microsoft/go-mssqldb/issues/93))([#94](https://github.com/microsoft/go-mssqldb/pull/94)) +* Fixed several kerberos authentication usages on Linux with new krb5 authentication provider. + +### Changed + +* New kerberos authenticator implementation uses more explicit connection string parameters. + +| Old | New | +|--------------|--------------------| +| krb5conffile | krb5-configfile | +| krbcache | krb5-credcachefile | +| keytabfile | krb5-keytabfile | +| realm | krb5-realm | ## 0.20.0 diff --git a/README.md b/README.md index 43497924..446d58b3 100644 --- a/README.md +++ b/README.md @@ -81,28 +81,50 @@ To force a specific protocol for the connection there two several options: `msdsn.ProtocolParsers` can be reordered to prioritize other protocols ahead of `tcp` ### Kerberos Active Directory authentication outside Windows + +To connect with kerberos authentication from a Linux server you can use the optional krb5 package. +Imported krb alongside the main driver +``` +package main + +import ( + ... + _ "github.com/microsoft/go-mssqldb" + _ "github.com/microsoft/go-mssqldb/integratedauth/krb5" +) + +func main() { + ... +} +``` + +It will become available for use when the connection string parameter "authenticator=krb5" is used. + The package supports authentication via 3 methods. * Keytabs - Specify the username, keytab file, the krb5.conf file, and realm. - authenticator=krb5;server=DatabaseServerName;database=DBName;user id=MyUserName;realm=domain.com;krb5conffile=/etc/krb5.conf;keytabfile=~/MyUserName.keytab + authenticator=krb5;server=DatabaseServerName;database=DBName;user id=MyUserName;krb5-realm=domain.com;krb5-configfile=/etc/krb5.conf;krb5-keytabfile=~/MyUserName.keytab * Credential Cache - Specify the krb5.conf file path and credential cache file path. - authenticator=krb5;server=DatabaseServerName;database=DBName;krb5conffile=/etc/krb5.conf;krbcache=~/MyUserNameCachedCreds + authenticator=krb5;server=DatabaseServerName;database=DBName;krb5-configfile=/etc/krb5.conf;krb5-credcachefile=~/MyUserNameCachedCreds * Raw credentials - Specity krb5.confg, Username, Password and Realm. - authenticator=krb5;server=DatabaseServerName;database=DBName;user id=MyUserName;password=foo;realm=comani.com;krb5conffile=/etc/krb5.conf; + authenticator=krb5;server=DatabaseServerName;database=DBName;user id=MyUserName;password=foo;krb5-realm=comani.com;krb5-configfile=/etc/krb5.conf; ### Kerberos Parameters * `authenticator` - set this to `krb5` to enable kerberos authentication. If this is not present, the default provider would be `ntlm` for unix and `winsspi` for windows. -* `krb5conffile` (mandatory) - path to kerberos configuration file. -* `realm` (required with keytab and raw credentials) - Domain name for kerberos authentication. -* `keytabfile` - path to Keytab file. -* `krbcache` - path to Credential cache. -* For further information on usage: +* `krb5-configfile` (mandatory) - path to kerberos configuration file. +* `krb5-realm` (required with keytab and raw credentials) - Domain name for kerberos authentication. +* `krb5-keytabfile` - path to Keytab file. +* `krb5-credcachefile` - path to Credential cache. +* `krb5-dnslookupkdc` - Optional parameter in all contexts. Set to lookup KDCs in DNS. Boolean. Default is true. +* `krb5-udppreferencelimit` - Optional parameter in all contexts. 1 means to always use tcp. MIT krb5 has a default value of 1465, and it prevents user setting more than 32700. Integer. Default is 1. + +For further information on usage: * * @@ -135,16 +157,16 @@ The package supports authentication via 3 methods. ``` -* `sqlserver://username@host/instance?krb5conffile=path/to/file&krbcache=/path/to/cache` - * `sqlserver://username@host/instance?krb5conffile=path/to/file&realm=domain.com&keytabfile=/path/to/keytabfile` +* `sqlserver://username@host/instance?krb5-configfile=path/to/file&krb5-credcachefile=/path/to/cache` + * `sqlserver://username@host/instance?krb5-configfile=path/to/file&krb5-realm=domain.com&krb5-keytabfile=/path/to/keytabfile` 2. ADO: `key=value` pairs separated by `;`. Values may not contain `;`, leading and trailing whitespace is ignored. Examples: * `server=localhost\\SQLExpress;user id=sa;database=master;app name=MyAppName` * `server=localhost;user id=sa;database=master;app name=MyAppName` - * `server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;krbcache=path/to/cache;authenticator=krb5` - * `server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;realm=domain.com;keytabfile=path/to/keytabfile;authenticator=krb5` + * `server=localhost;user id=sa;database=master;app name=MyAppName;krb5-configfile=path/to/file;krb5-credcachefile=path/to/cache;authenticator=krb5` + * `server=localhost;user id=sa;database=master;app name=MyAppName;krb5-configfile=path/to/file;krb5-realm=domain.com;krb5-keytabfile=path/to/keytabfile;authenticator=krb5` ADO strings support synonyms for database, app name, user id, and server @@ -165,8 +187,8 @@ The package supports authentication via 3 methods. * `odbc:server=localhost;user id=sa;password=foo}bar` // Literal `}`, password is "foo}bar" * `odbc:server=localhost;user id=sa;password={foo{bar}` // Literal `{`, password is "foo{bar" * `odbc:server=localhost;user id=sa;password={foo}}bar}` // Escaped `} with`}}`, password is "foo}bar" - * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;krbcache=path/to/cache;authenticator=krb5` - * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;realm=domain.com;keytabfile=path/to/keytabfile;authenticator=krb5` + * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName;krb5-configfile=path/to/file;krb5-credcachefile=path/to/cache;authenticator=krb5` + * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName;krb5-configfile=path/to/file;krb5-realm=domain.com;krb5-keytabfile=path/to/keytabfile;authenticator=krb5` ### Azure Active Directory authentication diff --git a/integratedauth/krb5/krb5.go b/integratedauth/krb5/krb5.go index 6d968714..3f443ec6 100644 --- a/integratedauth/krb5/krb5.go +++ b/integratedauth/krb5/krb5.go @@ -1,12 +1,11 @@ -//go:build !windows && go1.13 -// +build !windows,go1.13 - +// Package krb5 implements the integratedauth.IntegratedAuthenticator interface in order to provide kerberos/active directory (Windows) based authentication. package krb5 import ( "errors" "fmt" "io/ioutil" + "net" "os" "strconv" "strings" @@ -14,58 +13,39 @@ import ( "github.com/jcmturner/gokrb5/v8/client" "github.com/jcmturner/gokrb5/v8/config" "github.com/jcmturner/gokrb5/v8/credentials" + "github.com/jcmturner/gokrb5/v8/gssapi" "github.com/jcmturner/gokrb5/v8/keytab" "github.com/jcmturner/gokrb5/v8/spnego" + "github.com/microsoft/go-mssqldb/integratedauth" "github.com/microsoft/go-mssqldb/msdsn" ) -var ( - SetKrbConfig = setupKerbConfig - SetKrbKeytab = setupKerbKeytab - SetKrbCache = setupKerbCache +const ( + keytabConfigFile = "krb5-configfile" + keytabFile = "krb5-keytabfile" + credCacheFile = "krb5-credcachefile" + realm = "krb5-realm" + dnsLookupKDC = "krb5-dnslookupkdc" + udpPreferenceLimit = "krb5-udppreferencelimit" ) -// Kerberos Client State -type krb5ClientState int - -type krb5Auth struct { - username string - password string - realm string - serverSPN string - port uint64 - krb5Config *config.Config - krbKeytab *keytab.Keytab - krbCache *credentials.CCache - krb5Client *client.Client - state krb5ClientState -} - -type Kerberos struct { - // Kerberos configuration details - Config *config.Config - - // Credential cache - Cache *credentials.CCache - - // A Kerberos realm is the domain over which a Kerberos authentication server has the authority - // to authenticate a user, host or service. - Realm string - - // Kerberos keytab that stores long-term keys for one or more principals - Keytab *keytab.Keytab -} - -const ( - // Initiator states - initiatorStart krb5ClientState = iota - initiatorWaitForMutal = iota + 2 - initiatorReady +var ( + ErrRequiredParametersMissing = errors.New("failed to create krb5 client from login parameters") + ErrRealmRequiredWithUsernameAndPassword = errors.New("krb5-realm is required to login with krb5 when using user id and password") + ErrKrb5ConfigFileRequiredWithUsernameAndPassword = errors.New("krb5-configfile is required to login with krb5 when using user id and password") + ErrUsernameRequiredWithKeytab = errors.New("user id is required to login with krb5 when using krb5-keytabfile") + ErrRealmRequiredWithKeytab = errors.New("krb5-realm is required to login with krb5 when using krb5-keytabfile") + ErrKrb5ConfigFileRequiredWithKeytab = errors.New("krb5-configfile is required to login with krb5 when using krb5-keytabfile") + ErrKrb5ConfigFileDoesNotExist = errors.New("krb5-configfile does not exist") + ErrKeytabFileDoesNotExist = errors.New("krb5-keytabfile does not exist") + ErrKrb5ConfigFileRequiredWithCredCache = errors.New("krb5-configfile is required to login with krb5 when using krb5-credcachefile") + ErrCredCacheFileDoesNotExist = errors.New("krb5-credcachefile does not exist") ) var ( - _ integratedauth.IntegratedAuthenticator = (*krb5Auth)(nil) + _ integratedauth.IntegratedAuthenticator = (*krbAuth)(nil) + fileExists = fileExistsOS AuthProviderFunc integratedauth.Provider = integratedauth.ProviderFunc(getAuth) ) @@ -77,192 +57,279 @@ func init() { } func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error) { - var port uint64 - var realm, serviceStr string - var err error + krb5Config, err := readKrb5Config(config) + if err != nil { + return nil, err + } - krb, err := readKrb5Config(config) + err = validateKrb5LoginParams(krb5Config) if err != nil { - return &krb5Auth{}, err + return nil, err } - params1 := strings.Split(config.ServerSPN, ":") - if len(params1) != 2 { - return nil, errors.New("invalid ServerSPN") + + return &krbAuth{ + krb5Config: krb5Config, + }, nil +} + +type loginMethod uint8 + +const ( + none loginMethod = iota + usernameAndPassword + keyTabFile + cachedCredentialsFile +) + +type krb5Login struct { + Krb5ConfigFile string + KeytabFile string + CredCacheFile string + Realm string + UserName string + Password string + ServerSPN string + DNSLookupKDC bool + UDPPreferenceLimit int + loginMethod loginMethod +} + +// copies string parameters from connection string, parses optional parameters +func readKrb5Config(config msdsn.Config) (*krb5Login, error) { + login := &krb5Login{ + Krb5ConfigFile: config.Parameters[keytabConfigFile], + KeytabFile: config.Parameters[keytabFile], + CredCacheFile: config.Parameters[credCacheFile], + Realm: config.Parameters[realm], + UserName: config.User, + Password: config.Password, + ServerSPN: config.ServerSPN, + DNSLookupKDC: true, + UDPPreferenceLimit: 1, + loginMethod: none, } - params2 := strings.Split(params1[1], "@") - switch len(params2) { - case 1: - port, err = strconv.ParseUint(params1[1], 10, 16) + // read optional parameters + val, ok := config.Parameters[dnsLookupKDC] + if ok { + parsed, err := strconv.ParseBool(val) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid '%s' parameter '%s': %s", dnsLookupKDC, val, err.Error()) } - case 2: - port, err = strconv.ParseUint(params2[0], 10, 16) + login.DNSLookupKDC = parsed + } + + val, ok = config.Parameters[udpPreferenceLimit] + if ok { + parsed, err := strconv.Atoi(val) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid '%s' parameter '%s': %s", udpPreferenceLimit, val, err.Error()) } - default: - return nil, errors.New("invalid ServerSPN") + login.UDPPreferenceLimit = parsed } - params3 := strings.Split(config.ServerSPN, "@") - switch len(params3) { - case 1: - serviceStr = params3[0] - params3 = strings.Split(params1[0], "/") - params3 = strings.Split(params3[1], ".") - realm = params3[1] + "." + params3[2] - case 2: - realm = params3[1] - serviceStr = params3[0] + return login, nil +} + +func validateKrb5LoginParams(krbLoginParams *krb5Login) error { + switch { + // using explicit credentials + case krbLoginParams.UserName != "" && krbLoginParams.Password != "": + if krbLoginParams.Realm == "" { + return ErrRealmRequiredWithUsernameAndPassword + } + if krbLoginParams.Krb5ConfigFile == "" { + return ErrKrb5ConfigFileRequiredWithUsernameAndPassword + } + if ok, err := fileExists(krbLoginParams.Krb5ConfigFile, ErrKrb5ConfigFileDoesNotExist); !ok { + return err + } + krbLoginParams.loginMethod = usernameAndPassword + return nil + + //using a keytab file + case krbLoginParams.KeytabFile != "": + if krbLoginParams.UserName == "" { + return ErrUsernameRequiredWithKeytab + } + if krbLoginParams.Realm == "" { + return ErrRealmRequiredWithKeytab + } + if krbLoginParams.Krb5ConfigFile == "" { + return ErrKrb5ConfigFileRequiredWithKeytab + } + if ok, err := fileExists(krbLoginParams.Krb5ConfigFile, ErrKrb5ConfigFileDoesNotExist); !ok { + return err + } + if ok, err := fileExists(krbLoginParams.KeytabFile, ErrKeytabFileDoesNotExist); !ok { + return err + } + krbLoginParams.loginMethod = keyTabFile + return nil + + // using a credential cache file + case krbLoginParams.CredCacheFile != "": + if krbLoginParams.Krb5ConfigFile == "" { + return ErrKrb5ConfigFileRequiredWithCredCache + } + if ok, err := fileExists(krbLoginParams.Krb5ConfigFile, ErrKrb5ConfigFileDoesNotExist); !ok { + return err + } + if ok, err := fileExists(krbLoginParams.CredCacheFile, ErrCredCacheFileDoesNotExist); !ok { + return err + } + krbLoginParams.loginMethod = cachedCredentialsFile + return nil default: - return nil, errors.New("invalid ServerSPN") + return ErrRequiredParametersMissing } - - return &krb5Auth{ - username: config.User, - password: config.Password, - serverSPN: serviceStr, - port: port, - realm: realm, - krb5Config: krb.Config, - krbKeytab: krb.Keytab, - krbCache: krb.Cache, - }, nil } -func (auth *krb5Auth) InitialBytes() ([]byte, error) { - var cl *client.Client - var err error - // Init keytab from conf - if auth.username != "" && auth.password != "" { - cl = client.NewWithPassword(auth.username, auth.realm, auth.password, auth.krb5Config) - } else if auth.krbKeytab != nil { - // Init krb5 client and login - cl = client.NewWithKeytab(auth.username, auth.realm, auth.krbKeytab, auth.krb5Config, client.DisablePAFXFAST(true)) - } else { - cl, err = client.NewFromCCache(auth.krbCache, auth.krb5Config) - if err != nil { - return []byte{}, err - } +func fileExistsOS(filename string, errWhenFileNotFound error) (bool, error) { + _, err := os.Stat(filename) + if err == nil { + return true, nil } - auth.krb5Client = cl - auth.state = initiatorStart - tkt, sessionKey, err := cl.GetServiceTicket(auth.serverSPN) - if err != nil { - return []byte{}, err + if errors.Is(err, os.ErrNotExist) { + return false, errWhenFileNotFound } + return false, fmt.Errorf("%v : %w", errWhenFileNotFound, err) +} - negTok, err := spnego.NewNegTokenInitKRB5(auth.krb5Client, tkt, sessionKey) +// krbAuth implements the integratedauth.IntegratedAuthenticator interface. It is responsible for kerberos Service Provider Negotiation. +type krbAuth struct { + krb5Config *krb5Login + spnegoClient *spnego.SPNEGO + krb5Client *client.Client +} + +func (k *krbAuth) InitialBytes() ([]byte, error) { + krbClient, err := getKrb5Client(k.krb5Config) if err != nil { - return []byte{}, err + return nil, err } - outToken, err := negTok.Marshal() + err = krbClient.Login() if err != nil { - return []byte{}, err + return nil, err } - auth.state = initiatorWaitForMutal - return outToken, nil -} -func (auth *krb5Auth) Free() { - auth.krb5Client.Destroy() -} + k.krb5Client = krbClient + k.spnegoClient = spnego.SPNEGOClient(k.krb5Client, canonicalize(k.krb5Config.ServerSPN)) -func (auth *krb5Auth) NextBytes(token []byte) ([]byte, error) { - var spnegoToken spnego.SPNEGOToken - if err := spnegoToken.Unmarshal(token); err != nil { - err := fmt.Errorf("unmarshal APRep token failed: %w", err) - return []byte{}, err + tkn, err := k.spnegoClient.InitSecContext() + if err != nil { + return nil, err } - auth.state = initiatorReady - return []byte{}, nil + return tkn.Marshal() } -func readKrb5Config(config msdsn.Config) (Kerberos, error) { - krb := Kerberos{} - var err error - - krbConfig, ok := config.Parameters["krb5conffile"] - if !ok { - return krb, fmt.Errorf("krb5 config file is required") +func (k *krbAuth) NextBytes(bytes []byte) ([]byte, error) { + var resp spnego.SPNEGOToken + if err := resp.Unmarshal(bytes); err != nil { + return nil, err } - krb.Config, err = SetKrbConfig(krbConfig) - if err != nil { - return krb, err + ok, status := resp.Verify() + if ok { // we're ok, done + return nil, nil } - missingParam := validateKerbConfig(config.Parameters) - if missingParam != "" { - return krb, fmt.Errorf("missing parameter:%s", missingParam) + switch status.Code { + case gssapi.StatusContinueNeeded: + return nil, nil + default: + return nil, fmt.Errorf("bad status: %+v", status) } +} - if realm, ok := config.Parameters["realm"]; ok { - krb.Realm = realm +func (k *krbAuth) Free() { + if k.krb5Client != nil { + k.krb5Client.Destroy() + k.krb5Client = nil } +} - if krbCache, ok := config.Parameters["krbcache"]; ok { - krb.Cache, err = SetKrbCache(krbCache) - if err != nil { - return krb, err - } +func getKrb5Client(krbLoginParams *krb5Login) (*client.Client, error) { + cfg, err := newKrb5ConfigFromFile(krbLoginParams) + if err != nil { + return nil, err } - if keytabfile, ok := config.Parameters["keytabfile"]; ok { - krb.Keytab, err = SetKrbKeytab(keytabfile) - if err != nil { - return krb, err - } + switch krbLoginParams.loginMethod { + case usernameAndPassword: + return clientFromUsernameAndPassword(krbLoginParams, cfg) + case keyTabFile: + return clientFromKeytab(krbLoginParams, cfg) + case cachedCredentialsFile: + return clientFromCredentialCache(krbLoginParams, cfg) + default: + return nil, ErrRequiredParametersMissing } - - return krb, nil } -func validateKerbConfig(c map[string]string) (missingParam string) { - if c["keytabfile"] != "" { - if c["realm"] == "" { - missingParam = "realm" - return - } +func newKrb5ConfigFromFile(krb5Login *krb5Login) (*config.Config, error) { + f, err := os.Open(krb5Login.Krb5ConfigFile) + if err != nil { + return nil, err } - if c["krbcache"] == "" && c["keytabfile"] == "" { - missingParam = "atleast krbcache or keytab is required" - return + defer f.Close() + + cfg, err := config.NewFromReader(f) + if err != nil { + return nil, err } - return + cfg.LibDefaults.DNSLookupKDC = krb5Login.DNSLookupKDC + cfg.LibDefaults.UDPPreferenceLimit = krb5Login.UDPPreferenceLimit + + return cfg, nil +} + +// creates a client from hardcoded user id & password credentials in the connection string in addition to realm +func clientFromUsernameAndPassword(krb5Login *krb5Login, cfg *config.Config) (*client.Client, error) { + return client.NewWithPassword(krb5Login.UserName, krb5Login.Realm, krb5Login.Password, cfg, client.DisablePAFXFAST(true)), nil } -func setupKerbConfig(krb5configPath string) (*config.Config, error) { - krb5CnfFile, err := os.Open(krb5configPath) +// loads keytab file specified in keytabFile and creates a client from its content, username and realm +func clientFromKeytab(krb5Login *krb5Login, cfg *config.Config) (*client.Client, error) { + data, err := ioutil.ReadFile(krb5Login.KeytabFile) if err != nil { return nil, err } - c, err := config.NewFromReader(krb5CnfFile) + var kt = &keytab.Keytab{} + err = kt.Unmarshal(data) if err != nil { return nil, err } - return c, nil + + return client.NewWithKeytab(krb5Login.UserName, krb5Login.Realm, kt, cfg, client.DisablePAFXFAST(true)), nil } -func setupKerbCache(kerbCCahePath string) (*credentials.CCache, error) { - cache, err := credentials.LoadCCache(kerbCCahePath) +// loads credential cache file specified in credCacheFile parameter and creates a client +func clientFromCredentialCache(krb5Login *krb5Login, cfg *config.Config) (*client.Client, error) { + cache, err := credentials.LoadCCache(krb5Login.CredCacheFile) if err != nil { return nil, err } - return cache, nil + + return client.NewFromCCache(cache, cfg, client.DisablePAFXFAST(true)) } -func setupKerbKeytab(keytabFilePath string) (*keytab.Keytab, error) { - var kt = &keytab.Keytab{} - keytabConf, err := ioutil.ReadFile(keytabFilePath) +// responsible for transforming network CNames into their actual Hostname. +// For cases where service tickets can only be bound to hostnames, not cnames. +func canonicalize(service string) string { + parts := strings.SplitAfterN(service, "/", 2) + if len(parts) != 2 { + return service + } + host, port, err := net.SplitHostPort(parts[1]) if err != nil { - return nil, err + return service } - if err = kt.Unmarshal([]byte(keytabConf)); err != nil { - return nil, err + cname, err := net.LookupCNAME(strings.ToLower(host)) + if err != nil { + return service } - return kt, nil + // Put service back together with cname (stripped of trailing .) and port + return parts[0] + net.JoinHostPort(cname[:len(cname)-1], port) } diff --git a/integratedauth/krb5/krb5_test.go b/integratedauth/krb5/krb5_test.go index 5da6596d..ad1264ba 100644 --- a/integratedauth/krb5/krb5_test.go +++ b/integratedauth/krb5/krb5_test.go @@ -1,215 +1,377 @@ -//go:build !windows && go1.13 -// +build !windows,go1.13 - package krb5 import ( - "io/ioutil" - "reflect" + "strings" "testing" - "github.com/jcmturner/gokrb5/v8/client" - "github.com/jcmturner/gokrb5/v8/config" - "github.com/jcmturner/gokrb5/v8/credentials" - "github.com/jcmturner/gokrb5/v8/keytab" - "github.com/microsoft/go-mssqldb/integratedauth" "github.com/microsoft/go-mssqldb/msdsn" ) -func TestGetAuth(t *testing.T) { - kerberos := getKerberos() - var err error - configParams := msdsn.Config{ - User: "", - ServerSPN: "MSSQLSvc/mssql.domain.com:1433", - Port: 1433, +func TestReadKrb5ConfigHappyPath(t *testing.T) { + config := msdsn.Config{ + User: "username", + Password: "password", + ServerSPN: "serverspn", Parameters: map[string]string{ - "krb5conffile": "krb5conffile", - "keytabfile": "keytabfile", - "krbcache": "krbcache", - "realm": "domain.com", + "krb5-configfile": "krb5-configfile", + "krb5-keytabfile": "krb5-keytabfile", + "krb5-credcachefile": "krb5-credcachefile", + "krb5-realm": "krb5-realm", + "krb5-dnslookupkdc": "false", + "krb5-udppreferencelimit": "1234", }, } - SetKrbConfig = func(krb5configPath string) (*config.Config, error) { - return &config.Config{}, nil - } - SetKrbKeytab = func(keytabFilePath string) (*keytab.Keytab, error) { - return &keytab.Keytab{}, nil + actual, err := readKrb5Config(config) + + if err != nil { + t.Errorf("Unexpected error %v", err) } - SetKrbCache = func(kerbCCahePath string) (*credentials.CCache, error) { - return &credentials.CCache{}, nil + + if actual.Krb5ConfigFile != config.Parameters[keytabConfigFile] { + t.Errorf("Expected Krb5ConfigFile %v, found %v", config.Parameters[keytabConfigFile], actual.Krb5ConfigFile) } - got, err := getAuth(configParams) - if err != nil { - t.Errorf("failed:%v", err) + if actual.KeytabFile != config.Parameters[keytabFile] { + t.Errorf("Expected KeytabFile %v, found %v", config.Parameters[keytabFile], actual.KeytabFile) } - kt := &krb5Auth{username: "", - realm: "domain.com", - serverSPN: "MSSQLSvc/mssql.domain.com:1433", - port: 1433, - krb5Config: kerberos.Config, - krbKeytab: kerberos.Keytab, - krbCache: kerberos.Cache, - state: 0} - res := reflect.DeepEqual(got, kt) - if !res { - t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", kt, got) + if actual.CredCacheFile != config.Parameters[credCacheFile] { + t.Errorf("Expected CredCacheFile %v, found %v", config.Parameters[credCacheFile], actual.CredCacheFile) } - configParams.ServerSPN = "MSSQLSvc/mssql.domain.com" + if actual.Realm != config.Parameters[realm] { + t.Errorf("Expected Realm %v, found %v", config.Parameters[realm], actual.Realm) + } - _, val := getAuth(configParams) - if val == nil { - t.Errorf("Failed to get correct krb5Auth object: no port defined") + if actual.UserName != config.User { + t.Errorf("Expected username %v, found %v", config.User, actual.UserName) } - configParams.ServerSPN = "MSSQLSvc/mssql.domain.com:1433@DOMAIN.COM" + if actual.Password != config.Password { + t.Errorf("Expected password %v, found %v", config.Password, actual.Password) + } - got, _ = getAuth(configParams) - kt = &krb5Auth{username: "", - realm: "DOMAIN.COM", - serverSPN: "MSSQLSvc/mssql.domain.com:1433", - port: 1433, - krb5Config: kerberos.Config, - krbKeytab: kerberos.Keytab, - krbCache: kerberos.Cache, - state: 0} + if actual.ServerSPN != config.ServerSPN { + t.Errorf("Expected serverSpn %v, found %v", config.ServerSPN, actual.ServerSPN) + } - res = reflect.DeepEqual(got, kt) - if !res { - t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", kt, got) + if actual.DNSLookupKDC != false { + t.Errorf("Expected DNSLookupKDC %v, found %v", false, actual.DNSLookupKDC) } - configParams.ServerSPN = "MSSQLSvc/mssql.domain.com:1433@domain.com@test" - _, val = getAuth(configParams) - if val == nil { - t.Errorf("Failed to get correct krb5Auth object due to incorrect serverSPN name") + if actual.UDPPreferenceLimit != 1234 { + t.Errorf("Expected UDPPreferenceLimit %v, found %v", 1234, actual.UDPPreferenceLimit) } +} + +func TestReadKrb5ConfigErrorCases(t *testing.T) { - configParams.ServerSPN = "MSSQLSvc/mssql.domain.com:port@domain.com" - _, val = getAuth(configParams) - if val == nil { - t.Errorf("Failed to get correct krb5Auth object due to incorrect port") + tests := []struct { + name string + dnslookup string + udpPreferenceLimit string + expectedError string + }{ + + { + name: "invalid dnslookupkdc", + dnslookup: "a", + udpPreferenceLimit: "1234", + expectedError: "invalid 'krb5-dnslookupkdc' parameter 'a': strconv.ParseBool: parsing \"a\": invalid syntax", + }, + { + name: "invalid udpPreferenceLimit", + dnslookup: "true", + udpPreferenceLimit: "a", + expectedError: "invalid 'krb5-udppreferencelimit' parameter 'a': strconv.Atoi: parsing \"a\": invalid syntax", + }, } - configParams.ServerSPN = "MSSQLSvc/mssql.domain.com:port" - _, val = getAuth(configParams) - if val == nil { - t.Errorf("Failed to get correct krb5Auth object due to incorrect port") + for _, tt := range tests { + config := msdsn.Config{ + Parameters: map[string]string{ + "krb5-dnslookupkdc": tt.dnslookup, + "krb5-udppreferencelimit": tt.udpPreferenceLimit, + }, + } + + actual, err := readKrb5Config(config) + + if actual != nil { + t.Errorf("Unexpected return value expected nil, found %v", actual) + continue + } + + if err == nil { + t.Errorf("Expected error '%v', found nil", tt.expectedError) + continue + } + + if err.Error() != tt.expectedError { + t.Errorf("Expected error %v, found %v", tt.expectedError, err) + } } } -func TestInitialBytes(t *testing.T) { - kerberos := getKerberos() - krbObj := &krb5Auth{username: "", - realm: "domain.com", - serverSPN: "MSSQLSvc/mssql.domain.com:1433", - port: 1433, - krb5Config: kerberos.Config, - krbKeytab: kerberos.Keytab, - krbCache: kerberos.Cache, - state: 0, +func TestValidateKrb5LoginParams(t *testing.T) { + + tests := []struct { + name string + input *krb5Login + expectedLoginMethod loginMethod + expectedError error + }{ + + { + name: "happy username and password", + input: &krb5Login{ + Krb5ConfigFile: "exists", + Realm: "realm", + UserName: "username", + Password: "password", + }, + expectedLoginMethod: usernameAndPassword, + expectedError: nil, + }, + { + name: "username and password, missing realm", + input: &krb5Login{ + Krb5ConfigFile: "exists", + Realm: "", + UserName: "username", + Password: "password", + }, + expectedLoginMethod: none, + expectedError: ErrRealmRequiredWithUsernameAndPassword, + }, + { + name: "username and password, missing Krb5ConfigFile", + input: &krb5Login{ + Krb5ConfigFile: "", + Realm: "realm", + UserName: "username", + Password: "password", + }, + expectedLoginMethod: none, + expectedError: ErrKrb5ConfigFileRequiredWithUsernameAndPassword, + }, + { + name: "username and password, Krb5ConfigFile file not found", + input: &krb5Login{ + Krb5ConfigFile: "missing", + Realm: "realm", + UserName: "username", + Password: "password", + }, + expectedLoginMethod: none, + expectedError: ErrKrb5ConfigFileDoesNotExist, + }, + { + name: "happy keytab", + input: &krb5Login{ + KeytabFile: "exists", + Krb5ConfigFile: "exists", + Realm: "realm", + UserName: "username", + }, + expectedLoginMethod: keyTabFile, + expectedError: nil, + }, + { + name: "keytab, missing username", + input: &krb5Login{ + KeytabFile: "exists", + Krb5ConfigFile: "exists", + Realm: "realm", + UserName: "", + }, + expectedLoginMethod: none, + expectedError: ErrUsernameRequiredWithKeytab, + }, + { + name: "keytab, missing realm", + input: &krb5Login{ + KeytabFile: "exists", + Krb5ConfigFile: "exists", + Realm: "", + UserName: "username", + }, + expectedLoginMethod: none, + expectedError: ErrRealmRequiredWithKeytab, + }, + { + name: "keytab, missing Krb5ConfigFile", + input: &krb5Login{ + KeytabFile: "exists", + Krb5ConfigFile: "", + Realm: "realm", + UserName: "username", + }, + expectedLoginMethod: none, + expectedError: ErrKrb5ConfigFileRequiredWithKeytab, + }, + { + name: "keytab, Krb5ConfigFile file not found", + input: &krb5Login{ + KeytabFile: "exists", + Krb5ConfigFile: "missing", + Realm: "realm", + UserName: "username", + }, + expectedLoginMethod: none, + expectedError: ErrKrb5ConfigFileDoesNotExist, + }, + { + name: "keytab, KeytabFile file not found", + input: &krb5Login{ + KeytabFile: "missing", + Krb5ConfigFile: "exists", + Realm: "realm", + UserName: "username", + }, + expectedLoginMethod: none, + expectedError: ErrKeytabFileDoesNotExist, + }, + { + name: "happy credential cache", + input: &krb5Login{ + CredCacheFile: "exists", + Krb5ConfigFile: "exists", + }, + expectedLoginMethod: cachedCredentialsFile, + expectedError: nil, + }, + { + name: "credential cache, missing Krb5ConfigFile", + input: &krb5Login{ + CredCacheFile: "exists", + Krb5ConfigFile: "", + }, + expectedLoginMethod: none, + expectedError: ErrKrb5ConfigFileRequiredWithCredCache, + }, + { + name: "credential cache, Krb5ConfigFile file not found", + input: &krb5Login{ + CredCacheFile: "exists", + Krb5ConfigFile: "missing", + }, + expectedLoginMethod: none, + expectedError: ErrKrb5ConfigFileDoesNotExist, + }, + { + name: "credential cache, CredCacheFile file not found", + input: &krb5Login{ + CredCacheFile: "missing", + Krb5ConfigFile: "exists", + }, + expectedLoginMethod: none, + expectedError: ErrCredCacheFileDoesNotExist, + }, + { + name: "no login method math", + input: &krb5Login{}, + expectedLoginMethod: none, + expectedError: ErrRequiredParametersMissing, + }, } - _, err := krbObj.InitialBytes() - if err == nil { - t.Errorf("Initial Bytes expected to fail but it didn't") + revert := mockFileExists() + defer revert() + + for _, tt := range tests { + tt.input.loginMethod = none + err := validateKrb5LoginParams(tt.input) + + if err != nil && tt.expectedError == nil { + t.Errorf("Unexpected error %v, expected nil", err) + } + + if err == nil && tt.expectedError != nil { + t.Errorf("Expected error %v, found nil", tt.expectedError) + } + + if err != tt.expectedError { + t.Errorf("Expected error %v, found %v", tt.expectedError, err) + } + + if tt.input.loginMethod != tt.expectedLoginMethod { + t.Errorf("Expected loginMethod %v, found %v", tt.expectedLoginMethod, tt.input.loginMethod) + } } +} - krbObj.krbKeytab = nil - _, err = krbObj.InitialBytes() - if err == nil { - t.Errorf("Initial Bytes expected to fail but it didn't") +func mockFileExists() func() { + fileExists = func(filename string, errWhenFileNotFound error) (bool, error) { + if strings.Contains(filename, "exists") { + return true, nil + } + + return false, errWhenFileNotFound } + return func() { fileExists = fileExistsOS } } -func TestNextBytes(t *testing.T) { - ans := []byte{} - kerberos := getKerberos() +func TestGetAuth(t *testing.T) { + config := msdsn.Config{ + User: "username", + Password: "password", + ServerSPN: "serverspn", + Parameters: map[string]string{ + "krb5-configfile": "exists", + "krb5-keytabfile": "exists", + "krb5-keytabcachefile": "exists", + "krb5-realm": "krb5-realm", + "krb5-dnslookupkdc": "false", + "krb5-udppreferencelimit": "1234", + }, + } - var krbObj integratedauth.IntegratedAuthenticator = &krb5Auth{username: "", - realm: "domain.com", - serverSPN: "MSSQLSvc/mssql.domain.com:1433", - port: 1433, - krb5Config: kerberos.Config, - krbKeytab: kerberos.Keytab, - krbCache: kerberos.Cache, - state: 0} + revert := mockFileExists() + defer revert() - _, err := krbObj.NextBytes(ans) - if err == nil { - t.Errorf("Next Byte expected to fail but it didn't") + a, err := getAuth(config) + if err != nil { + t.Errorf("Unexpected error %v", err) } -} -func TestFree(t *testing.T) { - kerberos := getKerberos() - kt := &keytab.Keytab{} - c := &config.Config{} + actual := a.(*krbAuth) - cl := client.NewWithKeytab("Administrator", "DOMAIN.COM", kt, c, client.DisablePAFXFAST(true)) + if actual.krb5Config.Krb5ConfigFile != config.Parameters[keytabConfigFile] { + t.Errorf("Expected Krb5ConfigFile %v, found %v", config.Parameters[keytabConfigFile], actual.krb5Config.Krb5ConfigFile) + } - var krbObj integratedauth.IntegratedAuthenticator = &krb5Auth{username: "", - realm: "domain.com", - serverSPN: "MSSQLSvc/mssql.domain.com:1433", - port: 1433, - krb5Config: kerberos.Config, - krbKeytab: kerberos.Keytab, - krbCache: kerberos.Cache, - state: 0, - krb5Client: cl, + if actual.krb5Config.KeytabFile != config.Parameters[keytabFile] { + t.Errorf("Expected KeytabFile %v, found %v", config.Parameters[keytabFile], actual.krb5Config.KeytabFile) } - krbObj.Free() - cacheEntries := len(kerberos.Cache.GetEntries()) - if cacheEntries != 0 { - t.Errorf("Client not destroyed") + + if actual.krb5Config.CredCacheFile != config.Parameters[credCacheFile] { + t.Errorf("Expected CredCacheFile %v, found %v", config.Parameters[credCacheFile], actual.krb5Config.CredCacheFile) } -} -func TestSetKrbConfig(t *testing.T) { - krb5conffile := createTempFile(t, "krb5conffile") - _, err := setupKerbConfig(krb5conffile) - if err != nil { - t.Errorf("Failed to read krb5 config file") + if actual.krb5Config.Realm != config.Parameters[realm] { + t.Errorf("Expected Realm %v, found %v", config.Parameters[realm], actual.krb5Config.Realm) } -} -func TestSetKrbKeytab(t *testing.T) { - krbkeytab := createTempFile(t, "keytabfile") - _, err := setupKerbKeytab(krbkeytab) - if err == nil { - t.Errorf("Failed to read keytab file") + if actual.krb5Config.UserName != config.User { + t.Errorf("Expected username %v, found %v", config.User, actual.krb5Config.UserName) } -} -func TestSetKrbCache(t *testing.T) { - krbcache := createTempFile(t, "krbcache") - _, err := setupKerbCache(krbcache) - if err == nil { - t.Errorf("Failed to read cache file") + if actual.krb5Config.Password != config.Password { + t.Errorf("Expected password %v, found %v", config.Password, actual.krb5Config.Password) } -} -func getKerberos() (krbParams *Kerberos) { - krbParams = &Kerberos{ - Config: &config.Config{}, - Keytab: &keytab.Keytab{}, - Cache: &credentials.CCache{}, + if actual.krb5Config.ServerSPN != config.ServerSPN { + t.Errorf("Expected serverSpn %v, found %v", config.ServerSPN, actual.krb5Config.ServerSPN) } - return -} -func createTempFile(t *testing.T, filename string) string { - file, err := ioutil.TempFile("", "test-"+filename+".txt") - if err != nil { - t.Fatalf("Failed to create a temp file:%v", err) + if actual.krb5Config.DNSLookupKDC != false { + t.Errorf("Expected DNSLookupKDC %v, found %v", false, actual.krb5Config.DNSLookupKDC) } - if _, err := file.Write([]byte("This is a test file\n")); err != nil { - t.Fatalf("Failed to write file:%v", err) + + if actual.krb5Config.UDPPreferenceLimit != 1234 { + t.Errorf("Expected UDPPreferenceLimit %v, found %v", 1234, actual.krb5Config.UDPPreferenceLimit) } - return file.Name() }