From 361e5402d32b91074c3b1a354b4599b771eecb06 Mon Sep 17 00:00:00 2001 From: David Shiflet Date: Fri, 1 Oct 2021 13:32:34 -0400 Subject: [PATCH 1/4] add azuread package to implement AAD auth --- .gitignore | 1 + .pipelines/TestSql2017.yml | 4 +- README.md | 47 ++++++--- accesstokenconnector.go | 2 +- azuread/azuread_test.go | 63 +++++++++++ azuread/configuration.go | 192 ++++++++++++++++++++++++++++++++++ azuread/configuration_test.go | 116 ++++++++++++++++++++ azuread/driver.go | 54 ++++++++++ fedauth.go | 34 +++--- go.mod | 4 +- go.sum | 41 +++++++- tds.go | 14 +-- tds_login_test.go | 10 +- tds_test.go | 4 +- 14 files changed, 534 insertions(+), 52 deletions(-) create mode 100644 azuread/azuread_test.go create mode 100644 azuread/configuration.go create mode 100644 azuread/configuration_test.go create mode 100644 azuread/driver.go diff --git a/.gitignore b/.gitignore index 1f8b088f..3988a3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ coverage.json coverage.txt coverage.xml testresults.xml +.azureconnstr diff --git a/.pipelines/TestSql2017.yml b/.pipelines/TestSql2017.yml index 5422daab..33359434 100644 --- a/.pipelines/TestSql2017.yml +++ b/.pipelines/TestSql2017.yml @@ -39,7 +39,7 @@ steps: arguments: 'github.com/AlekSi/gocov-xml@latest' workingDirectory: '$(System.DefaultWorkingDirectory)' -#Your build pipeline references an undefined variables named SQLPASSWORD and HOST. +#Your build pipeline references an undefined variables named SQLPASSWORD and HOST and AZURESERVER_DSN. #Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 - task: Docker@2 @@ -57,6 +57,8 @@ steps: displayName: 'run tests' env: SQLPASSWORD: $(SQLPASSWORD) + AZURESERVER_DSN: $(AZURESERVER_DSN) + continueOnError: true - task: PublishTestResults@2 displayName: "Publish junit-style results" diff --git a/README.md b/README.md index 92e74b65..9a2a9bb3 100644 --- a/README.md +++ b/README.md @@ -112,30 +112,43 @@ Other supported formats are listed below. * `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" -### Azure Active Directory authentication - preview +### Azure Active Directory authentication + +Azure Active Directory authentication uses temporary authentication tokens to authenticate. +The `mssql` package does not provide an implementation to obtain tokens: instead, import the `azuread` package and use driver name `azuresql`. This driver uses [azidentity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#section-readme) to acquire tokens using a variety of credential types. + +The credential type is determined by the new `fedauth` connection string parameter. + +* `fedauth=ActiveDirectoryServicePrincipal` or `fedauth=ActiveDirectoryApplication` - authenticates using an Azure Active Directory application client ID and client secret or certificate. Implemented using [ClientSecretCredential or CertificateCredential](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity#authenticating-service-principals) + * `clientcertpath=;password=` or + * `password=` + * `user id=[@tenantid]` Note the `@tenantid` component can be omitted if the server's tenant is the same as the application's tenant. +* `fedauth=ActiveDirectoryPassword` - authenticates using a user name and password. + * `user id=username@domain` + * `password=` + * `applicationclientid=` - This guid identifies an Azure Active Directory enterprise application that the AAD admin has approved for accessing Azure SQL database resources in the tenant. This driver does not have an associated application id of its own. +* `fedauth=ActiveDirectoryDefault` - authenticates using a chained set of credentials. The chain is built from EnvironmentCredential -> ManagedIdentityCredential->AzureCLICredential. See [DefaultAzureCredential docs](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential) for instructions on setting up your host environment to use it. Using this option allows you to have the same connection string in a service deployment as on your interactive development machine. +* `fedauth=ActiveDirectoryManagedIdentity` or `fedauth=ActiveDirectoryMSI` - authenticates using a system-assigned or user-assigned Azure Managed Identity. + * `user id=` - optional id of user-assigned managed identity. If empty, system-assigned managed identity is used. +* `fedauth=ActiveDirectoryInteractive` - authenticates using credentials acquired from an external web browser. Only suitable for use with human interaction. + * `applicationclientid=` - This guid identifies an Azure Active Directory enterprise application that the AAD admin has approved for accessing Azure SQL database resources in the tenant. This driver does not have an associated application id of its own. -The configuration of functionality might change in the future. +```go -Azure Active Directory (AAD) access tokens are relatively short lived and need to be -valid when a new connection is made. Authentication is supported using a callback func that -provides a fresh and valid token using a connector: +import ( + "database/sql" + "net/url" -``` go + // Import the Azure AD driver module (also imports the regular driver package) + "github.com/denisenkom/go-mssqldb/azuread" +) -conn, err := mssql.NewAccessTokenConnector( - "Server=test.database.windows.net;Database=testdb", - tokenProvider) -if err != nil { - // handle errors in DSN +func ConnectWithMSI() (*sql.DB, error) { + return sql.Open(azuread.DriverName, "sqlserver://azuresql.database.windows.net?database=yourdb&fedauth=ActiveDirectoryMSI") } -db := sql.OpenDB(conn) ``` -Where `tokenProvider` is a function that returns a fresh access token or an error. None of these statements -actually trigger the retrieval of a token, this happens when the first statment is issued and a connection -is created. - ## Executing Stored Procedures To run a stored procedure, set the query text to the procedure name: @@ -306,6 +319,8 @@ Example: env SQLSERVER_DSN=sqlserver://user:pass@hostname/instance?database=test1 go test ``` +`AZURESERVER_DSN` environment variable provides the connection string for Azure Active Directory-based authentication. If it's not set the AAD test will be skipped. + ## Deprecated These features still exist in the driver, but they are are deprecated. diff --git a/accesstokenconnector.go b/accesstokenconnector.go index 80213d1e..7e9f95c2 100644 --- a/accesstokenconnector.go +++ b/accesstokenconnector.go @@ -22,7 +22,7 @@ func NewAccessTokenConnector(dsn string, tokenProvider func() (string, error)) ( } conn.fedAuthRequired = true - conn.fedAuthLibrary = fedAuthLibrarySecurityToken + conn.fedAuthLibrary = FedAuthLibrarySecurityToken conn.securityTokenProvider = func(ctx context.Context) (string, error) { return tokenProvider() } diff --git a/azuread/azuread_test.go b/azuread/azuread_test.go new file mode 100644 index 00000000..8afd4f69 --- /dev/null +++ b/azuread/azuread_test.go @@ -0,0 +1,63 @@ +package azuread + +import ( + "bufio" + "database/sql" + "io" + "os" + "testing" + + mssql "github.com/denisenkom/go-mssqldb" +) + +func TestAzureSqlAuth(t *testing.T) { + mssqlConfig := testConnParams(t) + + conn, err := newConnectorConfig(mssqlConfig) + if err != nil { + t.Fatalf("Unable to get a connector: %v", err) + } + db := sql.OpenDB(conn) + row := db.QueryRow("select 100, suser_sname()") + var val int + var user string + err = row.Scan(&val, &user) + if err != nil { + t.Fatalf("Unable to query the db: %v", err) + } + if val != 100 { + t.Fatalf("Got wrong value from query. Expected:100, Got: %d", val) + } + t.Logf("Got suser_sname value %s", user) + +} + +// returns parsed connection parameters derived from +// environment variables +func testConnParams(t testing.TB) *azureFedAuthConfig { + dsn := os.Getenv("AZURESERVER_DSN") + const logFlags = 127 + if dsn == "" { + // try loading connection string from file + f, err := os.Open(".azureconnstr") + if err == nil { + rdr := bufio.NewReader(f) + dsn, err = rdr.ReadString('\n') + if err != io.EOF && err != nil { + t.Fatal(err) + } + } + } + if dsn == "" { + t.Skip("no azure database connection string. set AZURESERVER_DSN environment variable or create .azureconnstr file") + } + config, err := parse(dsn) + if err != nil { + t.Skip("error parsing connection string " + dsn) + } + if config.fedAuthLibrary == mssql.FedAuthLibraryReserved { + t.Skip("Skipping azure test due to missing fedauth parameter " + dsn) + } + config.mssqlConfig.LogFlags = logFlags + return config +} diff --git a/azuread/configuration.go b/azuread/configuration.go new file mode 100644 index 00000000..a6e30e9e --- /dev/null +++ b/azuread/configuration.go @@ -0,0 +1,192 @@ +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 +} diff --git a/azuread/configuration_test.go b/azuread/configuration_test.go new file mode 100644 index 00000000..c58b8b84 --- /dev/null +++ b/azuread/configuration_test.go @@ -0,0 +1,116 @@ +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) + } + } + +} diff --git a/azuread/driver.go b/azuread/driver.go new file mode 100644 index 00000000..fa417576 --- /dev/null +++ b/azuread/driver.go @@ -0,0 +1,54 @@ +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 +} diff --git a/fedauth.go b/fedauth.go index 459c6641..cd74cb52 100644 --- a/fedauth.go +++ b/fedauth.go @@ -9,31 +9,31 @@ import ( // Federated authentication library affects the login data structure and message sequence. const ( - // fedAuthLibraryLiveIDCompactToken specifies the Microsoft Live ID Compact Token authentication scheme - fedAuthLibraryLiveIDCompactToken = 0x00 + // FedAuthLibraryLiveIDCompactToken specifies the Microsoft Live ID Compact Token authentication scheme + FedAuthLibraryLiveIDCompactToken = 0x00 - // fedAuthLibrarySecurityToken specifies a token-based authentication where the token is available + // FedAuthLibrarySecurityToken specifies a token-based authentication where the token is available // without additional information provided during the login sequence. - fedAuthLibrarySecurityToken = 0x01 + FedAuthLibrarySecurityToken = 0x01 - // fedAuthLibraryADAL specifies a token-based authentication where a token is obtained during the + // FedAuthLibraryADAL specifies a token-based authentication where a token is obtained during the // login sequence using the server SPN and STS URL provided by the server during login. - fedAuthLibraryADAL = 0x02 + FedAuthLibraryADAL = 0x02 - // fedAuthLibraryReserved is used to indicate that no federated authentication scheme applies. - fedAuthLibraryReserved = 0x7F + // FedAuthLibraryReserved is used to indicate that no federated authentication scheme applies. + FedAuthLibraryReserved = 0x7F ) // Federated authentication ADAL workflow affects the mechanism used to authenticate. const ( - // fedAuthADALWorkflowPassword uses a username/password to obtain a token from Active Directory - fedAuthADALWorkflowPassword = 0x01 + // FedAuthADALWorkflowPassword uses a username/password to obtain a token from Active Directory + FedAuthADALWorkflowPassword = 0x01 // fedAuthADALWorkflowPassword uses the Windows identity to obtain a token from Active Directory - fedAuthADALWorkflowIntegrated = 0x02 + FedAuthADALWorkflowIntegrated = 0x02 - // fedAuthADALWorkflowMSI uses the managed identity service to obtain a token - fedAuthADALWorkflowMSI = 0x03 + // FedAuthADALWorkflowMSI uses the managed identity service to obtain a token + FedAuthADALWorkflowMSI = 0x03 ) // newSecurityTokenConnector creates a new connector from a Config and a token provider. @@ -41,14 +41,14 @@ const ( // service specified and obtain the appropriate token, or return an error // to indicate why a token is not available. // The returned connector may be used with sql.OpenDB. -func newSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) { +func NewSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) { if tokenProvider == nil { return nil, errors.New("mssql: tokenProvider cannot be nil") } conn := NewConnectorConfig(config) conn.fedAuthRequired = true - conn.fedAuthLibrary = fedAuthLibrarySecurityToken + conn.fedAuthLibrary = FedAuthLibrarySecurityToken conn.securityTokenProvider = tokenProvider return conn, nil @@ -63,14 +63,14 @@ func newSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx conte // to indicate why a token is not available. // // The returned connector may be used with sql.OpenDB. -func newActiveDirectoryTokenConnector(config msdsn.Config, adalWorkflow byte, tokenProvider func(ctx context.Context, serverSPN, stsURL string) (string, error)) (*Connector, error) { +func NewActiveDirectoryTokenConnector(config msdsn.Config, adalWorkflow byte, tokenProvider func(ctx context.Context, serverSPN, stsURL string) (string, error)) (*Connector, error) { if tokenProvider == nil { return nil, errors.New("mssql: tokenProvider cannot be nil") } conn := NewConnectorConfig(config) conn.fedAuthRequired = true - conn.fedAuthLibrary = fedAuthLibraryADAL + conn.fedAuthLibrary = FedAuthLibraryADAL conn.fedAuthADALWorkflow = adalWorkflow conn.adalTokenProvider = tokenProvider diff --git a/go.mod b/go.mod index ebc02ab8..d0d623e8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/denisenkom/go-mssqldb go 1.11 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe - golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 ) diff --git a/go.sum b/go.sum index 1887801b..67630618 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,42 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 h1:lhSJz9RMbJcTgxifR1hUNJnn6CNYtbgEDtQV22/9RBA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0 h1:OYa9vmRX2XC5GXRAzeggG12sF/z5D9Ahtdm9EJ00WN4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 h1:v9p9TfTbf7AwNb5NYQt7hI41IfPoLFiFkLtb+bmGjT0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tds.go b/tds.go index dbc9b211..e1b07d9e 100644 --- a/tds.go +++ b/tds.go @@ -386,7 +386,7 @@ func (e *featureExtFedAuth) toBytes() []byte { var d []byte switch e.FedAuthLibrary { - case fedAuthLibrarySecurityToken: + case FedAuthLibrarySecurityToken: d = make([]byte, 5) d[0] = options @@ -400,7 +400,7 @@ func (e *featureExtFedAuth) toBytes() []byte { d = append(d, e.Nonce...) } - case fedAuthLibraryADAL: + case FedAuthLibraryADAL: d = []byte{options, e.ADALWorkflow} } @@ -930,7 +930,7 @@ func preparePreloginFields(p msdsn.Config, fe *featureExtFedAuth) map[uint8][]by preloginMARS: {0}, // MARS disabled } - if fe.FedAuthLibrary != fedAuthLibraryReserved { + if fe.FedAuthLibrary != FedAuthLibraryReserved { fields[preloginFEDAUTHREQUIRED] = []byte{1} } @@ -948,7 +948,7 @@ func interpretPreloginResponse(p msdsn.Config, fe *featureExtFedAuth, fields map // We need to be able to echo the value back to the server fe.FedAuthEcho = fedAuthSupport[0] != 0 - } else if fe.FedAuthLibrary != fedAuthLibraryReserved { + } else if fe.FedAuthLibrary != FedAuthLibraryReserved { return 0, fmt.Errorf("federated authentication is not supported by the server") } @@ -980,7 +980,7 @@ func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger Cont TypeFlags: typeFlags, } switch { - case fe.FedAuthLibrary == fedAuthLibrarySecurityToken: + case fe.FedAuthLibrary == FedAuthLibrarySecurityToken: if uint64(p.LogFlags)&logDebug != 0 { logger.Log(ctx, msdsn.LogDebug, "Starting federated authentication using security token") } @@ -995,7 +995,7 @@ func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger Cont l.FeatureExt.Add(fe) - case fe.FedAuthLibrary == fedAuthLibraryADAL: + case fe.FedAuthLibrary == FedAuthLibraryADAL: if uint64(p.LogFlags)&logDebug != 0 { logger.Log(ctx, msdsn.LogDebug, "Starting federated authentication using ADAL") } @@ -1096,7 +1096,7 @@ initiate_connection: } fedAuth := &featureExtFedAuth{ - FedAuthLibrary: fedAuthLibraryReserved, + FedAuthLibrary: FedAuthLibraryReserved, } if c.fedAuthRequired { fedAuth.FedAuthLibrary = c.fedAuthLibrary diff --git a/tds_login_test.go b/tds_login_test.go index f0ff1730..08f3e3e5 100644 --- a/tds_login_test.go +++ b/tds_login_test.go @@ -166,7 +166,7 @@ func TestLoginWithSecurityTokenAuth(t *testing.T) { if err != nil { t.Fatal(err) } - conn, err := newSecurityTokenConnector(config, + conn, err := NewSecurityTokenConnector(config, func(ctx context.Context) (string, error) { return "", nil }, @@ -227,9 +227,9 @@ func TestLoginWithADALUsernamePasswordAuth(t *testing.T) { if err != nil { t.Fatal(err) } - conn, err := newActiveDirectoryTokenConnector( + conn, err := NewActiveDirectoryTokenConnector( config, - fedAuthADALWorkflowPassword, + FedAuthADALWorkflowPassword, func(ctx context.Context, serverSPN, stsURL string) (string, error) { return "", nil }, @@ -301,9 +301,9 @@ func TestLoginWithADALManagedIdentityAuth(t *testing.T) { if err != nil { t.Fatal(err) } - conn, err := newActiveDirectoryTokenConnector( + conn, err := NewActiveDirectoryTokenConnector( config, - fedAuthADALWorkflowMSI, + FedAuthADALWorkflowMSI, func(ctx context.Context, serverSPN, stsURL string) (string, error) { return "", nil }, diff --git a/tds_test.go b/tds_test.go index 2a0e4b67..bfce86c5 100644 --- a/tds_test.go +++ b/tds_test.go @@ -41,7 +41,7 @@ func TestConstantsDefined(t *testing.T) { } for _, i := range []int{ - fedAuthLibraryLiveIDCompactToken, fChangePassword, fSendYukonBinaryXML, + FedAuthLibraryLiveIDCompactToken, fChangePassword, fSendYukonBinaryXML, } { if i < 0 { t.Fail() @@ -120,7 +120,7 @@ func TestSendLoginWithFeatureExt(t *testing.T) { ClientLCID: 0x204, } login.FeatureExt.Add(&featureExtFedAuth{ - FedAuthLibrary: fedAuthLibrarySecurityToken, + FedAuthLibrary: FedAuthLibrarySecurityToken, FedAuthToken: "fedauthtoken", }) err := sendLogin(buf, &login) From 61fdcc8928a85fb8c44237483a6101e1f4679f26 Mon Sep 17 00:00:00 2001 From: David Shiflet Date: Fri, 1 Oct 2021 13:43:33 -0400 Subject: [PATCH 2/4] run tests in subfolders --- .pipelines/TestSql2017.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/TestSql2017.yml b/.pipelines/TestSql2017.yml index 33359434..26614763 100644 --- a/.pipelines/TestSql2017.yml +++ b/.pipelines/TestSql2017.yml @@ -49,7 +49,7 @@ steps: arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(SQLPASSWORD) mcr.microsoft.com/mssql/server:2017-latest' - script: | - ~/go/bin/gotestsum --junitfile testresults.xml -- -coverprofile=coverage.txt -covermode count + ~/go/bin/gotestsum --junitfile testresults.xml -- -coverprofile=coverage.txt -covermode count ./... ~/go/bin/gocov convert coverage.txt > coverage.json ~/go/bin/gocov-xml < coverage.json > coverage.xml mkdir coverage From afd1204ece9efd1b33ec90db35da0923089643b4 Mon Sep 17 00:00:00 2001 From: David Shiflet Date: Fri, 1 Oct 2021 13:53:18 -0400 Subject: [PATCH 3/4] fix local server dsn --- .pipelines/TestSql2017.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.pipelines/TestSql2017.yml b/.pipelines/TestSql2017.yml index 26614763..2a052e96 100644 --- a/.pipelines/TestSql2017.yml +++ b/.pipelines/TestSql2017.yml @@ -2,6 +2,8 @@ pool: vmImage: 'ubuntu-latest' trigger: none +variables: + TESTPASSWORD: $(SQLPASSWORD) steps: - task: GoTool@0 @@ -39,14 +41,14 @@ steps: arguments: 'github.com/AlekSi/gocov-xml@latest' workingDirectory: '$(System.DefaultWorkingDirectory)' -#Your build pipeline references an undefined variables named SQLPASSWORD and HOST and AZURESERVER_DSN. +#Your build pipeline references an undefined variables named SQLPASSWORD and AZURESERVER_DSN. #Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 - task: Docker@2 displayName: 'Run SQL 2017 docker image' inputs: command: run - arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(SQLPASSWORD) mcr.microsoft.com/mssql/server:2017-latest' + arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(TESTPASSWORD) mcr.microsoft.com/mssql/server:2017-latest' - script: | ~/go/bin/gotestsum --junitfile testresults.xml -- -coverprofile=coverage.txt -covermode count ./... @@ -56,7 +58,7 @@ steps: workingDirectory: '$(Build.SourcesDirectory)' displayName: 'run tests' env: - SQLPASSWORD: $(SQLPASSWORD) + SQLSERVER_DSN: 'server=.;user id=sa;password=$(TESTPASSWORD)' AZURESERVER_DSN: $(AZURESERVER_DSN) continueOnError: true From fe11108b39d6ad7bd75742cddace64cdd034721d Mon Sep 17 00:00:00 2001 From: David Shiflet Date: Fri, 1 Oct 2021 13:59:14 -0400 Subject: [PATCH 4/4] don't print dsn in tests --- azuread/azuread_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azuread/azuread_test.go b/azuread/azuread_test.go index 8afd4f69..ba7de672 100644 --- a/azuread/azuread_test.go +++ b/azuread/azuread_test.go @@ -53,10 +53,10 @@ func testConnParams(t testing.TB) *azureFedAuthConfig { } config, err := parse(dsn) if err != nil { - t.Skip("error parsing connection string " + dsn) + t.Skip("error parsing connection string ") } if config.fedAuthLibrary == mssql.FedAuthLibraryReserved { - t.Skip("Skipping azure test due to missing fedauth parameter " + dsn) + t.Skip("Skipping azure test due to missing fedauth parameter ") } config.mssqlConfig.LogFlags = logFlags return config