Closed cmuncan closed 3 years ago
I did some research on this, but I don't have a solution yet.
Based on the experimental code path we do the following:
$serverName = 'aaaaaaaaaaa.bbbbbbbbb.database.windows.net'
$connInfo = New-Object -TypeName Microsoft.SqlServer.Management.Common.SqlConnectionInfo -ArgumentList $serverName
$srvConn = New-Object -TypeName Microsoft.SqlServer.Management.Common.ServerConnection -ArgumentList $connInfo
$server = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server -ArgumentList $srvConn
$server.ConnectionContext.SqlConnectionObject.Open()
The object $connInfo has a property named "AccessToken" with type "Microsoft.SqlServer.Management.Common.IRenewableToken" and thus only takes a renewable token and not an access token generated with Get-AzAccessToken -ResourceUrl https://database.windows.net
.
We have a command New-DbaAzAccessToken
to generate Azure oauth2 tokens and this command is used in the old code path. But to generate a renewable token it needs a credential to get UserID and ClientSecret from.
The workaround uses an access token which is generated with Get-AzAccessToken
and is valid for one hour only. The Command Invoke-Sqlcmd
from the module SqlServer
is accepting such an access token. Because of beeing closed source I don't know how this token is used inside of the command.
@potatoqualitee : Can you use your contacts to Microsoft to get some insight in the internal usage of -AccessToken
inside of Invoke-Sqlcmd
?
Maybe @shueybubbles can help?
There are a variety of issues to sort out here. It looks like you are mixing up AAD Password auth and AAD Interactive (aka MFA). I don't know how Connect-DbaInstance works, but successful use of AAD auth for SQL depends on several factors.
The main factor is which version of SMO you have. If you are using a 161 version built on Microsoft.Data.SqlClient, you can pretty much rely on the client to do all the heavy lifting for you. M.D.S has its own fully managed stack to implement the various AAD auth types. All you have to do is make sure its dependencies like Microsoft.Identity.Client are installed with it and that you set the auth type you want in the connection string.
If you are using an older SMO based on System.Data.SqlClient, or if you prefer to manage the Azure authentication separately altogether, then you have a few options.
System.Data.SqlClient only handles Active Directory Password
and Active Directory Integrated
auth for SQL connections if you have also installed one of the newer SQL client drivers like msodbcsql 17 or have installed SSMS. That implementation is practically obsolete, though, as it doesn't support many conditional access policies like device id-based.
Fully interactive/MFA support for AAD can be achieved a few ways. Given that you are using Get-AzToken
to login, you can simply set the SqlConnection.AccessToken
before calling Open on the connection. You could also implement IRenewableToken
and have your implementation simply return the token returned by Get-Aztoken
. The former implementation is ok if you know the connection isn't going to stay open longer than an hour. The latter is similar to what SSMS 17 does.
With System.Data.SqlClient, though, to fully support silent token refresh and a more streamlined experience, you need to implement SqlAuthenticationProvider
Again your implementation could just provide the same tokens returned by Get-AzToken
.
SSMS 18 provides its own implementation of AAD interactive this way. Implementing this provider will ensure that idle connections that are still in the Open
state can be re-established.
There's at least one implemention of SqlAuthenticationProvider available in a nuget package that might be interesting for you, if you are still using System.Data.SqlClient. See https://azure.microsoft.com/en-us/blog/securing-azure-sql-databases-with-managed-identities-just-got-easier/
Thanks for the quick response.
I think one problem might be that dbatools still uses an old version of the smo. The file properties show "15.0.18218.0" - is that the version?
Is there any documentation with the constructor of Microsoft.SqlServer.Management.Common.IRenewableToken?
It's an interface so it doesn't define a constructor. The basic doc is at https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.common.irenewabletoken?view=sql-smo-160
If you provide an implementation to ServerConnection
, it will call your GetAccessToken
method before opening the SqlConnection
.
Thanks, now I think I understand. But that is too much .NET coding and beyond my skills. @potatoqualitee : Who in the team could help here?
I think the best "connected" here in this regards are @potatoqualitee and @wsmelton
Support for MFA was pulled from the module year or so back because we had issues including the required DDL files for Azure. They had conflict with VS Code that caused users issues and it has not been revisited since then.
SMO itself does not handle direct auth to Azure AD with MFA. I'm not aware it can handle the Azure SQL auth options either, that is something SSMS deals with if I recall.
thanks, everyone. i have a few things backlogged that i have to work on, including integrating some much needed work done by @niphlod. I'll take a look at replacing SMO to see if it can work. @wsmelton is right, we removed mfa due to dll conflicts https://github.com/sqlcollaborative/dbatools/pull/5873
Only because I've helped a developer support this authentication type in our software at work but in order to fully support Integrated Azure Active Directory
and the other authentication requires some .NET magic to be done. SMO itself does not include support for these authentication types explicitly because it is part of Azure AD and not a authentication mechanism owned by SQL Server. (Hence that DLL we tried to use was published by Microsoft.).
I would note that right now, last I had checked (it's been a while), the sqlserver
module does not support these authentication types either.
Have a look at the workaround the original poster is using:
$Cred = New-Object System.Management.Automation.PSCredential -ArgumentList ($credid, $credpass)
$ConnectedAz = Connect-AzAccount -Credential $Cred -WarningAction SilentlyContinue
$token = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token
$instance = "aaaaa.bbbbb.database.windows.net"
Invoke-Sqlcmd -ServerInstance $instance -Database master -query "select [name] from [sys].[databases]" -AccessToken $token
So Invoke-Sqlcmd
is accepting an accesstoken generated by Get-AzAccessToken
. I have tried that myself - it works. But Invoke-Sqlcmd
is the only command from SqlServer that supports this. Sadly it is closed source - I would really like to see the source code...
That is where the use of IRenewableToken
comes into play, if I recall. Want to say there is an issue or PR we used to work through some of this in the repo but I've forgotten a good portion of it and would have to go test it again.
But the accesstoken in $token is not renewable, but only a string that is valid for one hour.
You should be able to set $server.ConnectionContext.SqlConnectionObject.AccessToken
to $token before the connection is opened. That's basically what invoke-sqlcmd does.
I have tried that. but $token
is a string, and $server.ConnectionContext.SqlConnectionObject.AccessToken
needs a IRenewableToken
.
SqlConnectionObject
is an instance of SqlConnection
Yeah, @shueybubbles I recall this is stuff @potatoqualitee and myself hit when we were trying to figure some of this out back in 2019 to support it all. We never could figure out the whole interface to that IRenewableToken because it also didn't make sense why that property requires an object that is for renewing a token itself.
That is what drove us to use the Azure DLLs, but then the issue with VS Code hit us.
Ok, it is a string. I will try to find my scripts again...
I found a script again.
I have tried this:
$serverName = "$ServerName.database.windows.net"
$connInfo = New-Object -TypeName Microsoft.SqlServer.Management.Common.SqlConnectionInfo -ArgumentList $serverName
$srvConn = New-Object -TypeName Microsoft.SqlServer.Management.Common.ServerConnection -ArgumentList $connInfo
$server = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server -ArgumentList $srvConn
$server.ConnectionContext.SqlConnectionObject.AccessToken = $token
And got this: Exception setting "AccessToken": "Cannot set the AccessToken property if the 'Integrated Security' connection string keyword has been set to 'true' or 'SSPI'."
This is the issue we captured most of our notes in #5445
IRenewableToken
only exists because we needed some way for SSMS 17 to provide tokens for SMO connections and because the the first implementation of AAD auth in System.Data.SqlClient
didn't have any such callback mechanism of its own. In .Net 4.7.2 you can register your own token provider which is what SSMS 18 does. You could potentially implement SqlAuthenticationProvider with a tiny class that wraps the azure cmdlet.
SSMS 19 will be based on newer SMO and on Microsoft.Data.SqlClient
. M.D.S comes with its own AAD implementation based on MSAL.Net.
@andreasjordan you'll probably have to construct your ServerConnection using a SqlConnection.
/// <summary>
/// Constructs a new ServerConnection object from the given SqlConnection
/// </summary>
/// <param name="sqlConnection"></param>
public ServerConnection(SqlConnection sqlConnection) : this(sqlConnection, null)
Some things work...
$connString = "Data Source=$ServerName.database.windows.net;Initial Catalog=$DatabaseName;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False"
$sqlConn = [System.Data.SqlClient.SqlConnection]::new($connString)
$sqlConn.AccessToken = $token
$sqlConn.Open()
works.
$srvConn = [Microsoft.SqlServer.Management.Common.ServerConnection]::new($sqlConn)
$srvConn.TrueLogin
$srvConn.ExecuteWithResults("select @@version").Tables[0].Column1
returns the correct login who generated the token and "Microsoft SQL Azure (RTM) - 12.0.2000.8 ..." as the version.
$server = [Microsoft.SqlServer.Management.Smo.Server]::new($srvConn)
$server.Collation
$server.Edition
$server.Query("select @@version")
Displays "SQL_Latin1_General_CP1_CI_AS" and "SQL Azure", but failes the .Query() with "Exception calling "Query" with "1" argument(s): "You cannot call a method on a null-valued expression.""
Is our [Microsoft.SqlServer.Management.Smo.Server] to old and incompatible?
Ok, .Query() is only added by dbatools and not working here. Will try to implement this in dbatools and come back to you later. Thanks for the help.
The Query()
method is added via PowerShell Types. https://github.com/sqlcollaborative/dbatools/blob/development/xml/dbatools.Types.ps1xml It is setup as an alias.
Thanks.
Now I know where the error is commming from: ($this.Databases[$Database].ExecuteWithResults($Query)).Tables[0]
does not work because .Databases
throws "...Failed to connect to server...".
($this.ConnectionContext.ExecuteWithResults($Query)).Tables[0]
works.
I think .Query()
never worked on Azure SQL Database. Can someone test? If so, do we want to fix?
prolly because azure needs a connection directly to the database and with onprem usually we connect to master and then change from there. If there's previous knowledge that this is in fact azure, an "if" sounds good. I don't think a lot of peoples use or know .Query(), but it's useful for internal function that needs elaborate batches to run as they run in SSMS .
Is one of you able to test my new branch and give feedback?
I don't have MI access but Azure SQL for sure: what should I test specifically ? will do ASAP
Try to follow the new example. I also don't have a MI, just Azure SQL Database. @cmuncan do you have a managed instance?
I do MI. I can test it out on Monday. What exactly is the test though?
Chris Muncan
Canadian Engineering Services | Database Engineer
E @.**@.>
M 519 897 6292<tel:519%20897%206292>
Log A Jira Ticket To Request Assistance from Database Engineering Teamhttps://cpcnissgwp01.americas.manulife.net:23800/secure/CreateIssue.jspa?pid=31170&issuetype=3
From: Andreas Jordan @.> Sent: Friday, May 14, 2021 2:19:32 PM To: sqlcollaborative/dbatools @.> Cc: Chris Muncan @.>; Mention @.> Subject: [EXTERNAL] Re: [sqlcollaborative/dbatools] [Bug]Connect-DbaInstance can't connect to Azure SQL or SQL MI (#7271)
CAUTION This email is from an external sender, be cautious with links and attachments.
Try to follow the new example. I also don't have a MI, just Azure SQL Database. @cmuncanhttps://github.com/cmuncan do you have a managed instance?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/sqlcollaborative/dbatools/issues/7271#issuecomment-841419567, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AQFI7AFDIBIP3OPOA3K2X7TTNVSTJANCNFSM4345R3PA.
STATEMENT OF CONFIDENTIALITY The information contained in this email message and any attachments may be confidential and legally privileged and is intended for the use of the addressee(s) only. If you are not an intended recipient, please: (1) notify me immediately by replying to this message; (2) do not use, disseminate, distribute or reproduce any part of the message or any attachment; and (3) destroy all copies of this message and any attachments.
If you can checkout this (https://github.com/sqlcollaborative/dbatools/tree/ConnectDbaInstance_Azure_Token) branch, you can try to follow the new example and use Connect-DbaInstance with AccessToken with the same token you use in your workaround.
@andreasjordan I was able to execute the test successfully.
$CredName = "user@domain.com"
$CredPass = ConvertTo-SecureString "superdupersecurepasswordhere" -AsPlainText -Force
$azureCredential = New-Object System.Management.Automation.PSCredential -ArgumentList ($CredName, $CredPass)
$azureAccount = Connect-AzAccount -Credential $azureCredential
$azureToken = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token
$azureInstance = "myinstance.database.windows.net"
Write-Output $azureAccount | Format-Table
Write-Output "===========`n"
Write-Output "Azure Token`n===========`n$azureToken`n==========="
$server = Connect-DbaInstance -SqlInstance $azureInstance -AccessToken $azureToken
Write-Output $server
Invoke-DbaQuery -SqlInstance $server -Query "select 1 as test"
Invoke-DbaQuery -SqlInstance $server -Query "select @@servername,@@version"
WARNING: TenantId 'my-tenant' contains more than one active subscription. First one will be selected for further use. To select another subscription, use Set-AzContext.
Account SubscriptionName TenantId Environment
------- ---------------- -------- -----------
user@domain.com MySub-NonProduction-S1 my-tenant AzureCloud
===========
Azure Token
===========
crazylongtokenhere
===========
Instance Name Version ProductLevel UpdateLevel HostPlatform HostDistribution
------------- ------- ------------ ----------- ------------ ----------------
myinstance.database.windows.net 15.0.100 RTM n/a Windows Windows Server 2019 Datacenter
test : 1
Column1 : myinstance.database.windows.net
Column2 : Microsoft SQL Azure (RTM) - 12.0.2000.8
Apr 29 2021 13:52:20
Copyright (C) 2019 Microsoft Corporation
PS C:\Users\muncach\Code>
@cmuncan Thank you very much.
@andreasjordan : works on Azure too
Report
We are working on the migration to that fancy fluffy stuff up in the sky. As of now, we have a mixed bag of SQL: SQL Server (on prem & VM), Azure SQL, & SQL MI. Now that we have Azure SQL & MI, I need to be able to connect holistically, regardless of what my SQL type is! I figured, based on the documentation in
Connect-DbaInstance
that I could do it, but unfortunately I was getting a bunch of errors! What are these errors you speak of? Well I am so happy you read this far and are interested for more!!!! Below I have the various errors and scripts that I used. I worked with @wsuhoey & @andreasjordan in the dbatools channel in Slack for this too.I did figure out a workaround for this (answered below too!)
Host used
Errors Received
Method 1
Yielded
Response status code does not indicate success: 400 (Bad Request).
Full error message with -DEBUG
Method 2 Using the new method of
Connect-DbaInstance
I get this (example 17 from the documentation page)Yielded
Cannot open server "MFCGD.COM" requested by the login. The login failed.
Full error message with -DEBUG
Steps to Reproduce
Method 1
Method 2 Using the new method of
Connect-DbaInstance
I get this (example 17 from the documentation page)Workaround
it would seem that dbatools doesn't yet support Azure SQL with Integrated Azure Active Directory ? well to get around this, I had to go outside of dbatools and use the classic
SqlServer
module'sInvoke-SqlCmd
Found in Example 11: Connect to Azure SQL Database (or Managed Instance) using an Access Token
Expected Behavior
to connect
Actual Behavior
it didn't connect
Environmental information
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 Please provide the output of the below script
SQL Server: