From adcf2bb53dcdbf3856ca55d03fdf1bd9b567fc83 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Mon, 22 Jul 2019 09:45:44 +0200 Subject: [PATCH] SqlServerDsc: Changes to Invoke-Query (#1407) - Changes to helper function Invoke-Query - Now it will output verbose messages of the query that is run, so it not as quiet of what it is doing when a user asks for verbose output (issue #1404). - It is possible to redact text in the verbose output by providing strings in the new parameter `RedactText`. --- CHANGELOG.md | 5 ++ .../SqlServerDsc.Common.psm1 | 47 +++++++++++-- .../en-US/SqlServerDsc.Common.strings.psd1 | 2 + .../sv-SE/SqlServerDsc.Common.strings.psd1 | 4 +- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 70 ++++++++++++++++--- 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be823aa6..f1cf52c85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ - Can also pipe in 'Microsoft.SqlServer.Management.Smo.Server' object. - Can pipe Connect-SQL | Invoke-Query. - Added default values to Invoke-Query. + - Now it will output verbose messages of the query that is run, so it + not as quiet of what it is doing when a user asks for verbose output + ([issue #1404](https://github.com/PowerShell/SqlServerDsc/issues/1404)). + - It is possible to redact text in the verbose output by providing + strings in the new parameter `RedactText`. - Minor style fixes in unit tests. - Changes to helper function Connect-SQL - When impersonating WindowsUser credential use the NetworkCredential UserName. diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index 8d9b609d9..4f36e2b04 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -1594,8 +1594,9 @@ function Restart-ReportingServicesService The query string to execute. .PARAMETER DatabaseCredential - PSCredential object with the credentials to use to impersonate a user when connecting. - If this is not provided then the current user will be used to connect to the SQL Server Database Engine instance. + PSCredential object with the credentials to use to impersonate a user + when connecting. If this is not provided then the current user will be + used to connect to the SQL Server Database Engine instance. .PARAMETER LoginType Specifies which type of logon credential should be used. The valid types are @@ -1603,8 +1604,8 @@ function Restart-ReportingServicesService then the SetupCredential needs to be specified as well. .PARAMETER SqlServerObject - You can pass in an object type of 'Microsoft.SqlServer.Management.Smo.Server'. This can also be passed in - through the pipeline allowing you to use connect-sql | invoke-query if you wish. + You can pass in an object type of 'Microsoft.SqlServer.Management.Smo.Server'. + This can also be passed in through the pipeline. See examples. .PARAMETER WithResults Specifies if the query should return results. @@ -1612,6 +1613,11 @@ function Restart-ReportingServicesService .PARAMETER StatementTimeout Set the query StatementTimeout in seconds. Default 600 seconds (10mins). + .PARAMETER RedactText + One or more strings to redact from the query when verbose messages are + written to the console. Strings here will be escaped so they will not + be interpreted as regular expressions (RegEx). + .EXAMPLE Invoke-Query -SQLServer Server1 -SQLInstanceName MSSQLSERVER -Database master ` -Query 'SELECT name FROM sys.databases' -WithResults @@ -1623,6 +1629,12 @@ function Restart-ReportingServicesService .EXAMPLE Connect-SQL @sqlConnectionParameters | Invoke-Query -Database master ` -Query 'SELECT name FROM sys.databases' -WithResults + + .EXAMPLE + Invoke-Query -SQLServer Server1 -SQLInstanceName MSSQLSERVER -Database MyDatabase ` + -Query "select * from MyTable where password = 'Pa\ssw0rd1' and password = 'secret passphrase'" ` + -WithResults -RedactText @('Pa\sSw0rd1','Secret PassPhrase') -Verbose + #> function Invoke-Query { @@ -1670,7 +1682,11 @@ function Invoke-Query [Parameter()] [ValidateNotNull()] [System.Int32] - $StatementTimeout = 600 + $StatementTimeout = 600, + + [Parameter()] + [System.String[]] + $RedactText ) if ($PSCmdlet.ParameterSetName -eq 'SqlObject') @@ -1694,10 +1710,27 @@ function Invoke-Query $serverObject = Connect-SQL @connectSQLParameters } + $redactedQuery = $Query + + foreach ($redactString in $RedactText) + { + <# + Escaping the string to handle strings which could look like + regular expressions, like passwords. + #> + $escapedRedactedString = [System.Text.RegularExpressions.Regex]::Escape($redactString) + + $redactedQuery = $redactedQuery -ireplace $escapedRedactedString,'*******' + } + if ($WithResults) { try { + Write-Verbose -Message ( + $script:localizedData.ExecuteQueryWithResults -f $redactedQuery + ) -Verbose + $result = $serverObject.Databases[$Database].ExecuteWithResults($Query) } catch @@ -1710,6 +1743,10 @@ function Invoke-Query { try { + Write-Verbose -Message ( + $script:localizedData.ExecuteNonQuery -f $redactedQuery + ) -Verbose + $serverObject.Databases[$Database].ExecuteNonQuery($Query) } catch diff --git a/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 index c60d79c3d..0a9a44bcf 100644 --- a/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 +++ b/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 @@ -57,4 +57,6 @@ ConvertFrom-StringData @' ConnectingUsingIntegrated = Connecting as current user '{0}' using integrated security. (SQLCOMMON0054) CredentialsNotSpecified = The Logon type of '{0}' was specified which requires credentials, but the credentials parameter was not specified. (SQLCOMMON0055) ConnectingUsingImpersonation = Impersonate credential '{0}' with login type '{1}'. (SQLCOMMON0056) + ExecuteQueryWithResults = Returning the results of the query `{0}`. (SQLCOMMON0057) + ExecuteNonQuery = Executing the query `{0}`. (SQLCOMMON0058) '@ diff --git a/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 index 0ed2dc922..ed4296803 100644 --- a/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 +++ b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 @@ -62,5 +62,7 @@ ConvertFrom-StringData @' ClusterLoginPermissionsPresent = The cluster login '{0}' has the required permissions. (SQLCOMMON0053) ConnectingUsingIntegrated = Anslutning som nuvarande användare '{0}' med integrerad säkerhet. (SQLCOMMON0054) CredentialsNotSpecified = The Logon type of '{0}' was specified which requires credentials, but the credentials parameter was not specified. (SQLCOMMON0055) - ConnectingUsingImpersonation = Impersoner credential '{0}' med inloggningstyp '{1}'. (SQLCOMMON0056) + ConnectingUsingImpersonation = Uppträder som behörigheten '{0}' med inloggningstyp '{1}'. (SQLCOMMON0056) + ExecuteQueryWithResults = Returnerar resultatet av frågan `{0}`. (SQLCOMMON0057) + ExecuteNonQuery = Exekverar frågan `{0}`. (SQLCOMMON0058) '@ diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 2539a4b14..032b940cb 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -1465,14 +1465,16 @@ InModuleScope 'SqlServerDsc.Common' { Database = 'master' } - Context 'Execute a query with no results' { + Context 'When executing a query with no results' { + AfterEach { + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + It 'Should execute the query silently' { $queryParams.Query = "EXEC sp_configure 'show advanced option', '1'" $mockExpectedQuery = $queryParams.Query.Clone() { Invoke-Query @queryParams } | Should -Not -Throw - - Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly } It 'Should throw the correct error, ExecuteNonQueryFailed, when executing the query fails' { @@ -1481,12 +1483,37 @@ InModuleScope 'SqlServerDsc.Common' { { Invoke-Query @queryParams } | Should -Throw ( $script:localizedData.ExecuteNonQueryFailed -f $queryParams.Database ) + } - Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + Context 'When text should be redacted' { + BeforeAll { + Mock -CommandName Write-Verbose -ParameterFilter { + $Message -eq ( + $script:localizedData.ExecuteNonQuery -f + "select * from MyTable where password = '*******' and password = '*******'" + ) + } -MockWith { + <# + MUST return another message than the parameter filter + is looking for, otherwise we get into a endless loop. + We returning the to show in the output how the verbose + message was redacted. + #> + Write-Verbose -Message ('MOCK OUTPUT: {0}' -f $Message) -Verbose + } + } + + It 'Should execute the query silently and redact text in the verbose output' { + $queryParams.Query = "select * from MyTable where password = 'Pa\ssw0rd1' and password = 'secret passphrase'" + $mockExpectedQuery = $queryParams.Query.Clone() + + # The `Secret PassPhrase` is using the casing like this to test case-insensitive replace. + { Invoke-Query @queryParams -RedactText @('Pa\sSw0rd1','Secret PassPhrase') } | Should -Not -Throw + } } } - Context 'Execute a query with results' { + Context 'When executing a query with results' { It 'Should execute the query and return a result set' { $queryParams.Query = 'SELECT name FROM sys.databases' $mockExpectedQuery = $queryParams.Query.Clone() @@ -1505,9 +1532,36 @@ InModuleScope 'SqlServerDsc.Common' { Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly } + + Context 'When text should be redacted' { + BeforeAll { + Mock -CommandName Write-Verbose -ParameterFilter { + $Message -eq ( + $script:localizedData.ExecuteQueryWithResults -f + "select * from MyTable where password = '*******' and password = '*******'" + ) + } -MockWith { + <# + MUST return another message than the parameter filter + is looking for, otherwise we get into a endless loop. + We returning the to show in the output how the verbose + message was redacted. + #> + Write-Verbose -Message ('MOCK OUTPUT: {0}' -f $Message) -Verbose + } + } + + It 'Should execute the query silently and redact text in the verbose output' { + $queryParams.Query = "select * from MyTable where password = 'Pa\ssw0rd1' and password = 'secret passphrase'" + $mockExpectedQuery = $queryParams.Query.Clone() + + # The `Secret PassPhrase` is using the casing like this to test case-insensitive replace. + { Invoke-Query @queryParams -RedactText @('Pa\sSw0rd1','Secret PassPhrase') -WithResults } | Should -Not -Throw + } + } } - Context 'Pass in an SMO Server Object' { + Context 'When passing in an SMO Server Object' { Context 'Execute a query with no results' { It 'Should execute the query silently' { $queryParametersWithSMO.Query = "EXEC sp_configure 'show advanced option', '1'" @@ -1529,7 +1583,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Execute a query with results' { + Context 'When executing a query with results' { It 'Should execute the query and return a result set' { $queryParametersWithSMO.Query = 'SELECT name FROM sys.databases' $mockExpectedQuery = $queryParametersWithSMO.Query.Clone() @@ -1550,7 +1604,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Execute a query with piped SMO server object' { + Context 'When executing a query with piped SMO server object' { It 'Should execute the query and return a result set' { $mockQuery = 'SELECT name FROM sys.databases' $mockExpectedQuery = $mockQuery