Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Allow krb5 config through environment variables #157

Merged
merged 5 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.7.0

### Changed

* krb5 authenticator supports standard Kerberos environment variables for configuration

## 1.6.0

### Changed
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ The package supports authentication via 3 methods.
### 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.
* `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-configfile` (optional) - path to kerberos configuration file. Defaults to `/etc/krb5.conf`. Can also be set using `KRB5_CONFIG` environment variable.
* `krb5-realm` (required with keytab and raw credentials) - Domain name for kerberos authentication. Omit this parameter if the realm is part of the user name like `username@REALM`.
* `krb5-keytabfile` - path to Keytab file. Can also be set using environment variable `KRB5_KTNAME`.
* `krb5-credcachefile` - path to Credential cache. Can also be set using environment variable `KRBCCNAME`.
* `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.

Expand Down
61 changes: 49 additions & 12 deletions integratedauth/krb5/krb5.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package krb5
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"strconv"
Expand Down Expand Up @@ -95,22 +94,60 @@ type krb5Login struct {
}

// copies string parameters from connection string, parses optional parameters
func readKrb5Config(config msdsn.Config) (*krb5Login, error) {
// Environment variables for Kerberos config are listed at https://web.mit.edu/kerberos/krb5-1.12/doc/admin/env_variables.html
func readKrb5Config(cfg 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,
Krb5ConfigFile: cfg.Parameters[keytabConfigFile],
KeytabFile: cfg.Parameters[keytabFile],
CredCacheFile: cfg.Parameters[credCacheFile],
Realm: cfg.Parameters[realm],
UserName: cfg.User,
Password: cfg.Password,
ServerSPN: cfg.ServerSPN,
DNSLookupKDC: true,
UDPPreferenceLimit: 1,
loginMethod: none,
}

defaults := config.New()
// If no conf file is provided , use the environment variable first then just use the default conf file location if not set
if len(login.Krb5ConfigFile) == 0 {
login.Krb5ConfigFile = os.Getenv("KRB5_CONFIG")
}

if len(login.Krb5ConfigFile) == 0 {
login.Krb5ConfigFile = `/etc/krb5.conf`
}

// If no Realm passed, try to split out the user name as `username@realm`
shueybubbles marked this conversation as resolved.
Show resolved Hide resolved
if len(login.Realm) == 0 {
nameParts := strings.SplitN(login.UserName, "@", 2)
if len(nameParts) > 1 {
login.UserName = nameParts[0]
login.Realm = nameParts[1]
}
}

// If the app provides a user name with no password, give the keytab file precedence over the credential cache
if len(login.UserName) > 0 && len(login.Password) == 0 {
if len(login.KeytabFile) == 0 {
login.KeytabFile = os.Getenv("KRB5_KTNAME")
}
if len(login.KeytabFile) == 0 {
kt := defaults.LibDefaults.DefaultKeytabName
if ok, _ := fileExists(kt, nil); ok {
login.KeytabFile = kt
}
}
}

// We fall back to the environment variable if set, but it will be ignored for login if login.KeytabFile is set
if len(login.CredCacheFile) == 0 {
login.CredCacheFile = os.Getenv("KRB5CCNAME")
}

// read optional parameters
val, ok := config.Parameters[dnsLookupKDC]
val, ok := cfg.Parameters[dnsLookupKDC]
if ok {
parsed, err := strconv.ParseBool(val)
if err != nil {
Expand All @@ -119,7 +156,7 @@ func readKrb5Config(config msdsn.Config) (*krb5Login, error) {
login.DNSLookupKDC = parsed
}

val, ok = config.Parameters[udpPreferenceLimit]
val, ok = cfg.Parameters[udpPreferenceLimit]
if ok {
parsed, err := strconv.Atoi(val)
if err != nil {
Expand Down Expand Up @@ -292,7 +329,7 @@ func clientFromUsernameAndPassword(krb5Login *krb5Login, cfg *config.Config) (*c

// 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)
data, err := os.ReadFile(krb5Login.KeytabFile)
if err != nil {
return nil, err
}
Expand Down
209 changes: 174 additions & 35 deletions integratedauth/krb5/krb5_test.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,171 @@
package krb5

import (
"os"
"strings"
"testing"

"github.com/microsoft/go-mssqldb/msdsn"
)

func TestReadKrb5ConfigHappyPath(t *testing.T) {
config := msdsn.Config{
User: "username",
Password: "password",
ServerSPN: "serverspn",
Parameters: map[string]string{
"krb5-configfile": "krb5-configfile",
"krb5-keytabfile": "krb5-keytabfile",
"krb5-credcachefile": "krb5-credcachefile",
"krb5-realm": "krb5-realm",
"krb5-dnslookupkdc": "false",
"krb5-udppreferencelimit": "1234",
tests := []struct {
name string
cfg msdsn.Config
validate func(t testing.TB, cfg msdsn.Config, actual *krb5Login)
confPath string
tabPath string
cachePath string
}{
{
name: "basic match",
cfg: msdsn.Config{
User: "username",
Password: "placeholderpassword",
ServerSPN: "serverspn",
Parameters: map[string]string{
"krb5-configfile": "krb5-configfile",
"krb5-keytabfile": "krb5-keytabfile",
"krb5-credcachefile": "krb5-credcachefile",
"krb5-realm": "krb5-realm",
"krb5-dnslookupkdc": "false",
"krb5-udppreferencelimit": "1234",
},
},
validate: basicConfigMatch,
},
{
name: "realm in user name",
cfg: msdsn.Config{
User: "username@realm.com",
Password: "placeholderpassword",
ServerSPN: "serverspn",
Parameters: map[string]string{
"krb5-configfile": "krb5-configfile",
"krb5-keytabfile": "krb5-keytabfile",
"krb5-credcachefile": "krb5-credcachefile",
"krb5-dnslookupkdc": "false",
"krb5-udppreferencelimit": "1234",
},
},
validate: func(t testing.TB, cfg msdsn.Config, actual *krb5Login) {
if actual.Realm != "realm.com" {
t.Errorf("Realm should have been copied from user name. Got: %s", actual.Realm)
}
if actual.UserName != "username" {
t.Errorf("UserName shouldn't include the realm. Got: %s", actual.UserName)
}
},
},
{
name: "using defaults for file paths",
cfg: msdsn.Config{
User: "username",
Password: "",
ServerSPN: "serverspn",
Parameters: map[string]string{
"krb5-dnslookupkdc": "false",
"krb5-udppreferencelimit": "1234",
"krb5-realm": "krb5-realm",
},
},
validate: func(t testing.TB, cfg msdsn.Config, actual *krb5Login) {
if actual.Krb5ConfigFile != `/etc/krb5.conf` {
t.Errorf("Expected default conf file path. Got: %s", actual.Krb5ConfigFile)
}
if actual.KeytabFile != `/etc/krb5.keytab` {
t.Errorf("Expecte keytab path from libdefaults. Got %s", actual.KeytabFile)
}
},
},
{
name: "Using environment variables",
confPath: `/etc/my.config`,
cachePath: `/tmp/mycache`,
tabPath: `/tmp/mytab`,
cfg: msdsn.Config{
User: "username",
Password: "",
ServerSPN: "serverspn",
Parameters: map[string]string{
"krb5-dnslookupkdc": "false",
"krb5-udppreferencelimit": "1234",
"krb5-realm": "krb5-realm",
},
},
validate: func(t testing.TB, cfg msdsn.Config, actual *krb5Login) {
if actual.Krb5ConfigFile != `/etc/my.config` {
t.Errorf("Expected conf file path from env var. Got: %s", actual.Krb5ConfigFile)
}
if actual.KeytabFile != `/tmp/mytab` {
t.Errorf("Expected tab file from env var. Got: %s", actual.KeytabFile)
}
if actual.CredCacheFile != `/tmp/mycache` {
t.Errorf("Expected cache file from env var. Got: %s", actual.CredCacheFile)
}
},
},
{
name: "no keytab from environment when user name is unset",
confPath: `/etc/my.config`,
cachePath: `/tmp/mycache`,
tabPath: `/tmp/mytab`,
cfg: msdsn.Config{
User: "",
Password: "",
ServerSPN: "serverspn",
Parameters: map[string]string{
"krb5-dnslookupkdc": "false",
"krb5-udppreferencelimit": "1234",
"krb5-realm": "krb5-realm",
},
},
validate: func(t testing.TB, cfg msdsn.Config, actual *krb5Login) {
if actual.Krb5ConfigFile != `/etc/my.config` {
t.Errorf("Expected conf file path from env var. Got: %s", actual.Krb5ConfigFile)
}
if actual.KeytabFile != "" {
t.Errorf("Expected no tab file. Got: %s", actual.KeytabFile)
}
if actual.CredCacheFile != `/tmp/mycache` {
t.Errorf("Expected cache file from env var. Got: %s", actual.CredCacheFile)
}
},
},
}
revert := mockFileExists()
defer revert()

actual, err := readKrb5Config(config)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if len(test.cachePath) > 0 {
cp := os.Getenv("KRB5CCNAME")
os.Setenv("KRB5CCNAME", test.cachePath)
defer os.Setenv("KRB5CCNAME", cp)
}
if len(test.confPath) > 0 {
cp := os.Getenv("KRB5_CONFIG")
os.Setenv("KRB5_CONFIG", test.confPath)
defer os.Setenv("KRB5_CONFIG", cp)
}
if len(test.tabPath) > 0 {
cp := os.Getenv("KRB5_KTNAME")
os.Setenv("KRB5_KTNAME", test.tabPath)
defer os.Setenv("KRB5_KTNAME", cp)
}

actual, err := readKrb5Config(test.cfg)

if err != nil {
t.Errorf("Unexpected error %v", err)
}
test.validate(t, test.cfg, actual)
})

if err != nil {
t.Errorf("Unexpected error %v", err)
}
}

func basicConfigMatch(t testing.TB, config msdsn.Config, actual *krb5Login) {
if actual.Krb5ConfigFile != config.Parameters[keytabConfigFile] {
t.Errorf("Expected Krb5ConfigFile %v, found %v", config.Parameters[keytabConfigFile], actual.Krb5ConfigFile)
}
Expand Down Expand Up @@ -64,7 +202,6 @@ func TestReadKrb5ConfigHappyPath(t *testing.T) {
t.Errorf("Expected UDPPreferenceLimit %v, found %v", 1234, actual.UDPPreferenceLimit)
}
}

func TestReadKrb5ConfigErrorCases(t *testing.T) {

tests := []struct {
Expand Down Expand Up @@ -270,7 +407,7 @@ func TestValidateKrb5LoginParams(t *testing.T) {
expectedError: ErrCredCacheFileDoesNotExist,
},
{
name: "no login method math",
name: "no login method match",
input: &krb5Login{},
expectedLoginMethod: none,
expectedError: ErrRequiredParametersMissing,
Expand All @@ -281,30 +418,32 @@ func TestValidateKrb5LoginParams(t *testing.T) {
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)
}
t.Run(tt.name, func(t *testing.T) {
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)
}
})
}
}

func mockFileExists() func() {
fileExists = func(filename string, errWhenFileNotFound error) (bool, error) {
if strings.Contains(filename, "exists") {
if strings.Contains(filename, "exists") || filename == `/etc/krb5.keytab` {
return true, nil
}

Expand Down
2 changes: 1 addition & 1 deletion version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "fmt"

// Update this variable with the release tag before pushing the tag
// This value is written to the prelogin and login7 packets during a new connection
const driverVersion = "v1.6.0"
const driverVersion = "v1.7.0"

func getDriverVersion(ver string) uint32 {
var majorVersion uint32
Expand Down