Skip to content

Commit 2c88f4c

Browse files
Connect-DbaInstance / Invoke-DbaQuery - Open a correct new connection when ConnectAsUserName is used and fix usage of AppendConnectionString (#9680)
1 parent 08c73cc commit 2c88f4c

File tree

2 files changed

+64
-69
lines changed

2 files changed

+64
-69
lines changed

public/Connect-DbaInstance.ps1

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ function Connect-DbaInstance {
289289
290290
#>
291291
[CmdletBinding()]
292+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
292293
param (
293294
[Parameter(Mandatory, ValueFromPipeline)]
294295
[Alias("Connstring", "ConnectionString")]
@@ -592,6 +593,7 @@ function Connect-DbaInstance {
592593
# Currently only if we have a different Database or have to switch to a NonPooledConnection or using a specific StatementTimeout or using ApplicationIntent
593594
# We do not test for SqlCredential as this would change the behavior compared to the legacy code path
594595
$copyContext = $false
596+
$createNewConnection = $false
595597
if ($Database) {
596598
Write-Message -Level Debug -Message "Database [$Database] provided."
597599
if (-not $inputObject.ConnectionContext.CurrentDatabase) {
@@ -602,6 +604,10 @@ function Connect-DbaInstance {
602604
if ($inputObject.ConnectionContext.CurrentDatabase -ne $Database) {
603605
Write-Message -Level Verbose -Message "Database [$Database] provided. Does not match ConnectionContext.CurrentDatabase [$($inputObject.ConnectionContext.CurrentDatabase)], copying ConnectionContext and setting the CurrentDatabase"
604606
$copyContext = $true
607+
if ($inputObject.ConnectionContext.ConnectAsUserName -ne '') {
608+
Write-Message -Level Debug -Message "Using ConnectAsUserName [$($inputObject.ConnectionContext.ConnectAsUserName)], so changing database context is not possible without loosing this information. We will create a new connection targeting database [$Database]"
609+
$createNewConnection = $true
610+
}
605611
}
606612
}
607613
if ($ApplicationIntent -and $inputObject.ConnectionContext.ApplicationIntent -ne $ApplicationIntent) {
@@ -620,7 +626,15 @@ function Connect-DbaInstance {
620626
Write-Message -Level Verbose -Message "DedicatedAdminConnection provided. Does not match ConnectionContext.ServerInstance, copying ConnectionContext and setting the ServerInstance"
621627
$copyContext = $true
622628
}
623-
if ($copyContext) {
629+
if ($createNewConnection) {
630+
$isNewConnection = $true
631+
$secStringPassword = ConvertTo-SecureString -String $inputObject.ConnectionContext.ConnectAsUserPassword -AsPlainText -Force
632+
$serverCredentialFromSMO = New-Object System.Management.Automation.PSCredential($inputObject.ConnectionContext.ConnectAsUserName, $secStringPassword)
633+
$connectParams = $PSBoundParameters
634+
$connectParams.SqlInstance = $inputObject.Name
635+
$connectParams.SqlCredential = $serverCredentialFromSMO
636+
$server = Connect-DbaInstance @connectParams
637+
} elseif ($copyContext) {
624638
$isNewConnection = $true
625639
$connContext = $inputObject.ConnectionContext.Copy()
626640
if ($ApplicationIntent) {

public/Invoke-DbaQuery.ps1

Lines changed: 49 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ function Invoke-DbaQuery {
169169
See https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql/sqlclient-support-for-high-availability-disaster-recovery#connecting-with-multisubnetfailover
170170
#>
171171
[CmdletBinding(DefaultParameterSetName = "Query")]
172-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
173172
param (
174173
[Parameter(ValueFromPipeline)]
175174
[Parameter(ParameterSetName = 'Query', Position = 0)]
@@ -406,83 +405,65 @@ function Invoke-DbaQuery {
406405
# We suppress the verbosity of all other functions in order to be sure the output is consistent with what you get, e.g., executing the same in SSMS
407406
Write-Message -Level Debug -Message "SqlInstance passed in, will work on: $instance"
408407
try {
408+
# We got another nightmare to solve, but fortunately @andreasjordan is "the king"
409+
# So. Here we are with a passed down "Server" instance. Problem is, we got two VERY different
410+
# libraries built with VERY different agendas. And we want to get the best of both worlds.
411+
# This is SUCH a nightmare because Invoke-DbaQuery is THE ONLY function that CAN use a Connection
412+
# (Microsoft.Data.SqlClient.SqlConnection) which can be used to run PARAMETRIZED queries.
413+
# So, recap of the recap:
414+
# 1. Microsoft.Data.SqlClient.SqlConnection:
415+
# PRO: is the only connection that can use Microsoft.Data.SqlClient.SqlCommand
416+
# that can use [Microsoft.Data.SqlClient.SqlParameter]s
417+
# CON: when using integrated auth, as in Integrated Security=True, cannot specify a DIFFERENT user than the current logged in one
418+
# 2. Microsoft.SqlServer.Management.Smo.Server
419+
# PRO: can specify a DIFFERENT user than the current logged in one via ConnectAsUser, ConnectAsUserName, ConnectAsUserPassword when Integrated Security=True
420+
# CON: cannot use Microsoft.Data.SqlClient.SqlCommand nor [Microsoft.Data.SqlClient.SqlParameter]s
421+
#
422+
# Till here, everything is clear: we want to reuse connection if we're sure the target is "95%" adherent to the supposed one
423+
# But, and that's a big but, the magic in Invoke-DbaQuery is making a Microsoft.SqlServer.Management.Smo.Server connection happen, let it "bleed" through here
424+
# land in Invoke-DbaAsync untouched, where it gets magically converted to a Microsoft.Data.SqlClient.SqlConnection, and we get the best of both worlds.
425+
# Thing is, the "magic" is rather ... not so magic. What it happens is that when the connection from Microsoft.SqlServer.Management.Smo.Server is "Open",
426+
# everything works fine, while if it's "Closed", Microsoft.Data.SqlClient.SqlConnection rehydrates a connection using the ConnectionString of Microsoft.SqlServer.Management.Smo.Server.
427+
# Microsoft.SqlServer.Management.Smo.Server is cheating though, because there's no connectionstring parameter that holds "log on as a different user", so, what happens is that
428+
# when Microsoft.Data.SqlClient.SqlConnection picks it up, rehydrates the connection that holds Integrated Security=True, and the information on "log on as a different user" is lost in
429+
# translation.
430+
# As anticipated, this happens ONLY when the connection is "Closed", because when it's "Open", no rehydration is needed, and everything works out of the box.
431+
# Now, another fancy thing: we want to use connection pooling by default, because pooling connection is more performant.
432+
# But, and that's the big but, when connection is pooled, it gets put in the "Closed" state which translates to "back to the pool, ready to be reused", as soon as no commands are actively used.
433+
# In dbatools world, that means pretty much that when we are here, it's always "Closed" UNLESS -NonPooledConnection is $true on Connect-DbaInstance, and that's why when we don't land here
434+
# we create a new instance with NonPooledConnection = $true ( see #8491 for details, also #7725 is still relevant)
435+
# If -NonPooled is passed, we instruct Microsoft.SqlServer.Management.Smo.Server we DON'T want pooling, so the connection stays "Open" (there's no pool to put it back),
436+
# and when it's "Open" we can leverage the fact that we already established a connection, verified the certificate, did the login handshake, etc AND when casting
437+
# to Microsoft.Data.SqlClient.SqlConnection it enables us to leverage [Microsoft.Data.SqlClient.SqlParameter]s !
438+
#
439+
# Again, here we are, but we cannot use the connection when an information is lost, which is that we are:
440+
# - Integrated Security=True
441+
# - using ConnectAsUser, ConnectAsUserName, ConnectAsUserPassword to "log on as a different user"
442+
409443
$startedWithAnOpenConnection = # we want to bypass Connect-DbaInstance if
410444
($instance.InputObject.GetType().Name -eq "Server") -and # we have Server SMO object and
411445
(-not $ReadOnly) -and # no readonly intent is requested and
412-
(-not $Database -or $instance.InputObject.ConnectionContext.DatabaseName -eq $Database) # the database is not set or the currently connected
446+
(-not $Database -or $instance.InputObject.ConnectionContext.DatabaseName -eq $Database) -and # the database is not set or the currently connected and
447+
(-not $AppendConnectionString) -and # we don't use AppendConnectionString and
448+
($instance.InputObject.ConnectionContext.ConnectAsUserName -eq '') # we don't use a DIFFERENT operating system user than the current logged in
413449
if ($startedWithAnOpenConnection) {
414-
Write-Message -Level Debug -Message "Current connection can be reused"
415-
# We got another nightmare to solve, but fortunately @andreasjordan is "the king"
416-
# So. Here we are with a passed down "Server" instance. Problem is, we got two VERY different
417-
# libraries built with VERY different agendas. And we want to get the best of both worlds.
418-
# This is SUCH a nightmare because Invoke-DbaQuery is THE ONLY function that CAN use a Connection
419-
# (Microsoft.Data.SqlClient.SqlConnection) which can be used to run PARAMETRIZED queries.
420-
# So, recap of the recap:
421-
# 1. Microsoft.Data.SqlClient.SqlConnection:
422-
# PRO: is the only connection that can use Microsoft.Data.SqlClient.SqlCommand
423-
# that can use [Microsoft.Data.SqlClient.SqlParameter]s
424-
# CON: when using integrated auth, as in Integrated Security=True, cannot specify a DIFFERENT user than the current logged in one
425-
# 2. Microsoft.SqlServer.Management.Smo.Server
426-
# PRO: can specify a DIFFERENT user than the current logged in one via ConnectAsUser, ConnectAsUserName, ConnectAsUserPassword when Integrated Security=True
427-
# CON: cannot use Microsoft.Data.SqlClient.SqlCommand nor [Microsoft.Data.SqlClient.SqlParameter]s
428-
#
429-
# Till here, everything is clear: we want to reuse connection if we're sure the target is "95%" adherent to the supposed one
430-
# But, and that's a big but, the magic in Invoke-DbaQuery is making a Microsoft.SqlServer.Management.Smo.Server connection happen, let it "bleed" through here
431-
# land in Invoke-DbaAsync untouched, where it gets magically converted to a Microsoft.Data.SqlClient.SqlConnection, and we get the best of both worlds.
432-
# Thing is, the "magic" is rather ... not so magic. What it happens is that when the connection from Microsoft.SqlServer.Management.Smo.Server is "Open",
433-
# everything works fine, while if it's "Closed", Microsoft.Data.SqlClient.SqlConnection rehydrates a connection using the ConnectionString of Microsoft.SqlServer.Management.Smo.Server.
434-
# Microsoft.SqlServer.Management.Smo.Server is cheating though, because there's no connectionstring parameter that holds "log on as a different user", so, what happens is that
435-
# when Microsoft.Data.SqlClient.SqlConnection picks it up, rehydrates the connection that holds Integrated Security=True, and the information on "log on as a different user" is lost in
436-
# translation.
437-
# As anticipated, this happens ONLY when the connection is "Closed", because when it's "Open", no rehydration is needed, and everything works out of the box.
438-
# Now, another fancy thing: we want to use connection pooling by default, because pooling connection is more performant.
439-
# But, and that's the big but, when connection is pooled, it gets put in the "Closed" state which translates to "back to the pool, ready to be reused", as soon as no commands are actively used.
440-
# In dbatools world, that means pretty much that when we are here, it's always "Closed" UNLESS -NonPooledConnection is $true on Connect-DbaInstance, and that's why when we don't land here
441-
# we create a new instance with NonPooledConnection = $true ( see #8491 for details, also #7725 is still relevant)
442-
# If -NonPooled is passed, we instruct Microsoft.SqlServer.Management.Smo.Server we DON'T want pooling, so the connection stays "Open" (there's no pool to put it back),
443-
# and when it's "Open" we can leverage the fact that we already established a connection, verified the certificate, did the login handshake, etc AND when casting
444-
# to Microsoft.Data.SqlClient.SqlConnection it enables us to leverage [Microsoft.Data.SqlClient.SqlParameter]s !
445-
#
446-
# Again, here we are, but we cannot use the connection when an information is lost, which is that we are:
447-
# - Integrated Security=True
448-
# - using ConnectAsUser, ConnectAsUserName, ConnectAsUserPassword to "log on as a different user"
449-
450-
if ($instance.InputObject.ConnectionContext.ConnectAsUserName -ne '') {
451-
Write-Message -Level Debug -Message "Current connection cannot be reused because logging in as a different user"
452-
# We rebuild correct credentials from SMO informations
453-
$secStringPassword = ConvertTo-SecureString $instance.InputObject.ConnectionContext.ConnectAsUserPassword -AsPlainText -Force
454-
[PSCredential]$serverCredentialFromSMO = New-Object System.Management.Automation.PSCredential($instance.InputObject.ConnectionContext.ConnectAsUserName, $secStringPassword)
455-
$connDbaInstanceParams = @{
456-
SqlInstance = $instance
457-
SqlCredential = $serverCredentialFromSMO
458-
Database = $Database
459-
NonPooledConnection = $true # see #8491 for details, also #7725 is still relevant
460-
Verbose = $false
461-
AppendConnectionString = $AppendConnectionString
462-
}
463-
if ($ReadOnly) {
464-
$connDbaInstanceParams.ApplicationIntent = "ReadOnly"
465-
}
466-
467-
$server = Connect-DbaInstance @connDbaInstanceParams
468-
469-
} else {
470-
Write-Message -Level Debug -Message "Current connection will be reused"
471-
$server = $instance.InputObject
472-
}
473-
450+
Write-Message -Level Debug -Message "Current connection will be reused"
451+
$server = $instance.InputObject
474452
} else {
475453
$connDbaInstanceParams = @{
476-
SqlInstance = $instance
477-
SqlCredential = $SqlCredential
478-
Database = $Database
479-
NonPooledConnection = $true # see #8491 for details, also #7725 is still relevant
480-
Verbose = $false
481-
AppendConnectionString = $AppendConnectionString
454+
SqlInstance = $instance
455+
SqlCredential = $SqlCredential
456+
Database = $Database
457+
NonPooledConnection = $true # see #8491 for details, also #7725 is still relevant
458+
Verbose = $false
482459
}
483460
if ($ReadOnly) {
484461
$connDbaInstanceParams.ApplicationIntent = "ReadOnly"
485462
}
463+
if ($AppendConnectionString) {
464+
$connDbaInstanceParams.AppendConnectionString = $AppendConnectionString
465+
$connDbaInstanceParams.SqlInstance = "$instance" # pass down "as a string" so it's forced to open a new one
466+
}
486467
$server = Connect-DbaInstance @connDbaInstanceParams
487468
}
488469
} catch {

0 commit comments

Comments
 (0)