Skip to content

Commit 805f8c8

Browse files
Add SqlServer MSI Support (golang-migrate#591)
* MSI Auth for SQL Server Add MSI Auth option to SQL Server connection. Default if no password is provided in connection string. * Parse resource endpoint from server for msi update host name parsing to get just the resource endpoint for msi * Update go-mssqldb update go-mssqldb to resolve panic issue referenced here: denisenkom/go-mssqldb#642 * Update sqlserver.go switch from deprecated methods. NewServicePrincipalTokenFromManagedIdentity calls the two methods that are deprecated for us * Update sqlserver.go add useMsi param instead of looking for nil password * Update sqlserver readme Update sqlserver readme for msi auth. make useMsi a bit safer * Update sqlserver.go remove comment * Update database/sqlserver/README.md Co-authored-by: Keegan Campbell <[email protected]> * Update sqlserver.go refactor resource uri logic into its own method * Update sqlserver_test.go add tests for msi connection. can only test whether it fails with useMsi= true, or succeeds with useMsi=false * Update sqlserver.go check msi.EnsureFresh return value * Return error for multiple auth and move query filter move migrate.FilterCustomQuery(purl).String() into one line out of if/else. return error if both useMsi=true and password are passed * Update README.md update readme with warning for useMsi * Update sqlserver_test.go Update TestMsiFalse test case as now it should fail when useMsi=false and no password is provided Co-authored-by: Keegan Campbell <[email protected]>
1 parent 3dfb0ff commit 805f8c8

File tree

6 files changed

+153
-20
lines changed

6 files changed

+153
-20
lines changed

database/mysql/mysql.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,10 @@ import (
1414
nurl "net/url"
1515
"strconv"
1616
"strings"
17-
)
1817

19-
import (
2018
"github.com/go-sql-driver/mysql"
21-
"github.com/hashicorp/go-multierror"
22-
)
23-
24-
import (
2519
"github.com/golang-migrate/migrate/v4/database"
20+
"github.com/hashicorp/go-multierror"
2621
)
2722

2823
func init() {

database/sqlserver/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
| `dial+timeout` | | in seconds (default is 15), set to 0 for no timeout. |
1717
| `encrypt` | | `disable` - Data send between client and server is not encrypted. `false` - Data sent between client and server is not encrypted beyond the login packet (Default). `true` - Data sent between client and server is encrypted. |
1818
| `app+name` || The application name (default is go-mssqldb). |
19+
| `useMsi` | | `true` - Use Azure MSI Authentication for connecting to Sql Server. Must be running from an Azure VM/an instance with MSI enabled. `false` - Use password authentication (Default). See [here for Azure MSI Auth details](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-connect-msi). NOTE: Since this cannot be tested locally, this is not officially supported.
1920

2021
See https://github.com/denisenkom/go-mssqldb for full parameter list.
2122

database/sqlserver/sqlserver.go

+70-9
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import (
44
"context"
55
"database/sql"
66
"fmt"
7-
"go.uber.org/atomic"
87
"io"
98
"io/ioutil"
109
nurl "net/url"
10+
"strconv"
11+
"strings"
12+
13+
"go.uber.org/atomic"
1114

15+
"github.com/Azure/go-autorest/autorest/adal"
1216
mssql "github.com/denisenkom/go-mssqldb" // mssql support
1317
"github.com/golang-migrate/migrate/v4"
1418
"github.com/golang-migrate/migrate/v4/database"
@@ -23,10 +27,11 @@ func init() {
2327
var DefaultMigrationsTable = "schema_migrations"
2428

2529
var (
26-
ErrNilConfig = fmt.Errorf("no config")
27-
ErrNoDatabaseName = fmt.Errorf("no database name")
28-
ErrNoSchema = fmt.Errorf("no schema")
29-
ErrDatabaseDirty = fmt.Errorf("database is dirty")
30+
ErrNilConfig = fmt.Errorf("no config")
31+
ErrNoDatabaseName = fmt.Errorf("no database name")
32+
ErrNoSchema = fmt.Errorf("no schema")
33+
ErrDatabaseDirty = fmt.Errorf("database is dirty")
34+
ErrMultipleAuthOptionsPassed = fmt.Errorf("both password and useMsi=true were passed.")
3035
)
3136

3237
var lockErrorMap = map[mssql.ReturnStatus]string{
@@ -117,16 +122,49 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
117122
return ss, nil
118123
}
119124

120-
// Open a connection to the database
125+
// Open a connection to the database.
121126
func (ss *SQLServer) Open(url string) (database.Driver, error) {
122127
purl, err := nurl.Parse(url)
123128
if err != nil {
124129
return nil, err
125130
}
126131

127-
db, err := sql.Open("sqlserver", migrate.FilterCustomQuery(purl).String())
128-
if err != nil {
129-
return nil, err
132+
useMsiParam := purl.Query().Get("useMsi")
133+
useMsi := false
134+
if len(useMsiParam) > 0 {
135+
useMsi, err = strconv.ParseBool(useMsiParam)
136+
if err != nil {
137+
return nil, err
138+
}
139+
}
140+
141+
if _, isPasswordSet := purl.User.Password(); useMsi && isPasswordSet {
142+
return nil, ErrMultipleAuthOptionsPassed
143+
}
144+
145+
filteredURL := migrate.FilterCustomQuery(purl).String()
146+
147+
var db *sql.DB
148+
if useMsi {
149+
resource := getAADResourceFromServerUri(purl)
150+
tokenProvider, err := getMSITokenProvider(resource)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
connector, err := mssql.NewAccessTokenConnector(
156+
filteredURL, tokenProvider)
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
db = sql.OpenDB(connector)
162+
163+
} else {
164+
db, err = sql.Open("sqlserver", filteredURL)
165+
if err != nil {
166+
return nil, err
167+
}
130168
}
131169

132170
migrationsTable := purl.Query().Get("x-migrations-table")
@@ -339,3 +377,26 @@ func (ss *SQLServer) ensureVersionTable() (err error) {
339377

340378
return nil
341379
}
380+
381+
func getMSITokenProvider(resource string) (func() (string, error), error) {
382+
msi, err := adal.NewServicePrincipalTokenFromManagedIdentity(resource, nil)
383+
if err != nil {
384+
return nil, err
385+
}
386+
387+
return func() (string, error) {
388+
err := msi.EnsureFresh()
389+
if err != nil {
390+
return "", err
391+
}
392+
token := msi.OAuthToken()
393+
return token, nil
394+
}, nil
395+
}
396+
397+
// The sql server resource can change across clouds so get it
398+
// dynamically based on the server uri.
399+
// ex. <server name>.database.windows.net -> https://database.windows.net
400+
func getAADResourceFromServerUri(purl *nurl.URL) string {
401+
return fmt.Sprintf("%s%s", "https://", strings.Join(strings.Split(purl.Hostname(), ".")[1:], "."))
402+
}

database/sqlserver/sqlserver_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ func msConnectionString(host, port string) string {
3838
return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master", saPassword, host, port)
3939
}
4040

41+
func msConnectionStringMsiWithPassword(host, port string, useMsi bool) string {
42+
return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master&useMsi=%t", saPassword, host, port, useMsi)
43+
}
44+
45+
func msConnectionStringMsi(host, port string, useMsi bool) string {
46+
return fmt.Sprintf("sqlserver://sa@%v:%v?database=master&useMsi=%t", host, port, useMsi)
47+
}
48+
4149
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
4250
ip, port, err := c.Port(defaultPort)
4351
if err != nil {
@@ -218,3 +226,66 @@ func TestLockWorks(t *testing.T) {
218226
}
219227
})
220228
}
229+
230+
func TestMsiTrue(t *testing.T) {
231+
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
232+
ip, port, err := c.Port(defaultPort)
233+
if err != nil {
234+
t.Fatal(err)
235+
}
236+
237+
addr := msConnectionStringMsi(ip, port, true)
238+
p := &SQLServer{}
239+
_, err = p.Open(addr)
240+
if err == nil {
241+
t.Fatal("MSI should fail when not running in an Azure context.")
242+
}
243+
})
244+
}
245+
246+
func TestOpenWithPasswordAndMSI(t *testing.T) {
247+
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
248+
ip, port, err := c.Port(defaultPort)
249+
if err != nil {
250+
t.Fatal(err)
251+
}
252+
253+
addr := msConnectionStringMsiWithPassword(ip, port, true)
254+
p := &SQLServer{}
255+
_, err = p.Open(addr)
256+
if err == nil {
257+
t.Fatal("Open should fail when both password and useMsi=true are passed.")
258+
}
259+
260+
addr = msConnectionStringMsiWithPassword(ip, port, false)
261+
p = &SQLServer{}
262+
d, err := p.Open(addr)
263+
if err != nil {
264+
t.Fatal(err)
265+
}
266+
267+
defer func() {
268+
if err := d.Close(); err != nil {
269+
t.Error(err)
270+
}
271+
}()
272+
273+
dt.Test(t, d, []byte("SELECT 1"))
274+
})
275+
}
276+
277+
func TestMsiFalse(t *testing.T) {
278+
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
279+
ip, port, err := c.Port(defaultPort)
280+
if err != nil {
281+
t.Fatal(err)
282+
}
283+
284+
addr := msConnectionStringMsi(ip, port, false)
285+
p := &SQLServer{}
286+
_, err = p.Open(addr)
287+
if err == nil {
288+
t.Fatal("Open should fail since no password was passed and useMsi is false.")
289+
}
290+
})
291+
}

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/golang-migrate/migrate/v4
33
require (
44
cloud.google.com/go/spanner v1.18.0
55
cloud.google.com/go/storage v1.10.0
6+
github.com/Azure/go-autorest/autorest/adal v0.9.14
67
github.com/ClickHouse/clickhouse-go v1.4.3
78
github.com/apache/arrow/go/arrow v0.0.0-20210521153258-78c88a9f517b // indirect
89
github.com/aws/aws-sdk-go v1.17.7
@@ -12,7 +13,7 @@ require (
1213
github.com/cenkalti/backoff/v4 v4.0.2
1314
github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051
1415
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect
15-
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
16+
github.com/denisenkom/go-mssqldb v0.10.0
1617
github.com/dhui/dktest v0.3.4
1718
github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible
1819
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect

go.sum

+8-4
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
4848
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
4949
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
5050
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
51-
github.com/Azure/go-autorest/autorest/adal v0.9.2 h1:Aze/GQeAN1RRbGmnUJvUj+tFGBzFdIg3293/A9rbxC4=
5251
github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=
52+
github.com/Azure/go-autorest/autorest/adal v0.9.14 h1:G8hexQdV5D4khOXrWG2YuLCFKhWYmWD8bHYaXN5ophk=
53+
github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
5354
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
5455
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
56+
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
5557
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
58+
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
59+
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
5660
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
5761
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
5862
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -136,9 +140,8 @@ github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs
136140
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
137141
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
138142
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
139-
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec h1:NfhRXXFDPxcF5Cwo06DzeIaE7uuJtAUhsDwH3LNsjos=
140-
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
141-
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
143+
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
144+
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
142145
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
143146
github.com/dhui/dktest v0.3.4 h1:VbUEcaSP+U2/yUr9d2JhSThXYEnDlGabRSHe2rIE46E=
144147
github.com/dhui/dktest v0.3.4/go.mod h1:4m4n6lmXlmVfESth7mzdcv8nBI5mOb5UROPqjM02csU=
@@ -506,6 +509,7 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
506509
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
507510
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
508511
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
512+
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
509513
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
510514
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
511515
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=

0 commit comments

Comments
 (0)