From faffc32fa43a1e266df2e39054a670e03a84bdd6 Mon Sep 17 00:00:00 2001 From: Rudra Pratap Singh Nirvan Date: Fri, 20 May 2022 21:16:18 +0530 Subject: [PATCH] Connect to a DB by directly supplying auth token --- azuread/configuration.go | 395 +++++++++--------- azuread/configuration_test.go | 242 ++++++----- azuread/driver.go | 117 +++--- .../service_principal_authtoken.go | 58 +++ fedauth.go | 3 + 5 files changed, 453 insertions(+), 362 deletions(-) create mode 100644 examples/azuread-service-principal-authtoken/service_principal_authtoken.go diff --git a/azuread/configuration.go b/azuread/configuration.go index a6e30e9e..32ee70cd 100644 --- a/azuread/configuration.go +++ b/azuread/configuration.go @@ -1,192 +1,203 @@ -package azuread - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - mssql "github.com/denisenkom/go-mssqldb" - "github.com/denisenkom/go-mssqldb/msdsn" -) - -const ( - ActiveDirectoryDefault = "ActiveDirectoryDefault" - ActiveDirectoryIntegrated = "ActiveDirectoryIntegrated" - ActiveDirectoryPassword = "ActiveDirectoryPassword" - ActiveDirectoryInteractive = "ActiveDirectoryInteractive" - // ActiveDirectoryMSI is a synonym for ActiveDirectoryManagedIdentity - ActiveDirectoryMSI = "ActiveDirectoryMSI" - ActiveDirectoryManagedIdentity = "ActiveDirectoryManagedIdentity" - // ActiveDirectoryApplication is a synonym for ActiveDirectoryServicePrincipal - ActiveDirectoryApplication = "ActiveDirectoryApplication" - ActiveDirectoryServicePrincipal = "ActiveDirectoryServicePrincipal" - scopeDefaultSuffix = "/.default" -) - -type azureFedAuthConfig struct { - adalWorkflow byte - mssqlConfig msdsn.Config - // The detected federated authentication library - fedAuthLibrary int - fedAuthWorkflow string - // Service principal logins - clientID string - tenantID string - clientSecret string - certificatePath string - - // AD password/managed identity/interactive - user string - password string - applicationClientID string -} - -// parse returns a config based on an msdsn-style connection string -func parse(dsn string) (*azureFedAuthConfig, error) { - mssqlConfig, params, err := msdsn.Parse(dsn) - if err != nil { - return nil, err - } - config := &azureFedAuthConfig{ - fedAuthLibrary: mssql.FedAuthLibraryReserved, - mssqlConfig: mssqlConfig, - } - - err = config.validateParameters(params) - if err != nil { - return nil, err - } - - return config, nil -} - -func (p *azureFedAuthConfig) validateParameters(params map[string]string) error { - - fedAuthWorkflow, _ := params["fedauth"] - if fedAuthWorkflow == "" { - return nil - } - - p.fedAuthLibrary = mssql.FedAuthLibraryADAL - - p.applicationClientID, _ = params["applicationclientid"] - - switch { - case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryPassword): - if p.applicationClientID == "" { - return errors.New("applicationclientid parameter is required for " + ActiveDirectoryPassword) - } - p.adalWorkflow = mssql.FedAuthADALWorkflowPassword - p.user, _ = params["user id"] - p.password, _ = params["password"] - case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryIntegrated): - // Active Directory Integrated authentication is not fully supported: - // you can only use this by also implementing an a token provider - // and supplying it via ActiveDirectoryTokenProvider in the Connection. - p.adalWorkflow = mssql.FedAuthADALWorkflowIntegrated - case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryManagedIdentity) || strings.EqualFold(fedAuthWorkflow, ActiveDirectoryMSI): - // When using MSI, to request a specific client ID or user-assigned identity, - // provide the ID in the "user id" parameter - p.adalWorkflow = mssql.FedAuthADALWorkflowMSI - p.clientID, _ = splitTenantAndClientID(params["user id"]) - case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryApplication) || strings.EqualFold(fedAuthWorkflow, ActiveDirectoryServicePrincipal): - p.adalWorkflow = mssql.FedAuthADALWorkflowPassword - // Split the clientID@tenantID format - // If no tenant is provided we'll use the one from the server - p.clientID, p.tenantID = splitTenantAndClientID(params["user id"]) - if p.clientID == "" { - return errors.New("Must provide 'client id[@tenant id]' as username parameter when using ActiveDirectoryApplication authentication") - } - - p.clientSecret, _ = params["password"] - - p.certificatePath, _ = params["clientcertpath"] - - if p.certificatePath == "" && p.clientSecret == "" { - return errors.New("Must provide 'password' parameter when using ActiveDirectoryApplication authentication without cert/key credentials") - } - case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryDefault): - p.adalWorkflow = mssql.FedAuthADALWorkflowPassword - case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryInteractive): - if p.applicationClientID == "" { - return errors.New("applicationclientid parameter is required for " + ActiveDirectoryInteractive) - } - p.adalWorkflow = mssql.FedAuthADALWorkflowPassword - // user is an optional login hint - p.user, _ = params["user id"] - // we don't really have a password but we need to use some value. - p.adalWorkflow = mssql.FedAuthADALWorkflowPassword - - default: - return fmt.Errorf("Invalid federated authentication type '%s': expected one of %+v", - fedAuthWorkflow, - []string{ActiveDirectoryApplication, ActiveDirectoryServicePrincipal, ActiveDirectoryDefault, ActiveDirectoryIntegrated, ActiveDirectoryInteractive, ActiveDirectoryManagedIdentity, ActiveDirectoryMSI, ActiveDirectoryPassword}) - } - p.fedAuthWorkflow = fedAuthWorkflow - return nil -} - -func splitTenantAndClientID(user string) (string, string) { - // Split the user name into client id and tenant id at the @ symbol - at := strings.IndexRune(user, '@') - if at < 1 || at >= (len(user)-1) { - return user, "" - } - - return user[0:at], user[at+1:] -} - -func splitAuthorityAndTenant(authorityUrl string) (string, string) { - separatorIndex := strings.LastIndex(authorityUrl, "/") - tenant := authorityUrl[separatorIndex+1:] - authority := authorityUrl[:separatorIndex] - return authority, tenant -} - -func (p *azureFedAuthConfig) provideActiveDirectoryToken(ctx context.Context, serverSPN, stsURL string) (string, error) { - var cred azcore.TokenCredential - var err error - authority, tenant := splitAuthorityAndTenant(stsURL) - // client secret connection strings may override the server tenant - if p.tenantID != "" { - tenant = p.tenantID - } - scope := stsURL - if !strings.HasSuffix(serverSPN, scopeDefaultSuffix) { - scope = strings.TrimRight(serverSPN, "/") + scopeDefaultSuffix - } - - switch p.fedAuthWorkflow { - case ActiveDirectoryServicePrincipal, ActiveDirectoryApplication: - switch { - case p.certificatePath != "": - cred, err = azidentity.NewClientCertificateCredential(tenant, p.clientID, p.certificatePath, &azidentity.ClientCertificateCredentialOptions{Password: p.clientSecret}) - default: - cred, err = azidentity.NewClientSecretCredential(tenant, p.clientID, p.clientSecret, nil) - } - case ActiveDirectoryPassword: - cred, err = azidentity.NewUsernamePasswordCredential(tenant, p.applicationClientID, p.user, p.password, nil) - case ActiveDirectoryMSI, ActiveDirectoryManagedIdentity: - cred, err = azidentity.NewManagedIdentityCredential(p.clientID, nil) - case ActiveDirectoryInteractive: - cred, err = azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{AuthorityHost: authority, ClientID: p.applicationClientID}) - - default: - // Integrated just uses Default until azidentity adds Windows-specific authentication - cred, err = azidentity.NewDefaultAzureCredential(nil) - } - - if err != nil { - return "", err - } - opts := policy.TokenRequestOptions{Scopes: []string{scope}} - tk, err := cred.GetToken(ctx, opts) - if err != nil { - return "", err - } - return tk.Token, err -} +package azuread + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + + mssql "github.com/denisenkom/go-mssqldb" + "github.com/denisenkom/go-mssqldb/msdsn" +) + +const ( + ActiveDirectoryDefault = "ActiveDirectoryDefault" + ActiveDirectoryIntegrated = "ActiveDirectoryIntegrated" + ActiveDirectoryPassword = "ActiveDirectoryPassword" + ActiveDirectoryInteractive = "ActiveDirectoryInteractive" + // ActiveDirectoryMSI is a synonym for ActiveDirectoryManagedIdentity + ActiveDirectoryMSI = "ActiveDirectoryMSI" + ActiveDirectoryManagedIdentity = "ActiveDirectoryManagedIdentity" + // ActiveDirectoryApplication is a synonym for ActiveDirectoryServicePrincipal + ActiveDirectoryApplication = "ActiveDirectoryApplication" + ActiveDirectoryServicePrincipal = "ActiveDirectoryServicePrincipal" + ActiveDirectoryServicePrincipalAuthToken = "ActiveDirectoryServicePrincipalAuthToken" + scopeDefaultSuffix = "/.default" +) + +type azureFedAuthConfig struct { + adalWorkflow byte + mssqlConfig msdsn.Config + // The detected federated authentication library + fedAuthLibrary int + fedAuthWorkflow string + // Service principal logins + clientID string + tenantID string + clientSecret string + certificatePath string + + // AD password/managed identity/interactive + user string + password string + applicationClientID string +} + +// parse returns a config based on an msdsn-style connection string +func parse(dsn string) (*azureFedAuthConfig, error) { + mssqlConfig, params, err := msdsn.Parse(dsn) + if err != nil { + return nil, err + } + config := &azureFedAuthConfig{ + fedAuthLibrary: mssql.FedAuthLibraryReserved, + mssqlConfig: mssqlConfig, + } + + err = config.validateParameters(params) + if err != nil { + return nil, err + } + + return config, nil +} + +func (p *azureFedAuthConfig) validateParameters(params map[string]string) error { + + fedAuthWorkflow, _ := params["fedauth"] + if fedAuthWorkflow == "" { + return nil + } + + p.fedAuthLibrary = mssql.FedAuthLibraryADAL + + p.applicationClientID, _ = params["applicationclientid"] + + switch { + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryPassword): + if p.applicationClientID == "" { + return errors.New("applicationclientid parameter is required for " + ActiveDirectoryPassword) + } + p.adalWorkflow = mssql.FedAuthADALWorkflowPassword + p.user, _ = params["user id"] + p.password, _ = params["password"] + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryIntegrated): + // Active Directory Integrated authentication is not fully supported: + // you can only use this by also implementing an a token provider + // and supplying it via ActiveDirectoryTokenProvider in the Connection. + p.adalWorkflow = mssql.FedAuthADALWorkflowIntegrated + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryManagedIdentity) || strings.EqualFold(fedAuthWorkflow, ActiveDirectoryMSI): + // When using MSI, to request a specific client ID or user-assigned identity, + // provide the ID in the "user id" parameter + p.adalWorkflow = mssql.FedAuthADALWorkflowMSI + p.clientID, _ = splitTenantAndClientID(params["user id"]) + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryApplication) || strings.EqualFold(fedAuthWorkflow, ActiveDirectoryServicePrincipal): + p.adalWorkflow = mssql.FedAuthADALWorkflowPassword + // Split the clientID@tenantID format + // If no tenant is provided we'll use the one from the server + p.clientID, p.tenantID = splitTenantAndClientID(params["user id"]) + if p.clientID == "" { + return errors.New("Must provide 'client id[@tenant id]' as username parameter when using ActiveDirectoryApplication authentication") + } + + p.clientSecret, _ = params["password"] + + p.certificatePath, _ = params["clientcertpath"] + + if p.certificatePath == "" && p.clientSecret == "" { + return errors.New("Must provide 'password' parameter when using ActiveDirectoryApplication authentication without cert/key credentials") + } + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryDefault): + p.adalWorkflow = mssql.FedAuthADALWorkflowPassword + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryInteractive): + if p.applicationClientID == "" { + return errors.New("applicationclientid parameter is required for " + ActiveDirectoryInteractive) + } + p.adalWorkflow = mssql.FedAuthADALWorkflowPassword + // user is an optional login hint + p.user, _ = params["user id"] + // we don't really have a password but we need to use some value. + p.adalWorkflow = mssql.FedAuthADALWorkflowPassword + case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryServicePrincipalAuthToken): + p.fedAuthLibrary = mssql.FedAuthLibrarySecurityToken + p.adalWorkflow = mssql.FedAuthADALWorkflowNone + p.password, _ = params["password"] + + if p.password == "" { + return errors.New("Must provide 'password' parameter when using ActiveDirectoryApplicationAuthToken authentication") + } + default: + return fmt.Errorf("Invalid federated authentication type '%s': expected one of %+v", + fedAuthWorkflow, + []string{ActiveDirectoryApplication, ActiveDirectoryServicePrincipal, ActiveDirectoryDefault, ActiveDirectoryIntegrated, ActiveDirectoryInteractive, ActiveDirectoryManagedIdentity, ActiveDirectoryMSI, ActiveDirectoryPassword}) + } + p.fedAuthWorkflow = fedAuthWorkflow + return nil +} + +func splitTenantAndClientID(user string) (string, string) { + // Split the user name into client id and tenant id at the @ symbol + at := strings.IndexRune(user, '@') + if at < 1 || at >= (len(user)-1) { + return user, "" + } + + return user[0:at], user[at+1:] +} + +func splitAuthorityAndTenant(authorityUrl string) (string, string) { + separatorIndex := strings.LastIndex(authorityUrl, "/") + tenant := authorityUrl[separatorIndex+1:] + authority := authorityUrl[:separatorIndex] + return authority, tenant +} + +func (p *azureFedAuthConfig) provideActiveDirectoryToken(ctx context.Context, serverSPN, stsURL string) (string, error) { + var cred azcore.TokenCredential + var err error + authority, tenant := splitAuthorityAndTenant(stsURL) + // client secret connection strings may override the server tenant + if p.tenantID != "" { + tenant = p.tenantID + } + scope := stsURL + if !strings.HasSuffix(serverSPN, scopeDefaultSuffix) { + scope = strings.TrimRight(serverSPN, "/") + scopeDefaultSuffix + } + + switch p.fedAuthWorkflow { + case ActiveDirectoryServicePrincipal, ActiveDirectoryApplication: + switch { + case p.certificatePath != "": + cred, err = azidentity.NewClientCertificateCredential(tenant, p.clientID, p.certificatePath, &azidentity.ClientCertificateCredentialOptions{Password: p.clientSecret}) + default: + cred, err = azidentity.NewClientSecretCredential(tenant, p.clientID, p.clientSecret, nil) + } + case ActiveDirectoryServicePrincipalAuthToken: + return p.password, nil + case ActiveDirectoryPassword: + cred, err = azidentity.NewUsernamePasswordCredential(tenant, p.applicationClientID, p.user, p.password, nil) + case ActiveDirectoryMSI, ActiveDirectoryManagedIdentity: + cred, err = azidentity.NewManagedIdentityCredential(p.clientID, nil) + case ActiveDirectoryInteractive: + cred, err = azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{AuthorityHost: authority, ClientID: p.applicationClientID}) + + default: + // Integrated just uses Default until azidentity adds Windows-specific authentication + cred, err = azidentity.NewDefaultAzureCredential(nil) + } + + if err != nil { + return "", err + } + opts := policy.TokenRequestOptions{Scopes: []string{scope}} + tk, err := cred.GetToken(ctx, opts) + if err != nil { + return "", err + } + return tk.Token, err +} diff --git a/azuread/configuration_test.go b/azuread/configuration_test.go index c58b8b84..b926027c 100644 --- a/azuread/configuration_test.go +++ b/azuread/configuration_test.go @@ -1,116 +1,126 @@ -package azuread - -import ( - "testing" - - mssql "github.com/denisenkom/go-mssqldb" - "github.com/denisenkom/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:somesecret@someserver.database.windows.net?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 id=azure-ad-user@example.com;password=somesecret;" + appid, - expected: &azureFedAuthConfig{ - adalWorkflow: mssql.FedAuthADALWorkflowPassword, - user: "azure-ad-user@example.com", - 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, - }, - }, - } - 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) - } - } - -} +package azuread + +import ( + "testing" + + mssql "github.com/denisenkom/go-mssqldb" + "github.com/denisenkom/go-mssqldb/msdsn" +) + +func TestValidateParameters(t *testing.T) { + passphrase := "somesecret" + authToken := "some-auth-token" + 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:somesecret@someserver.database.windows.net?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 id=azure-ad-user@example.com;password=somesecret;" + appid, + expected: &azureFedAuthConfig{ + adalWorkflow: mssql.FedAuthADALWorkflowPassword, + user: "azure-ad-user@example.com", + 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: "application with auth token", + dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryServicePrincipalAuthToken;password=some-auth-token;", + expected: &azureFedAuthConfig{ + password: authToken, + adalWorkflow: mssql.FedAuthADALWorkflowNone, + fedAuthWorkflow: ActiveDirectoryServicePrincipalAuthToken, + }, + }, + } + 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'. \nExpected:%+v, \nActual:%+v", tst.name, tst.expected, config) + } + } + +} diff --git a/azuread/driver.go b/azuread/driver.go index fa417576..e93d4fb8 100644 --- a/azuread/driver.go +++ b/azuread/driver.go @@ -1,54 +1,63 @@ -package azuread - -import ( - "context" - "database/sql" - "database/sql/driver" - - mssql "github.com/denisenkom/go-mssqldb" -) - -// DriverName is the name used to register the driver -const DriverName = "azuresql" - -func init() { - sql.Register(DriverName, &Driver{}) -} - -// Driver wraps the underlying MSSQL driver, but configures the Azure AD token provider -type Driver struct { -} - -// Open returns a new connection to the database. -func (d *Driver) Open(dsn string) (driver.Conn, error) { - c, err := NewConnector(dsn) - if err != nil { - return nil, err - } - - return c.Connect(context.Background()) -} - -// NewConnector creates a new connector from a DSN. -// The returned connector may be used with sql.OpenDB. -func NewConnector(dsn string) (*mssql.Connector, error) { - - config, err := parse(dsn) - if err != nil { - return nil, err - } - return newConnectorConfig(config) -} - -// newConnectorConfig creates a Connector from config. -func newConnectorConfig(config *azureFedAuthConfig) (*mssql.Connector, error) { - if config.fedAuthLibrary == mssql.FedAuthLibraryADAL { - return mssql.NewActiveDirectoryTokenConnector( - config.mssqlConfig, config.adalWorkflow, - func(ctx context.Context, serverSPN, stsURL string) (string, error) { - return config.provideActiveDirectoryToken(ctx, serverSPN, stsURL) - }, - ) - } - return mssql.NewConnectorConfig(config.mssqlConfig), nil -} +package azuread + +import ( + "context" + "database/sql" + "database/sql/driver" + + mssql "github.com/denisenkom/go-mssqldb" +) + +// DriverName is the name used to register the driver +const DriverName = "azuresql" + +func init() { + sql.Register(DriverName, &Driver{}) +} + +// Driver wraps the underlying MSSQL driver, but configures the Azure AD token provider +type Driver struct { +} + +// Open returns a new connection to the database. +func (d *Driver) Open(dsn string) (driver.Conn, error) { + c, err := NewConnector(dsn) + if err != nil { + return nil, err + } + + return c.Connect(context.Background()) +} + +// NewConnector creates a new connector from a DSN. +// The returned connector may be used with sql.OpenDB. +func NewConnector(dsn string) (*mssql.Connector, error) { + + config, err := parse(dsn) + if err != nil { + return nil, err + } + return newConnectorConfig(config) +} + +// newConnectorConfig creates a Connector from config. +func newConnectorConfig(config *azureFedAuthConfig) (*mssql.Connector, error) { + switch config.fedAuthLibrary { + case mssql.FedAuthLibraryADAL: + return mssql.NewActiveDirectoryTokenConnector( + config.mssqlConfig, config.adalWorkflow, + func(ctx context.Context, serverSPN, stsURL string) (string, error) { + return config.provideActiveDirectoryToken(ctx, serverSPN, stsURL) + }, + ) + case mssql.FedAuthLibrarySecurityToken: + return mssql.NewSecurityTokenConnector( + config.mssqlConfig, + func(ctx context.Context) (string, error) { + return config.password, nil + }, + ) + default: + return mssql.NewConnectorConfig(config.mssqlConfig), nil + } +} diff --git a/examples/azuread-service-principal-authtoken/service_principal_authtoken.go b/examples/azuread-service-principal-authtoken/service_principal_authtoken.go new file mode 100644 index 00000000..4ba6cf76 --- /dev/null +++ b/examples/azuread-service-principal-authtoken/service_principal_authtoken.go @@ -0,0 +1,58 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + + _ "github.com/denisenkom/go-mssqldb" + "github.com/denisenkom/go-mssqldb/azuread" +) + +var ( + debug = flag.Bool("debug", true, "enable debugging") + password = flag.String("password", "", "the client secret for the app/client ID") + port *int = flag.Int("port", 1433, "the database port") + server = flag.String("server", "", "the database server") + database = flag.String("database", "", "the database name") +) + +func main() { + flag.Parse() + + if *debug { + fmt.Printf(" password:%s\n", *password) + fmt.Printf(" port:%d\n", *port) + fmt.Printf(" server:%s\n", *server) + fmt.Printf(" database:%s\n", *database) + } + + connString := fmt.Sprintf("server=%s;password=%s;port=%d;database=%s;fedauth=ActiveDirectoryServicePrincipalAuthToken;", *server, *password, *port, *database) + if *debug { + fmt.Printf(" connString:%s\n", connString) + } + conn, err := sql.Open(azuread.DriverName, connString) + if err != nil { + log.Fatal("Open connection failed:", err.Error()) + } + defer conn.Close() + + stmt, err := conn.Prepare("select 1, 'abc'") + if err != nil { + log.Fatal("Prepare failed:", err.Error()) + } + defer stmt.Close() + + row := stmt.QueryRow() + var somenumber int64 + var somechars string + err = row.Scan(&somenumber, &somechars) + if err != nil { + log.Fatal("Scan failed:", err.Error()) + } + fmt.Printf("somenumber:%d\n", somenumber) + fmt.Printf("somechars:%s\n", somechars) + + fmt.Printf("bye\n") +} diff --git a/fedauth.go b/fedauth.go index cd74cb52..3f4aa968 100644 --- a/fedauth.go +++ b/fedauth.go @@ -34,6 +34,9 @@ const ( // FedAuthADALWorkflowMSI uses the managed identity service to obtain a token FedAuthADALWorkflowMSI = 0x03 + + // FedAuthADALWorkflowNone does not need to obtain token + FedAuthADALWorkflowNone = 0x04 ) // newSecurityTokenConnector creates a new connector from a Config and a token provider.