Add azidentity-based AAD auth to sqlcmd (#15)

* basic azure auth support

* add tests for AAD auth

* pipeline fixes

* pipeline fix

* fix variable cyclic reference

* fix file name in pipeline

* merge coverage data

* fix missing quote

* remove unneeded scope

* fix test for azure auth
This commit is contained in:
David Shiflet 2021-09-30 15:10:40 -04:00 коммит произвёл GitHub
Родитель 44e5b52904
Коммит 63b8f6c791
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 464 добавлений и 93 удалений

Просмотреть файл

@ -1,80 +1,51 @@
variables:
# AZURE_CLIENT_SECRET and SQLPASSWORD must be defined as secret variables in the pipeline.
# AZURE_TENANT_ID and AZURE_CLIENT_ID are not expected to be secret variables, just regular variables
AZURECLIENTSECRET: $(AZURE_CLIENT_SECRET)
PASSWORD: $(SQLPASSWORD)
pool:
vmImage: 'ubuntu-latest'
steps:
- task: GoTool@0
inputs:
version: '1.16.5'
- task: Go@0
displayName: 'Go: get dependencies'
inputs:
command: 'get'
arguments: '-d'
workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd'
steps:
- template: include-install-go-tools.yml
- 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=$(PASSWORD) mcr.microsoft.com/mssql/server:2017-latest'
- task: Go@0
displayName: 'Go: install gotest.tools/gotestsum'
inputs:
command: 'custom'
customCommand: 'install'
arguments: 'gotest.tools/gotestsum@latest'
workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Go@0
displayName: 'Go: install github.com/axw/gocov/gocov'
inputs:
command: 'custom'
customCommand: 'install'
arguments: 'github.com/axw/gocov/gocov@latest'
workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Go@0
displayName: 'Go: install github.com/axw/gocov/gocov'
inputs:
command: 'custom'
customCommand: 'install'
arguments: 'github.com/AlekSi/gocov-xml@latest'
workingDirectory: '$(System.DefaultWorkingDirectory)'
#Your build pipeline references an undefined variables named SQLPASSWORD.
#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'
- script: |
~/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
workingDirectory: '$(Build.SourcesDirectory)'
displayName: 'run tests'
env:
SQLPASSWORD: $(SQLPASSWORD)
SQLCMDUSER: sa
SQLCMDPASSWORD: $(SQLPASSWORD)
continueOnError: true
- task: PublishTestResults@2
displayName: "Publish junit-style results"
inputs:
testResultsFiles: 'testresults.xml'
testResultsFormat: JUnit
searchFolder: '$(Build.SourcesDirectory)'
testRunTitle: 'SQL 2017 - $(Build.SourceBranchName)'
condition: always()
continueOnError: true
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
pathToSources: '$(Build.SourcesDirectory)'
summaryFileLocation: $(Build.SourcesDirectory)/**/coverage.xml
reportDirectory: $(Build.SourcesDirectory)/**/coverage
failIfCoverageEmpty: true
condition: always()
continueOnError: true
- template: include-runtests-linux.yml
parameters:
RunName: 'SQL 2017'
SQLCMDUSER: sa
SQLPASSWORD: $(PASSWORD)
- template: include-runtests-linux.yml
parameters:
RunName: 'SQL DB'
# AZURESERVER must be defined as a variable in the pipeline
SQLCMDSERVER: $(AZURESERVER)
AZURECLIENTSECRET: $(AZURECLIENTSECRET)
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
displayName: Merge coverage data
inputs:
reports: '"SQL 2017.coverage.xml";"SQL DB.coverage.xml"' # REQUIRED # The coverage reports that should be parsed (separated by semicolon). Globbing is supported.
targetdir: 'coverage' # REQUIRED # The directory where the generated report should be saved.
reporttypes: 'HtmlInline_AzurePipelines;Cobertura' # The output formats and scope (separated by semicolon) Values: Badges, Clover, Cobertura, CsvSummary, Html, HtmlChart, HtmlInline, HtmlInline_AzurePipelines, HtmlInline_AzurePipelines_Dark, HtmlSummary, JsonSummary, Latex, LatexSummary, lcov, MarkdownSummary, MHtml, PngChart, SonarQube, TeamCitySummary, TextSummary, Xml, XmlSummary
sourcedirs: '$(Build.SourcesDirectory)' # Optional directories which contain the corresponding source code (separated by semicolon). The source directories are used if coverage report contains classes without path information.
verbosity: 'Info' # The verbosity level of the log messages. Values: Verbose, Info, Warning, Error, Off
tag: '$(build.buildnumber)_#$(build.buildid)_$(Build.SourceBranchName)' # Optional tag or build version.
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
pathToSources: '$(Build.SourcesDirectory)'
summaryFileLocation: $(Build.SourcesDirectory)/coverage/*.xml
reportDirectory: $(Build.SourcesDirectory)/coverage
failIfCoverageEmpty: true
condition: always()
continueOnError: true
env:
disable.coverage.autogenerate: 'true'

Просмотреть файл

@ -0,0 +1,36 @@
steps:
- task: GoTool@0
inputs:
version: '1.16.5'
- task: Go@0
displayName: 'Go: get dependencies'
inputs:
command: 'get'
arguments: '-d'
workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd'
- task: Go@0
displayName: 'Go: install gotest.tools/gotestsum'
inputs:
command: 'custom'
customCommand: 'install'
arguments: 'gotest.tools/gotestsum@latest'
workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Go@0
displayName: 'Go: install github.com/axw/gocov/gocov'
inputs:
command: 'custom'
customCommand: 'install'
arguments: 'github.com/axw/gocov/gocov@latest'
workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Go@0
displayName: 'Go: install github.com/axw/gocov/gocov'
inputs:
command: 'custom'
customCommand: 'install'
arguments: 'github.com/AlekSi/gocov-xml@latest'
workingDirectory: '$(System.DefaultWorkingDirectory)'

Просмотреть файл

@ -0,0 +1,46 @@
parameters:
- name: RunName
type: string
- name: SQLCMDUSER
type: string
default: ''
- name: SQLPASSWORD
type: string
default: ''
- name: AZURECLIENTSECRET
type: string
default: ''
- name: SQLCMDSERVER
type: string
default: .
- name: SQLCMDDBNAME
type: string
default: ''
steps:
- script: |
~/go/bin/gotestsum --junitfile "${{ parameters.RunName }}.testresults.xml" -- ./... -coverprofile="${{ parameters.RunName }}.coverage.txt" -covermode count
~/go/bin/gocov convert "${{ parameters.RunName }}.coverage.txt" > "${{ parameters.RunName }}.coverage.json"
~/go/bin/gocov-xml < "${{ parameters.RunName }}.coverage.json" > "${{ parameters.RunName }}.coverage.xml"
mkdir -p coverage
workingDirectory: '$(Build.SourcesDirectory)'
displayName: 'run tests'
env:
SQLPASSWORD: ${{ parameters.SQLPASSWORD }}
SQLCMDUSER: ${{ parameters.SQLCMDUSER }}
SQLCMDPASSWORD: ${{ parameters.SQLPASSWORD }}
AZURE_TENANT_ID: $(AZURE_TENANT_ID)
AZURE_CLIENT_ID: $(AZURE_CLIENT_ID)
AZURE_CLIENT_SECRET: ${{ parameters.AZURECLIENTSECRET }}
SQLCMDSERVER: ${{ parameters.SQLCMDSERVER }}
SQLCMDDBNAME: ${{ parameters.SQLCMDDBNAME }}
continueOnError: true
- task: PublishTestResults@2
displayName: "Publish junit-style results"
inputs:
testResultsFiles: '"${{ parameters.RunName }}.coverage.xml"'
testResultsFormat: JUnit
searchFolder: '$(Build.SourcesDirectory)'
testRunTitle: '${{ parameters.RunName }} - $(Build.SourceBranchName)'
condition: always()
continueOnError: true

Просмотреть файл

@ -20,6 +20,60 @@ We will be implementing as many command line switches and behaviors as possible
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
- All commands must fit on one line, even `EXIT`. Interactive mode will not check for open parentheses or quotes for commands and prompt for successive lines. The native sqlcmd allows the query run by `EXIT(query)` to span multiple lines.
### Azure Active Directory Authentication
This version of sqlcmd supports a broader range of AAD authentication models, based on the [azidentity package](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity).
#### Command line
To use AAD auth, you can use one of two command line switches
`-G` is (mostly) compatible with its usage in the prior version of sqlcmd. If a user name and password are provided, it will authenticate using AAD Password authentication. If a user name is provided it will use AAD Interactive authentication which may display a web browser. If no user name or password is provided, it will use a DefaultAzureCredential which attempts to authenticate through a variety of mechanisms.
`--authentication-method=` can be used to specify one of the following authentication types.
`ActiveDirectoryDefault`
- For an overview of the types of authentication this mode will use, see (<https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity#defaultazurecredential>).
- Choose this method if your database automation scripts are intended to run in both local development environments and in a production deployment in Azure. You'll be able to use a client secret or an Azure CLI login on your development environment and a managed identity or client secret on your production deployment without changing the script.
- Setting environment variables AZURE_TENANT_ID, and AZURE_CLIENT_ID are necessary for DefaultAzureCredential to begin checking the environment configuration and look for one of the following additional environment variables in order to authenticate:
- Setting environment variable AZURE_CLIENT_SECRET configures the DefaultAzureCredential to choose ClientSecretCredential.
- Setting environment variable AZURE_CLIENT_CERTIFICATE_PATH configures the DefaultAzureCredential to choose ClientCertificateCredential if AZURE_CLIENT_SECRET is not set.
- Setting environment variable AZURE_USERNAME configures the DefaultAzureCredential to choose UsernamePasswordCredential if AZURE_CLIENT_SECRET and AZURE_CLIENT_CERTIFICATE_PATH are not set.
`ActiveDirectoryIntegrated`
This method is currently not implemented and will fall back to `ActiveDirectoryDefault`
`ActiveDirectoryPassword`
This method will authenticate using a user name and password. It will not work if MFA is required.
You provide the user name and password using the usual command line switches or SQLCMD environment variables.
Set `AZURE_TENANT_ID` environment variable to the tenant id of the server if not using the default tenant of the user.
`ActiveDirectoryInteractive`
This method will launch a web browser to authenticate the user.
Set `AZURE_TENANT_ID` environment variable to the tenant id of the server if not using the default.
`ActiveDirectoryManagedIdentity`
Use this method when running sqlcmd on an Azure VM that has either a system-assigned or user-assigned managed identity. If using a user-assigned managed identity, set the user name to the ID of the managed identity. If using a system-assigned identity, leave user name empty.
`ActiveDirectoryServicePrincipal`
This method authenticates the provided user name as a service principal id and the password as the client secret for the service principal. Set `AZURE_TENANT_ID` environment variable to the tenant id of the service principal.
### Environment variables for AAD auth
Some settings for AAD auth do not have command line inputs, and some environment variables are consumed directly by the `azidentity` package used by `sqlcmd`.
These environment variables can be set to configure some aspects of AAD auth and to bypass default behaviors. In addition to the variables listed above, the following are sqlcmd-specific and apply to multiple methods.
`SQLCMDAZURERESOURCE` - defines the URL of the Azure SQL database resource in the Azure cloud where the database resides. By default, `sqlcmd` attempts to match the DNS suffix of the server name with one of the well known Azure cloud DNS suffixes. If no match is found it uses `https://database.windows.net`.
`SQLCMDCLIENTID` - set this to the identifier of an application registered in your AAD which is authorized to authenticate to Azure SQL Database. Applies to `ActiveDirectoryInteractive` and `ActiveDirectoryPassword` methods.
### Packages
#### sqlcmd executable

Просмотреть файл

@ -20,7 +20,7 @@ type SQLCmdArguments struct {
// Whether to trust the server certificate on an encrypted connection
TrustServerCertificate bool `short:"C" help:"Implicitly trust the server certificate without validation."`
DatabaseName string `short:"d" help:"This option sets the sqlcmd scripting variable SQLCMDDBNAME. This parameter specifies the initial database. The default is your login's default-database property. If the database does not exist, an error message is generated and sqlcmd exits."`
UseTrustedConnection bool `short:"E" xor:"uid" help:"Uses a trusted connection instead of using a user name and password to sign in to SQL Server, ignoring any any environment variables that define user name and password."`
UseTrustedConnection bool `short:"E" xor:"uid, auth" help:"Uses a trusted connection instead of using a user name and password to sign in to SQL Server, ignoring any any environment variables that define user name and password."`
UserName string `short:"U" xor:"uid" help:"The login name or contained database user name. For contained database users, you must provide the database name option"`
// Files from which to read query text
InputFile []string `short:"i" xor:"input1, input2" type:"existingFile" help:"Identifies one or more files that contain batches of SQL statements. If one or more files do not exist, sqlcmd will exit. Mutually exclusive with -Q/-q."`
@ -31,7 +31,10 @@ type SQLCmdArguments struct {
Query string `short:"Q" xor:"input2" help:"Executes a query when sqlcmd starts and then immediately exits sqlcmd. Multiple-semicolon-delimited queries can be executed."`
Server string `short:"S" help:"[tcp:]server[\\instance_name][,port]Specifies the instance of SQL Server to which to connect. It sets the sqlcmd scripting variable SQLCMDSERVER."`
// Disable syscommands with a warning
DisableCmdAndWarn bool `short:"X" xor:"syscmd" help:"Disables commands that might compromise system security. Sqlcmd issues a warning and continues."`
DisableCmdAndWarn bool `short:"X" xor:"syscmd" help:"Disables commands that might compromise system security. Sqlcmd issues a warning and continues."`
// AuthenticationMethod is new for go-sqlcmd
AuthenticationMethod string `xor:"auth" help:"Specifies the SQL authentication method to use to connect to Azure SQL Database. One of:ActiveDirectoryDefault,ActiveDirectoryIntegrated,ActiveDirectoryPassword,ActiveDirectoryInteractive,ActiveDirectoryManagedIdentity,ActiveDirectoryServicePrincipal,SqlPassword"`
UseAad bool `short:"G" xor:"auth" help:"Tells sqlcmd to use Active Directory authentication. If no user name is provided, authentication method ActiveDirectoryDefault is used. If a password is provided, ActiveDirectoryPassword is used. Otherwise ActiveDirectoryInteractive is used."`
DisableVariableSubstitution bool `short:"x" help:"Causes sqlcmd to ignore scripting variables. This parameter is useful when a script contains many INSERT statements that may contain strings that have the same format as regular variables, such as $(variable_name)."`
Variables map[string]string `short:"v" help:"Creates a sqlcmd scripting variable that can be used in a sqlcmd script. Enclose the value in quotation marks if the value contains spaces. You can specify multiple var=values values. If there are errors in any of the values specified, sqlcmd generates an error message and then exits"`
}
@ -51,6 +54,26 @@ func newArguments() SQLCmdArguments {
var args SQLCmdArguments
func (a SQLCmdArguments) authenticationMethod(hasPassword bool) string {
if a.UseTrustedConnection {
return sqlcmd.NotSpecified
}
if a.UseAad {
switch {
case a.UserName == "":
return sqlcmd.ActiveDirectoryIntegrated
case hasPassword:
return sqlcmd.ActiveDirectoryPassword
default:
return sqlcmd.ActiveDirectoryInteractive
}
}
if a.AuthenticationMethod == "" {
return sqlcmd.NotSpecified
}
return a.AuthenticationMethod
}
func main() {
kong.Parse(&args)
vars := sqlcmd.InitializeVariables(!args.DisableCmdAndWarn)
@ -64,9 +87,20 @@ func main() {
// setVars initializes scripting variables from command line arguments
func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) {
varmap := map[string]func(*SQLCmdArguments) string{
sqlcmd.SQLCMDDBNAME: func(a *SQLCmdArguments) string { return a.DatabaseName },
sqlcmd.SQLCMDLOGINTIMEOUT: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDUSEAAD: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDDBNAME: func(a *SQLCmdArguments) string { return a.DatabaseName },
sqlcmd.SQLCMDLOGINTIMEOUT: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDUSEAAD: func(a *SQLCmdArguments) string {
if a.UseAad {
return "true"
}
switch a.AuthenticationMethod {
case sqlcmd.ActiveDirectoryIntegrated:
case sqlcmd.ActiveDirectoryInteractive:
case sqlcmd.ActiveDirectoryPassword:
return "true"
}
return ""
},
sqlcmd.SQLCMDWORKSTATION: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDSERVER: func(a *SQLCmdArguments) string { return a.Server },
sqlcmd.SQLCMDERRORLEVEL: func(a *SQLCmdArguments) string { return "" },
@ -124,6 +158,7 @@ func run(vars *sqlcmd.Variables) (int, error) {
}
s.Connect.UseTrustedConnection = args.UseTrustedConnection
s.Connect.TrustServerCertificate = args.TrustServerCertificate
s.Connect.AuthenticationMethod = args.authenticationMethod(s.Connect.Password != "")
s.Connect.DisableEnvironmentVariables = args.DisableCmdAndWarn
s.Connect.DisableVariableSubstitution = args.DisableVariableSubstitution
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(false)

Просмотреть файл

@ -105,6 +105,9 @@ func TestRunInputFiles(t *testing.T) {
args = newArguments()
args.InputFile = []string{"testdata/select100.sql", "testdata/select100.sql"}
args.OutputFile = o.Name()
if canTestAzureAuth() {
args.UseAad = true
}
vars := sqlcmd.InitializeVariables(!args.DisableCmdAndWarn)
vars.Set(sqlcmd.SQLCMDMAXVARTYPEWIDTH, "0")
setVars(vars, &args)
@ -127,6 +130,9 @@ func TestQueryAndExit(t *testing.T) {
args.Query = "SELECT '$(VAR1) $(VAR2)'"
args.OutputFile = o.Name()
args.Variables = map[string]string{"var2": "val2"}
if canTestAzureAuth() {
args.UseAad = true
}
vars := sqlcmd.InitializeVariables(!args.DisableCmdAndWarn)
vars.Set(sqlcmd.SQLCMDMAXVARTYPEWIDTH, "0")
vars.Set("VAR1", "100")
@ -140,3 +146,37 @@ func TestQueryAndExit(t *testing.T) {
assert.Equal(t, "100 val2"+sqlcmd.SqlcmdEol+sqlcmd.SqlcmdEol, string(bytes), "Incorrect output from run")
}
}
func TestAzureAuth(t *testing.T) {
if !canTestAzureAuth() {
t.Skip("AZURE auth environment variables are not set or server name is not an Azure DB name")
}
o, err := os.CreateTemp("", "sqlcmdmain")
assert.NoError(t, err, "os.CreateTemp")
defer os.Remove(o.Name())
defer o.Close()
args = newArguments()
args.Query = "SELECT 'AZURE'"
args.OutputFile = o.Name()
args.UseAad = true
vars := sqlcmd.InitializeVariables(!args.DisableCmdAndWarn)
vars.Set(sqlcmd.SQLCMDMAXVARTYPEWIDTH, "0")
setVars(vars, &args)
exitCode, err := run(vars)
assert.NoError(t, err, "run")
assert.Equal(t, 0, exitCode, "exitCode")
bytes, err := os.ReadFile(o.Name())
if assert.NoError(t, err, "os.ReadFile") {
assert.Equal(t, "AZURE"+sqlcmd.SqlcmdEol+sqlcmd.SqlcmdEol, string(bytes), "Incorrect output from run")
}
}
func canTestAzureAuth() bool {
tenant := os.Getenv("AZURE_TENANT_ID")
clientId := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
server := os.Getenv("SQLCMDSERVER")
return tenant != "" && clientId != "" && clientSecret != "" && strings.Contains(server, ".database.")
}

4
go.mod
Просмотреть файл

@ -3,7 +3,9 @@ module github.com/microsoft/go-sqlcmd
go 1.16
require (
github.com/alecthomas/kong v0.2.17
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/alecthomas/kong v0.2.18-0.20210621093454-54558f65e86f
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20210722231415-061457976a23 // indirect
github.com/denisenkom/go-mssqldb v0.10.0

33
go.sum
Просмотреть файл

@ -1,5 +1,13 @@
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/alecthomas/kong v0.2.17 h1:URDISCI96MIgcIlQyoCAlhOmrSw6pZScBNkctg8r0W0=
github.com/alecthomas/kong v0.2.17/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
github.com/alecthomas/kong v0.2.18-0.20210621093454-54558f65e86f h1:VgRM6/wqZIB1D9W3XMllm/wplTmPgI5yvCHUXEsmKps=
github.com/alecthomas/kong v0.2.18-0.20210621093454-54558f65e86f/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ=
@ -9,12 +17,16 @@ 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/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c h1:yE35fKFwcelIte3q5q1/cPiY7pI7vvf5/j/0ddxNCKs=
github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c/go.mod h1:9S/fKAutQ6wVHqm1jnp9D9sc5hu689s9AaTWFS92LaU=
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=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -22,12 +34,29 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
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-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
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-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 h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
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/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=

100
pkg/sqlcmd/azure_auth.go Normal file
Просмотреть файл

@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package sqlcmd
import (
"context"
"database/sql/driver"
"os"
"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"
)
const (
ActiveDirectoryDefault = "ActiveDirectoryDefault"
ActiveDirectoryIntegrated = "ActiveDirectoryIntegrated"
ActiveDirectoryPassword = "ActiveDirectoryPassword"
ActiveDirectoryInteractive = "ActiveDirectoryInteractive"
ActiveDirectoryManagedIdentity = "ActiveDirectoryManagedIdentity"
ActiveDirectoryServicePrincipal = "ActiveDirectoryServicePrincipal"
SqlPassword = "SqlPassword"
NotSpecified = "NotSpecified"
sqlClientId = "a94f9c62-97fe-4d19-b06d-472bed8d2bcf"
)
func azureTenantId() string {
t := os.Getenv("AZURE_TENANT_ID")
if t == "" {
t = "common"
}
return t
}
var resourceMap = map[string]string{
".database.chinacloudapi.cn": "https://database.chinacloudapi.cn/",
".database.cloudapi.de": "https://database.cloudapi.de/",
".database.usgovcloudapi.net": "https://database.usgovcloudapi.net/",
".database.windows.net": "https://database.windows.net/",
}
func (s *Sqlcmd) getResourceUrl() string {
resource := os.Getenv("SQLCMDAZURERESOURCE")
if resource == "" {
server, _, _, _ := s.vars.SQLCmdServer()
for k := range resourceMap {
if strings.HasSuffix(strings.ToLower(server), k) {
return resourceMap[k]
}
}
}
return "https://database.windows.net"
}
func getSqlClientId() string {
if clientId := os.Getenv("SQLCMDCLIENTID"); clientId != "" {
return clientId
}
return sqlClientId
}
func (s *Sqlcmd) GetTokenBasedConnection(connstr string, user string, password string) (driver.Connector, error) {
var cred azcore.TokenCredential
var err error
scope := ".default"
t := azureTenantId()
switch s.Connect.AuthenticationMethod {
case ActiveDirectoryDefault:
cred, err = azidentity.NewDefaultAzureCredential(nil)
case ActiveDirectoryInteractive:
cred, err = azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{TenantID: t, ClientID: getSqlClientId()})
case ActiveDirectoryPassword:
cred, err = azidentity.NewUsernamePasswordCredential(t, getSqlClientId(), user, password, nil)
case ActiveDirectoryManagedIdentity:
cred, err = azidentity.NewManagedIdentityCredential(user, nil)
case ActiveDirectoryServicePrincipal:
cred, err = azidentity.NewClientSecretCredential(t, user, password, nil)
default:
// no implementation of AAD Integrated yet
cred, err = azidentity.NewDefaultAzureCredential(nil)
}
if err != nil {
return nil, err
}
resourceUrl := s.getResourceUrl()
conn, err := mssql.NewAccessTokenConnector(connstr, func() (string, error) {
opts := policy.TokenRequestOptions{Scopes: []string{resourceUrl + scope}}
tk, err := cred.GetToken(context.Background(), opts)
if err != nil {
return "", err
}
return tk.Token, err
})
return conn, err
}

Просмотреть файл

@ -6,6 +6,7 @@ package sqlcmd
import (
"bufio"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"io"
@ -37,6 +38,7 @@ type ConnectSettings struct {
UseTrustedConnection bool
// TrustServerCertificate sets the TrustServerCertificate setting on the connection string
TrustServerCertificate bool
AuthenticationMethod string
// DisableEnvironmentVariables determines if sqlcmd resolves scripting variables from the process environment
DisableEnvironmentVariables bool
// DisableVariableSubstitution determines if scripting variables should be evaluated
@ -45,6 +47,13 @@ type ConnectSettings struct {
Password string
}
func (c ConnectSettings) authenticationMethod() string {
if c.AuthenticationMethod == "" {
return NotSpecified
}
return c.AuthenticationMethod
}
// Sqlcmd is the core processor for text lines.
//
// It accumulates non-command lines in a buffer and and sends command lines to the appropriate command runner.
@ -208,8 +217,8 @@ func (s *Sqlcmd) ConnectionString() (connectionString string, err error) {
Scheme: "sqlserver",
Path: instance,
}
useTrustedConnection := s.Connect.UseTrustedConnection || (s.vars.SQLCmdUser() == "" && !s.vars.UseAad())
if !useTrustedConnection {
if s.sqlAuthentication() {
connectionURL.User = url.UserPassword(s.vars.SQLCmdUser(), s.Connect.Password)
}
if port > 0 {
@ -257,21 +266,33 @@ func (s *Sqlcmd) ConnectDb(server string, user string, password string, nopw boo
}
}
var connector driver.Connector
// To determine whether to use Sql auth/windows auth/aad auth, compare the current ConnectSettings with the new parameters
// If sqlcmd was started with sql auth or windows auth, :connect will not switch to AAD
// if sqlcmd was started with AAD auth, it will remain in some variant of AAD auth depending on the user/password combination
useAad := !s.sqlAuthentication() && !s.integratedAuthentication()
if password == "" {
password = s.Connect.Password
}
if !useAad {
if user != "" {
connectionURL.User = url.UserPassword(user, password)
}
if user != "" {
connectionURL.User = url.UserPassword(user, password)
connector, err = mssql.NewConnector(connectionURL.String())
} else {
if user == "" {
user = s.vars.SQLCmdUser()
}
connector, err = s.GetTokenBasedConnection(connectionURL.String(), user, password)
}
connector, err := mssql.NewConnector(connectionURL.String())
if err != nil {
return err
}
db := sql.OpenDB(connector)
err = db.Ping()
if err != nil {
fmt.Fprintln(s.GetOutput(), err)
return err
}
// we got a good connection so we can update the Sqlcmd
@ -291,7 +312,9 @@ func (s *Sqlcmd) ConnectDb(server string, user string, password string, nopw boo
if e != nil {
panic("Unable to get user name")
}
s.Connect.UseTrustedConnection = true
if !useAad {
s.Connect.UseTrustedConnection = true
}
s.vars.Set(SQLCMDUSER, u.Username)
}
@ -384,6 +407,15 @@ func setupCloseHandler(s *Sqlcmd) {
}()
}
func (s *Sqlcmd) integratedAuthentication() bool {
return s.Connect.UseTrustedConnection || (s.vars.SQLCmdUser() == "" && s.Connect.authenticationMethod() == NotSpecified)
}
func (s *Sqlcmd) sqlAuthentication() bool {
return s.Connect.authenticationMethod() == SqlPassword ||
(!s.Connect.UseTrustedConnection && s.Connect.authenticationMethod() == NotSpecified && s.vars.SQLCmdUser() != "")
}
// runQuery runs the query and prints the results
// The return value is based on the first cell of the last column of the last result set.
// If it's numeric, it will be converted to int

Просмотреть файл

@ -9,6 +9,7 @@ import (
"fmt"
"os"
"os/user"
"strings"
"testing"
"github.com/google/uuid"
@ -84,7 +85,12 @@ set will be to localhost using Windows auth.
func TestSqlCmdConnectDb(t *testing.T) {
v := InitializeVariables(true)
s := &Sqlcmd{vars: v}
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
if canTestAzureAuth() {
s.Connect.AuthenticationMethod = ActiveDirectoryDefault
} else {
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
}
err := s.ConnectDb("", "", "", false)
if assert.NoError(t, err, "ConnectDb should succeed") {
sqlcmduser := os.Getenv(SQLCMDUSER)
@ -99,7 +105,11 @@ func TestSqlCmdConnectDb(t *testing.T) {
func ConnectDb() (*sql.DB, error) {
v := InitializeVariables(true)
s := &Sqlcmd{vars: v}
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
if canTestAzureAuth() {
s.Connect.AuthenticationMethod = ActiveDirectoryDefault
} else {
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
}
err := s.ConnectDb("", "", "", false)
return s.db, err
}
@ -225,7 +235,11 @@ func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) {
v := InitializeVariables(true)
v.Set(SQLCMDMAXVARTYPEWIDTH, "0")
s := New(nil, "", v)
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
if canTestAzureAuth() {
s.Connect.AuthenticationMethod = ActiveDirectoryDefault
} else {
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
}
s.Format = NewSQLCmdDefaultFormatter(true)
buf := &memoryBuffer{buf: new(bytes.Buffer)}
s.SetOutput(buf)
@ -238,7 +252,11 @@ func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) {
v := InitializeVariables(true)
v.Set(SQLCMDMAXVARTYPEWIDTH, "0")
s := New(nil, "", v)
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
if canTestAzureAuth() {
s.Connect.AuthenticationMethod = ActiveDirectoryDefault
} else {
s.Connect.Password = os.Getenv(SQLCMDPASSWORD)
}
s.Format = NewSQLCmdDefaultFormatter(true)
file, err := os.CreateTemp("", "sqlcmdout")
assert.NoError(t, err, "os.CreateTemp")
@ -247,3 +265,11 @@ func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) {
assert.NoError(t, err, "s.ConnectDB")
return s, file
}
func canTestAzureAuth() bool {
tenant := os.Getenv("AZURE_TENANT_ID")
clientId := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
server := os.Getenv("SQLCMDSERVER")
return tenant != "" && clientId != "" && clientSecret != "" && strings.Contains(server, ".database.")
}