Skip to content

Commit

Permalink
Export auth interface to allow external implementations e.g. krb5 - V…
Browse files Browse the repository at this point in the history
…1-Candidate (#15)

* Export auth interface to allow external implementations

The original driver auth interface and implementations have been moved into their own packages and exported. There are two implementations within the driver, NTML and Windows kerberos.
As before the default on windows is the winsspi package and on, on Linux, NTLM.

The consuming application can now override this at runtime by calling mssql.SetAuthProvider(authProvider)

This allows the application to provide a custom implementation of the auth.Provider interface.
auth.Provider is in turn responsible for creating an instance of auth.Auth for the required authentication mechanism. windows, NTML etc.

* remove unused function and merge master changes

* add golang.org/x/crypto/md4 to appveyor.yml

* add unit tests for SetAuthProvider and getAuth

* Rename Auth to IntegratedAuthenticator

* change package references

* modular authentication and runtime registration

* fix windows sspi

* change test to use DeepEqual instead of struct equality

* whitespace change to assess appveyor issue

* fix linting issues

* bring the kerberos authenticator into the driver

* Revert "bring the kerberos authenticator into the driver"

This reverts commit 6437077.
  • Loading branch information
PeteBassettBet365 authored Aug 22, 2022
1 parent 574b528 commit ba5a4a0
Show file tree
Hide file tree
Showing 19 changed files with 651 additions and 218 deletions.
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ install:
- go env
- go get -u github.com/golang-sql/civil
- go get -u github.com/golang-sql/sqlexp

- go get -u golang.org/x/crypto/md4
build_script:
- go build

Expand Down
15 changes: 15 additions & 0 deletions auth_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// +build !windows

package mssql

import (
"github.com/microsoft/go-mssqldb/integratedauth"
// nolint importing the ntlm package causes it to be registered as an available authentication provider
_ "github.com/microsoft/go-mssqldb/integratedauth/ntlm"
)

func init() {
// we set the default authentication provider name here, rather than within each imported package,
// to force a known default. Go will order execution of init() calls but it is better to be explicit.
integratedauth.DefaultProviderName = "ntlm"
}
18 changes: 18 additions & 0 deletions auth_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// +build windows

package mssql

import (
"github.com/microsoft/go-mssqldb/integratedauth"

// nolint importing the ntlm package causes it to be registered as an available authentication provider
_ "github.com/microsoft/go-mssqldb/integratedauth/ntlm"
// nolint importing the winsspi package causes it to be registered as an available authentication provider
_ "github.com/microsoft/go-mssqldb/integratedauth/winsspi"
)

func init() {
// we set the default authentication provider name here, rather than within each imported package,
// to force a known default. Go will order execution of init() calls but it is better to be explicit.
integratedauth.DefaultProviderName = "winsspi"
}
4 changes: 2 additions & 2 deletions azuread/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type azureFedAuthConfig struct {

// parse returns a config based on an msdsn-style connection string
func parse(dsn string) (*azureFedAuthConfig, error) {
mssqlConfig, params, err := msdsn.Parse(dsn)
mssqlConfig, err := msdsn.Parse(dsn)
if err != nil {
return nil, err
}
Expand All @@ -62,7 +62,7 @@ func parse(dsn string) (*azureFedAuthConfig, error) {
mssqlConfig: mssqlConfig,
}

err = config.validateParameters(params)
err = config.validateParameters(mssqlConfig.Parameters)
if err != nil {
return nil, err
}
Expand Down
256 changes: 128 additions & 128 deletions azuread/configuration_test.go
Original file line number Diff line number Diff line change
@@ -1,128 +1,128 @@
//go:build go1.18
// +build go1.18

package azuread

import (
"testing"

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

func TestValidateParameters(t *testing.T) {
passphrase := "somesecret"
certificatepath := "/user/cert/cert.pfx"
appid := "applicationclientid=someguid"
certprop := "clientcertpath=" + certificatepath
tests := []struct {
name string
dsn string
expected *azureFedAuthConfig
}{
{
name: "no fed auth configured",
dsn: "server=someserver",
expected: &azureFedAuthConfig{fedAuthLibrary: mssql.FedAuthLibraryReserved},
},
{
name: "application with cert/key",
dsn: `sqlserver://service-principal-id%40tenant-id:[email protected]?fedauth=ActiveDirectoryApplication&` + certprop + "&" + appid,
expected: &azureFedAuthConfig{
fedAuthLibrary: mssql.FedAuthLibraryADAL,
clientID: "service-principal-id",
tenantID: "tenant-id",
certificatePath: certificatepath,
clientSecret: passphrase,
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
fedAuthWorkflow: ActiveDirectoryApplication,
applicationClientID: "someguid",
},
},
{
name: "application with cert/key missing tenant id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryApplication;user id=service-principal-id;password=somesecret;" + certprop + ";" + appid,
expected: &azureFedAuthConfig{
fedAuthLibrary: mssql.FedAuthLibraryADAL,
clientID: "service-principal-id",
certificatePath: certificatepath,
clientSecret: passphrase,
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
fedAuthWorkflow: ActiveDirectoryApplication,
applicationClientID: "someguid",
},
},
{
name: "application with secret",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryServicePrincipal;user id=service-principal-id@tenant-id;password=somesecret;",
expected: &azureFedAuthConfig{
clientID: "service-principal-id",
tenantID: "tenant-id",
clientSecret: passphrase,
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
fedAuthWorkflow: ActiveDirectoryServicePrincipal,
},
},
{
name: "user with password",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryPassword;user [email protected];password=somesecret;" + appid,
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
user: "[email protected]",
password: passphrase,
applicationClientID: "someguid",
fedAuthWorkflow: ActiveDirectoryPassword,
},
},
{
name: "managed identity without client id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryMSI",
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
fedAuthWorkflow: ActiveDirectoryMSI,
},
},
{
name: "managed identity with client id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;user id=identity-client-id",
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
clientID: "identity-client-id",
fedAuthWorkflow: ActiveDirectoryManagedIdentity,
},
},
{
name: "managed identity with resource id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;resource id=/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
resourceID: "/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
fedAuthWorkflow: ActiveDirectoryManagedIdentity,
},
},
}
for _, tst := range tests {
config, err := parse(tst.dsn)
if tst.expected == nil {
if err == nil {
t.Errorf("No error returned when error expected in test case '%s'", tst.name)
}
continue
}
if err != nil {
t.Errorf("Error returned when none expected in test case '%s': %v", tst.name, err)
continue
}
if tst.expected.fedAuthLibrary != mssql.FedAuthLibraryReserved {
if tst.expected.fedAuthLibrary == 0 {
tst.expected.fedAuthLibrary = mssql.FedAuthLibraryADAL
}
}
// mssqlConfig is not idempotent due to pointers in it, plus we aren't testing its correctness here
config.mssqlConfig = msdsn.Config{}
if *config != *tst.expected {
t.Errorf("Captured parameters do not match in test case '%s'. Expected:%+v, Actual:%+v", tst.name, tst.expected, config)
}
}

}
//go:build go1.18
// +build go1.18

package azuread

import (
"reflect"
"testing"

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

func TestValidateParameters(t *testing.T) {
passphrase := "somesecret"
certificatepath := "/user/cert/cert.pfx"
appid := "applicationclientid=someguid"
certprop := "clientcertpath=" + certificatepath
tests := []struct {
name string
dsn string
expected *azureFedAuthConfig
}{
{
name: "no fed auth configured",
dsn: "server=someserver",
expected: &azureFedAuthConfig{fedAuthLibrary: mssql.FedAuthLibraryReserved},
},
{
name: "application with cert/key",
dsn: `sqlserver://service-principal-id%40tenant-id:[email protected]?fedauth=ActiveDirectoryApplication&` + certprop + "&" + appid,
expected: &azureFedAuthConfig{
fedAuthLibrary: mssql.FedAuthLibraryADAL,
clientID: "service-principal-id",
tenantID: "tenant-id",
certificatePath: certificatepath,
clientSecret: passphrase,
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
fedAuthWorkflow: ActiveDirectoryApplication,
applicationClientID: "someguid",
},
},
{
name: "application with cert/key missing tenant id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryApplication;user id=service-principal-id;password=somesecret;" + certprop + ";" + appid,
expected: &azureFedAuthConfig{
fedAuthLibrary: mssql.FedAuthLibraryADAL,
clientID: "service-principal-id",
certificatePath: certificatepath,
clientSecret: passphrase,
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
fedAuthWorkflow: ActiveDirectoryApplication,
applicationClientID: "someguid",
},
},
{
name: "application with secret",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryServicePrincipal;user id=service-principal-id@tenant-id;password=somesecret;",
expected: &azureFedAuthConfig{
clientID: "service-principal-id",
tenantID: "tenant-id",
clientSecret: passphrase,
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
fedAuthWorkflow: ActiveDirectoryServicePrincipal,
},
},
{
name: "user with password",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryPassword;user [email protected];password=somesecret;" + appid,
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
user: "[email protected]",
password: passphrase,
applicationClientID: "someguid",
fedAuthWorkflow: ActiveDirectoryPassword,
},
},
{
name: "managed identity without client id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryMSI",
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
fedAuthWorkflow: ActiveDirectoryMSI,
},
},
{
name: "managed identity with client id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;user id=identity-client-id",
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
clientID: "identity-client-id",
fedAuthWorkflow: ActiveDirectoryManagedIdentity,
},
},
{
name: "managed identity with resource id",
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;resource id=/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
expected: &azureFedAuthConfig{
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
resourceID: "/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
fedAuthWorkflow: ActiveDirectoryManagedIdentity,
},
},
}
for _, tst := range tests {
config, err := parse(tst.dsn)
if tst.expected == nil {
if err == nil {
t.Errorf("No error returned when error expected in test case '%s'", tst.name)
}
continue
}
if err != nil {
t.Errorf("Error returned when none expected in test case '%s': %v", tst.name, err)
continue
}
if tst.expected.fedAuthLibrary != mssql.FedAuthLibraryReserved {
if tst.expected.fedAuthLibrary == 0 {
tst.expected.fedAuthLibrary = mssql.FedAuthLibraryADAL
}
}
// mssqlConfig is not idempotent due to pointers in it, plus we aren't testing its correctness here
config.mssqlConfig = msdsn.Config{}
if !reflect.DeepEqual(config, tst.expected) {
t.Errorf("Captured parameters do not match in test case '%s'. Expected:%+v, Actual:%+v", tst.name, tst.expected, config)
}
}
}
73 changes: 73 additions & 0 deletions integratedauth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package integratedauth

import (
"errors"
"fmt"

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

var (
providers map[string]Provider
DefaultProviderName string

ErrProviderCannotBeNil = errors.New("provider cannot be nil")
ErrProviderNameMustBePopulated = errors.New("provider name must be populated")
)

func init() {
providers = make(map[string]Provider)
}

// GetIntegratedAuthenticator calls the authProvider specified in the 'authenticator' connection string parameter, if supplied.
// Otherwise fails back to the DefaultProviderName implementation for the platform.
func GetIntegratedAuthenticator(config msdsn.Config) (IntegratedAuthenticator, error) {
authenticatorName, ok := config.Parameters["authenticator"]
if !ok {
provider, err := getProvider(DefaultProviderName)
if err != nil {
return nil, err
}

p, err := provider.GetIntegratedAuthenticator(config)
// we ignore the error in this case to force a fallback to sqlserver authentication.
// this preserves the original behaviour
if err != nil {
return nil, nil
}

return p, nil
}

provider, err := getProvider(authenticatorName)
if err != nil {
return nil, err
}

return provider.GetIntegratedAuthenticator(config)
}

func getProvider(name string) (Provider, error) {
provider, ok := providers[name]

if !ok {
return nil, fmt.Errorf("provider %v not found", name)
}

return provider, nil
}

// SetIntegratedAuthenticationProvider stores a named authentication provider. It should be called before any connections are created.
func SetIntegratedAuthenticationProvider(providerName string, p Provider) error {
if p == nil {
return ErrProviderCannotBeNil
}

if providerName == "" {
return ErrProviderNameMustBePopulated
}

providers[providerName] = p

return nil
}
Loading

0 comments on commit ba5a4a0

Please sign in to comment.