From e9e70242eaf14784ac746c267cf7c050febd60c8 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Thu, 6 Jun 2019 22:41:05 +0300 Subject: [PATCH 01/10] 1 --- CHANGELOG.md | 9 + .../MSFT_SqlServerDatabaseMail.psm1 | 238 ++++++++++- .../MSFT_SqlServerDatabaseMail.schema.mof | 3 + .../MSFT_SqlServerDatabaseMail.strings.psd1 | 6 + .../1-EnableDatabaseMail.ps1 | 14 +- .../SqlServerDsc.Common.psm1 | 303 ++++++++++++- .../en-US/SqlServerDsc.Common.strings.psd1 | 7 + .../sv-SE/SqlServerDsc.Common.strings.psd1 | 7 + README.md | 6 + ...qlServerDatabaseMail.Integration.Tests.ps1 | 40 +- .../MSFT_SqlServerDatabaseMail.config.ps1 | 11 + .../Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 | 258 ++++++++++- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 400 +++++++++++++++++- 13 files changed, 1240 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 446e47e437..376508bad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,15 @@ - Fix issue where calling Get would return an error because the database name list may have been returned as a string instead of as a string array ([issue #1368](https://github.com/PowerShell/SqlServerDsc/issues/1368)). +- Changes to SqlServerDatabaseMail + - Added new parameter `EnableSsl` which controls encryption of communication using Secure Sockets Layer. + - Added new parameter `Authentication` and `SMTPAccount` for configuration of SMTP authentication mode and credential used [issue #1215](https://github.com/PowerShell/SqlServerDsc/issues/1215). + - Added new helper function `Get-MailServerCredentialId` which gets credential Id used by mail server. + - Added new helper function `Get-ServiceMasterKey` which gets unencrypted Service Master Key for specified SQL Instance. + - Added new helper function `Get-SqlPSCredential` which decrypts and returns PSCredential object of SQL Credential by Id. + - Added DAC switch to function 'Connect-SQL' with default value $false. Used to specify that non-pooled Dedicated Admin Connection should be established. + - Added UsingDAC switch to function 'Invoke-Query' with default value $false. Used to specify that query should be executed using Dedicated Admin Connection. After execution DAC connection will be closed. + - Added PSCredential option to function 'Test-DscParameterState' which will compare PSCredential objects using username/password keys. ## 12.5.0.0 diff --git a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 index d276f06d7a..4502307e63 100644 --- a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 +++ b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 @@ -75,6 +75,9 @@ function Get-TargetResource ReplyToAddress = $null Description = $null TcpPort = $null + EnableSsl = $null + Authentication = $null + SMTPAccount = $null } Write-Verbose -Message ( @@ -142,6 +145,36 @@ function Get-TargetResource $returnValue['MailServerName'] = $mailServer.Name $returnValue['TcpPort'] = $mailServer.Port + $returnValue['EnableSsl'] = $mailServer.EnableSsl + + <# + When UseDefaultCredentials is True, Database Mail uses the + credentials of the SQL Server Database Engine service. When + this parameter is False, Database Mail uses the **@username** + and **@password** for authentication on the SMTP server. + If **@username** and **@password** are NULL, then Database Mail + uses anonymous authentication. + #> + if ($mailServer.UseDefaultCredentials) + { + $returnValue['Authentication'] = 'Windows' + } + elseif ($mailServer.UserName) + { + $returnValue['Authentication'] = 'Basic' + + $credentialId = Get-MailServerCredentialId -SQLServer $ServerName ` + -SQLInstanceName $InstanceName ` + -MailServerName $mailServer.Name + + $returnValue['SMTPAccount'] = Get-SqlPSCredential -SQLServer $ServerName ` + -SQLInstanceName $InstanceName ` + -CredentialId $credentialId + } + else + { + $returnValue['Authentication'] = 'Anonymous' + } # Currently only one profile is handled, so this make sure only the first string (profile name) is returned. $returnValue['ProfileName'] = $databaseMail.Profiles | Select-Object -First 1 -ExpandProperty Name @@ -224,6 +257,15 @@ function Get-TargetResource .PARAMETER TcpPort The TCP port used for communication. Default value is port 25. + .PARAMETER EnableSsl + Specifies whether to encrypt communication using Secure Sockets Layer or not. + + .PARAMETER Authentication + SMTP authentication mode to be used. Default value is 'Anonymous'. + + .PARAMETER SMTPAccount + Account used for SMTP authentication if 'Basic' mode was chosen. + .NOTES Information about the different properties can be found here https://docs.microsoft.com/en-us/sql/relational-databases/database-mail/configure-database-mail. @@ -283,9 +325,29 @@ function Set-TargetResource [Parameter()] [System.UInt16] - $TcpPort = 25 + $TcpPort = 25, + + [Parameter()] + [System.Boolean] + $EnableSsl, + + [Parameter()] + [ValidateSet('Anonymous', 'Basic', 'Windows')] + [System.String] + $Authentication = 'Anonymous', + + [Parameter()] + [System.Management.Automation.PSCredential] + $SMTPAccount ) + if (-not $PSBoundParameters.ContainsKey('SMTPAccount') -and ` + $Authentication -eq 'Basic') + { + $errorMessage = $script:localizedData.SMTPAccountMissingParameter + New-InvalidArgumentException -ArgumentName 'SMTPAccount' -Message $errorMessage + } + Write-Verbose -Message ( $script:localizedData.ConnectToSqlInstance ` -f $ServerName, $InstanceName @@ -372,6 +434,27 @@ function Set-TargetResource $mailServer.Port = $TcpPort } + if ($PSBoundParameters.ContainsKey('EnableSsl')) + { + $mailServer.EnableSsl = $EnableSsl + } + + if ($PSBoundParameters.ContainsKey('Authentication')) + { + # Default Authentication is Anonymous so it's absent in the selection. + switch($Authentication) + { + 'Basic' + { + $mailServer.SetAccount($SMTPAccount.UserName, $SMTPAccount.Password) + } + 'Windows' + { + $mailServer.UseDefaultCredentials = $true + } + } + } + $mailServer.Alter() } } @@ -473,6 +556,110 @@ function Set-TargetResource $mailServer.Port = $TcpPort $mailServer.Alter() } + + $currentEnableSsl = $mailServer.EnableSsl + if ($currentEnableSsl -ne $EnableSsl) + { + Write-Verbose -Message ( + $script:localizedData.UpdatingPropertyOfMailServer -f @( + $currentEnableSsl + $EnableSsl + $script:localizedData.MailServerPropertyEnableSsl + ) + ) + + $mailServer.EnableSsl = $EnableSsl + $mailServer.Alter() + } + + # Checking current SMTP Authentication mode and SMTP Account + $currentSMTPAccount = $null + if ($mailServer.UseDefaultCredentials) + { + $currentAuthentication = 'Windows' + } + elseif ($mailServer.UserName) + { + $currentAuthentication = 'Basic' + + $credentialId = Get-MailServerCredentialId -SQLServer $ServerName ` + -SQLInstanceName $InstanceName ` + -MailServerName $MailServerName + + $currentSMTPAccount = Get-SqlPSCredential -SQLServer $ServerName ` + -SQLInstanceName $InstanceName ` + -CredentialId $credentialId + } + else + { + $currentAuthentication = 'Anonymous' + } + + if ($currentAuthentication -ne $Authentication) + { + Write-Verbose -Message ( + $script:localizedData.UpdatingPropertyOfMailServer -f @( + $currentAuthentication + $Authentication + $script:localizedData.MailServerPropertyAuthentication + ) + ) + + $mailServer.UseDefaultCredentials = switch($Authentication) + { + 'Windows' { $true } + Default { $false } + } + + if ($Authentication -ne 'Basic' -and $currentSMTPAccount.UserName) + { + Write-Verbose -Message ( + $script:localizedData.UpdatingPropertyOfMailServer -f @( + $currentSMTPAccount.UserName + '' + $script:localizedData.MailServerPropertySMTPAccount + ) + ) + $mailServer.UserName = '' + } + + $mailServer.Alter() + } + + if ($Authentication -eq 'Basic') + { + if ($SMTPAccount.UserName -ne $currentSMTPAccount.UserName) + { + Write-Verbose -Message ( + $script:localizedData.UpdatingPropertyOfMailServer -f @( + $currentSMTPAccount.UserName + $SMTPAccount.UserName + $script:localizedData.MailServerPropertySMTPAccount + ) + ) + + $mailServer.SetAccount($SMTPAccount.UserName, $SMTPAccount.Password) + } + elseif (([System.String]::IsNullOrEmpty($currentSMTPAccount.Password) -and $SMTPAccount.Password) -or ` + ([System.String]::IsNullOrEmpty($SMTPAccount.Password) -and $currentSMTPAccount.Password) -or ` + ($currentSMTPAccount.GetNetworkCredential().Password -ne ` + $SMTPAccount.GetNetworkCredential().Password)) + { + <# + Message will not include real password values unless password is not set. + This was done on purpose. + #> + Write-Verbose -Message ( + $script:localizedData.UpdatingPropertyOfMailServer -f @( + $currentSMTPAccount.Password + $SMTPAccount.Password + $script:localizedData.MailServerPropertySMTPAccountPassword + ) + ) + + $mailServer.SetPassword($SMTPAccount.Password) + } + } } $databaseMailProfile = $databaseMail.Profiles | Where-Object -FilterScript { @@ -623,6 +810,15 @@ function Set-TargetResource .PARAMETER TcpPort The TCP port used for communication. Default value is port 25. + + .PARAMETER EnableSsl + Specifies whether to encrypt communication using Secure Sockets Layer or not. + + .PARAMETER Authentication + SMTP authentication mode to be used. Default value is 'Anonymous'. + + .PARAMETER SMTPAccount + Account used for SMTP authentication if 'Basic' mode was chosen. #> function Test-TargetResource { @@ -679,7 +875,20 @@ function Test-TargetResource [Parameter()] [System.UInt16] - $TcpPort = 25 + $TcpPort = 25, + + [Parameter()] + [System.Boolean] + $EnableSsl, + + [Parameter()] + [ValidateSet('Anonymous', 'Basic', 'Windows')] + [System.String] + $Authentication = 'Anonymous', + + [Parameter()] + [System.Management.Automation.PSCredential] + $SMTPAccount ) $getTargetResourceParameters = @{ @@ -701,10 +910,7 @@ function Test-TargetResource if ($Ensure -eq 'Present') { - $returnValue = Test-DscParameterState ` - -CurrentValues $getTargetResourceResult ` - -DesiredValues $PSBoundParameters ` - -ValuesToCheck @( + $valuesToCheck = @( 'AccountName' 'EmailAddress' 'MailServerName' @@ -715,7 +921,27 @@ function Test-TargetResource 'DisplayName' 'Description' 'LoggingLevel' + 'EnableSsl' + 'Authentication' ) + + # If current or desired Authentication is set to 'Basic' we need to include SMTPAccount property + if ('Basic' -in @($Authentication, $getTargetResourceResult.Authentication)) + { + $valuesToCheck += 'SMTPAccount' + + # Ignore SMTPAccount property value if it was specified with Authentication not set to 'Basic' + if ($Authentication -ne 'Basic' -and $PSBoundParameters.ContainsKey('SMTPAccount')) + { + Write-Warning -Message $script:localizedData.SMTPAccountIgnoringParameter + $PSBoundParameters['SMTPAccount'] = [System.Management.Automation.PSCredential]::Empty + } + } + + $returnValue = Test-DscParameterState ` + -CurrentValues $getTargetResourceResult ` + -DesiredValues $PSBoundParameters ` + -ValuesToCheck $valuesToCheck } else { diff --git a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.schema.mof b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.schema.mof index a1e76a1146..d05de637dd 100644 --- a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.schema.mof +++ b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.schema.mof @@ -13,4 +13,7 @@ class MSFT_SqlServerDatabaseMail : OMI_BaseResource [Write, Description("The description for the Database Mail profile and account.")] String Description; [Write, Description("The logging level that the Database Mail will use. If not specified the default logging level is 'Extended'."), ValueMap{"Normal","Extended","Verbose"}, Values{"Normal","Extended","Verbose"}] String LoggingLevel; [Write, Description("The TCP port used for communication. Default value is port 25.")] UInt16 TcpPort; + [Write, Description("Specifies whether to encrypt communication using Secure Sockets Layer or not.")] Boolean EnableSsl; + [Write, Description("SMTP authentication mode to be used. Default value is 'Anonymous'."), ValueMap{"Anonymous","Basic","Windows"}, Values{"Anonymous","Basic","Windows"}] String Authentication; + [Write, EmbeddedInstance("MSFT_Credential"), Description("Account used for SMTP authentication if 'Basic' was chosen.")] String SMTPAccount; }; diff --git a/DSCResources/MSFT_SqlServerDatabaseMail/en-US/MSFT_SqlServerDatabaseMail.strings.psd1 b/DSCResources/MSFT_SqlServerDatabaseMail/en-US/MSFT_SqlServerDatabaseMail.strings.psd1 index 9f91e87266..4be5b72bd6 100644 --- a/DSCResources/MSFT_SqlServerDatabaseMail/en-US/MSFT_SqlServerDatabaseMail.strings.psd1 +++ b/DSCResources/MSFT_SqlServerDatabaseMail/en-US/MSFT_SqlServerDatabaseMail.strings.psd1 @@ -22,6 +22,10 @@ ConvertFrom-StringData @' MailServerPropertyReplyToEmailAddress = reply to e-mail address MailServerPropertyServerName = server name MailServerPropertyTcpPort = TCP port + MailServerPropertyEnableSsl = SSL property + MailServerPropertySMTPAccount = SMTP account + MailServerPropertySMTPAccountPassword = password for SMTP account + MailServerPropertyAuthentication = authentication CreatingMailProfile = Creating a public default profile '{0}'. MailProfileExist = The public default profile '{0}' already exist. ConfigureSqlAgent = Configure the SQL Agent to use Database Mail. @@ -30,4 +34,6 @@ ConvertFrom-StringData @' RemovingSqlAgentConfiguration = Configure the SQL Agent to not use Database Mail (changing it back to SQL Agent Mail). RemovingMailProfile = Removing the public default profile '{0}'. RemovingMailAccount = Removing the mail account '{0}'. + SMTPAccountMissingParameter = SMTPAccount parameter must be specified when Authentication is set to 'Basic'. + SMTPAccountIgnoringParameter = SMTPAccount parameter was specified and will be ignored as Authentication was not set to 'Basic'. '@ diff --git a/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 b/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 index d5fa462d4b..da307adebc 100644 --- a/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 @@ -18,6 +18,8 @@ $ConfigurationData = @{ Description = 'Default mail account and profile.' LoggingLevel = 'Normal' TcpPort = 25 + EnableSsl = $true + Authentication = 'Basic' } ) } @@ -29,15 +31,20 @@ Configuration Example [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] - $SqlInstallCredential + $SqlInstallCredential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SMTPAccountCredential ) Import-DscResource -ModuleName 'SqlServerDsc' node localhost { + SqlServerConfiguration 'EnableDatabaseMailXPs' { - ServerName = $Node.ServerName InstanceName = $Node.InstanceName OptionName = 'Database Mail XPs' @@ -59,6 +66,9 @@ Configuration Example Description = $Node.Description LoggingLevel = $Node.LoggingLevel TcpPort = $Node.TcpPort + EnableSsl = $Node.EnableSsl + Authentication = $Node.Authentication + SMTPAccount = $SMTPAccountCredential PsDscRunAsCredential = $SqlInstallCredential } diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index f780af7b6d..2117be30c4 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -477,6 +477,30 @@ function Test-DscParameterState } } + 'PSCredential' + { + if ($CurrentValues.$fieldName.UserName -ne $DesiredValues.$fieldName.UserName) + { + Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName.UserName), $($DesiredValues.$fieldName.UserName)) -Verbose + + $returnValue = $false + } + elseif (([System.String]::IsNullOrEmpty($CurrentValues.$fieldName.Password) -and $DesiredValues.$fieldName.Password) -or + ([System.String]::IsNullOrEmpty($DesiredValues.$fieldName.Password) -and $CurrentValues.$fieldName.Password) -or + ($CurrentValues.$fieldName.GetNetworkCredential().Password -ne $DesiredValues.$fieldName.GetNetworkCredential().Password)) + { + <# + Message will not include real password values unless password is not set. + This was done on purpose. + #> + Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName.Password), $($DesiredValues.$fieldName.Password)) -Verbose + + $returnValue = $false + } + } + default { Write-Warning -Message ($script:localizedData.UnableToCompareProperty ` @@ -940,6 +964,9 @@ function Start-SqlSetupProcess login specified in the parameter SetupCredential. Default value is 'Integrated'. + .PARAMETER DAC + Specifies whether Dedicated Admin Connection should be established instead of an ordinary one. + .PARAMETER StatementTimeout Set the query StatementTimeout in seconds. Default 600 seconds (10mins). #> @@ -969,6 +996,10 @@ function Connect-SQL [System.String] $LoginType = 'Integrated', + [Parameter()] + [System.Management.Automation.SwitchParameter] + $DAC, + [Parameter()] [ValidateNotNull()] [System.Int32] @@ -989,6 +1020,13 @@ function Connect-SQL $sqlServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' $sqlConnectionContext = $sqlServerObject.ConnectionContext + if ($DAC.IsPresent) + { + # Changing instance name to use Dedicated Admin Connection + $databaseEngineInstance = "ADMIN:$databaseEngineInstance" + $sqlConnectionContext.NonPooledConnection = $true + } + $sqlConnectionContext.ServerInstance = $databaseEngineInstance $sqlConnectionContext.StatementTimeout = $StatementTimeout $sqlConnectionContext.ApplicationName = 'SqlServerDsc' @@ -1602,6 +1640,9 @@ function Restart-ReportingServicesService .PARAMETER WithResults Specifies if the query should return results. + .PARAMETER UsingDAC + Specifies whether Dedicated Admin Connection should be used to execute query. + .PARAMETER StatementTimeout Set the query StatementTimeout in seconds. Default 600 seconds (10mins). @@ -1622,14 +1663,14 @@ function Invoke-Query [CmdletBinding(DefaultParameterSetName='SqlServer')] param ( - [Alias("ServerName")] [Parameter(ParameterSetName='SqlServer')] + [Alias("ServerName")] [ValidateNotNullOrEmpty()] [System.String] $SQLServer = $env:COMPUTERNAME, - [Alias("InstanceName")] [Parameter(ParameterSetName='SqlServer')] + [Alias("InstanceName")] [System.String] $SQLInstanceName = 'MSSQLSERVER', @@ -1641,8 +1682,8 @@ function Invoke-Query [System.String] $Query, - [Alias("SetupCredential")] [Parameter()] + [Alias("SetupCredential")] [System.Management.Automation.PSCredential] $DatabaseCredential, @@ -1657,9 +1698,14 @@ function Invoke-Query $SqlServerObject, [Parameter()] - [Switch] + [System.Management.Automation.SwitchParameter] $WithResults, + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsingDAC, + + [Parameter()] [ValidateNotNull()] [System.Int32] $StatementTimeout = 600 @@ -1675,6 +1721,7 @@ function Invoke-Query ServerName = $SQLServer InstanceName = $SQLInstanceName LoginType = $LoginType + DAC = $UsingDAC.IsPresent StatementTimeout = $StatementTimeout } @@ -1686,28 +1733,28 @@ function Invoke-Query $serverObject = Connect-SQL @connectSQLParameters } - if ($WithResults) + try { - try + if ($WithResults.IsPresent) { $result = $serverObject.Databases[$Database].ExecuteWithResults($Query) } - catch + else { - $errorMessage = $script:localizedData.ExecuteQueryWithResultsFailed -f $Database - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + $serverObject.Databases[$Database].ExecuteNonQuery($Query) } } - else + catch { - try - { - $serverObject.Databases[$Database].ExecuteNonQuery($Query) - } - catch + $errorMessage = $script:localizedData.$(@('ExecuteNonQueryFailed', 'ExecuteQueryWithResultsFailed')[$WithResults.IsPresent]) -f $Database + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + if ($UsingDAC.IsPresent) { - $errorMessage = $script:localizedData.ExecuteNonQueryFailed -f $Database - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + $serverObject.ConnectionContext.Disconnect() + Write-Verbose -Message ($script:localizedData.DisconnectFromSQLInstance -f $serverObject.ConnectionContext.ServerInstance) -Verbose } } @@ -2428,6 +2475,225 @@ function Find-ExceptionByNumber return $errorFound } +<# + .SYNOPSIS + Gets credential Id used to hold account authentication information for specified SMTP + mail server on the SQL Instance. + + .PARAMETER SQLServer + String containing the host name of the SQL Server to connect to. + + .PARAMETER SQLInstanceName + String containing the SQL Server Database Engine instance to connect to. + + .PARAMETER MailServerName + Specifies name of the SMTP mail server for which credential Id should be returned. +#> +function Get-MailServerCredentialId +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SQLServer, + + [Parameter(Mandatory = $true)] + [System.String] + $SQLInstanceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $MailServerName + ) + + $invokeQueryParameters = @{ + SQLServer = $SQLServer + SQLInstanceName = $SQLInstanceName + Database = 'master' + WithResults = $true + } + + $queryToGetCredentialId = " + SELECT credential_id + FROM msdb.dbo.sysmail_server + WHERE servername = '$MailServerName' + " + + Write-Verbose -Message ($script:localizedData.GetMailServerCredentialId -f $MailServerName, $SQLInstanceName) -Verbose + $result = (Invoke-Query @invokeQueryParameters -Query $queryToGetCredentialId).Tables.credential_id + + return $result +} + +<# + .SYNOPSIS + Gets unencrypted Service Master Key for specified SQL Instance + which will be used for credential password decryption. + + .PARAMETER SQLServer + String containing the host name of the SQL Server to connect to. + + .PARAMETER SQLInstanceName + String containing the SQL Server Database Engine instance to connect to. + + .NOTES + This function is based on Antti Rantasaari's script at https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1 + Antti Rantasaari 2014, NetSPI + License: BSD 3-Clause http://opensource.org/licenses/BSD-3-Clause +#> +function Get-ServiceMasterKey +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SQLServer, + + [Parameter(Mandatory = $true)] + [System.String] + $SQLInstanceName + ) + + Add-Type -AssemblyName System.Security + + $invokeQueryParameters = @{ + SQLServer = $SQLServer + SQLInstanceName = $SQLInstanceName + Database = 'master' + WithResults = $true + } + + $queryToGetServiceMasterKey = " + SELECT substring(crypt_property,9,len(crypt_property)-8) AS ServiceMasterKey + FROM sys.key_encryptions + WHERE key_id=102 and (thumbprint=0x03 or thumbprint=0x0300000001) + " + + Write-Verbose -Message ($script:localizedData.GetServiceMasterKey -f $SQLInstanceName) -Verbose + $smkEncryptedBytes = (Invoke-Query @invokeQueryParameters -Query $queryToGetServiceMasterKey).Tables[0].ServiceMasterKey + + Write-Verbose -Message ($script:localizedData.GetEntropyForSqlInstance -f $SQLInstanceName) -Verbose + $sqlInstanceId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -ErrorAction Stop).$SQLInstanceName + [byte[]]$entropy = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$sqlInstanceId\Security" -ErrorAction Stop).Entropy + + # Decrypt the service master key + $serviceMasterKey = [System.Security.Cryptography.ProtectedData]::Unprotect($smkEncryptedBytes, $entropy, 'LocalMachine') + + return $serviceMasterKey +} + +<# + .SYNOPSIS + Gets MSFT_Credential object for specified credential Id on specified SQL Instance. + + .PARAMETER SQLServer + String containing the host name of the SQL Server to connect to. + + .PARAMETER SQLInstanceName + String containing the SQL Server Database Engine instance to connect to. + + .PARAMETER CredentialId + Specifies Id of the credential for which MSFT_Credential should be returned. + + .NOTES + This function is partialy based on Antti Rantasaari's script at https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1 +#> +function Get-SqlPSCredential +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSCredential])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SQLServer, + + [Parameter(Mandatory = $true)] + [System.String] + $SQLInstanceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Int32] + $CredentialId + ) + + $invokeQueryParameters = @{ + SQLServer = $SQLServer + SQLInstanceName = $SQLInstanceName + Database = 'master' + WithResults = $true + UsingDAC = $true + } + + $smk = Get-ServiceMasterKey -SQLServer $SQLServer -SQLInstanceName $SQLInstanceName + + <# + Choose the encryption algorithm based on the Service Master Key length - 3DES for 2008, AES for 2012+ + Choose initialization vector (IV) length based on the algorithm + #> + switch($smk.Length) + { + 16 { $cryptoProvider = New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider } + + 32 { $cryptoProvider = New-Object System.Security.Cryptography.AESCryptoServiceProvider } + + default + { + $errorMessage = $script:localizedData.UnknownSmkSize -f $smk.Length, $SQLInstanceName + New-InvalidResultException -Message $errorMessage + } + } + + $cryptoProvider.Padding = 'PKCS7' + $cryptoProvider.Mode = 'CBC' + $ivLen = $cryptoProvider.IV.length + + $queryToGetEncryptedCredential = " + SELECT credential_identity AS username,substring(imageval,5,$ivLen) AS iv, substring(imageval,$($ivLen + 5),len(imageval)-$($ivLen + 4)) AS enc_message + FROM sys.credentials as cred + INNER JOIN sys.sysobjvalues AS obj + ON cred.credential_id = obj.objid + WHERE valclass=28 and valnum=2 and objid=$CredentialId + " + + Write-Verbose -Message ($script:localizedData.GetEncryptedCredential -f $CredentialId, $SQLInstanceName) -Verbose + $credInfo = (Invoke-Query @invokeQueryParameters -Query $queryToGetEncryptedCredential).Tables[0] + + $message = New-Object Byte[]($credInfo.enc_message.Length) + $decryptor = $cryptoProvider.CreateDecryptor($smk, $credInfo.iv) + $memoryStream = New-Object System.IO.MemoryStream (,$credInfo.enc_message) + $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read) + $messageLength = $cryptoStream.Read($message, 0, $message.Length) + $cryptoStream.Close() + $memoryStream.Close() + $cryptoProvider.Clear() + + <# + verifying magic number using bytes from 0 to 3 + according to https://blogs.msdn.microsoft.com/sqlsecurity/2009/03/30/sql-server-encryptbykey-cryptographic-message-description/ + #> + if ([System.BitConverter]::ToString($message[3..0]) -ne 'BA-AD-F0-0D') + { + Write-Warning -Message $script:localizedData.FailedCredentialDecryption + } + # getting password length using bytes at 6 and 7 + $len = [System.BitConverter]::ToInt16($message[6..7],0) + + # Convert password to secure string + $securePassword = New-Object System.Security.SecureString + [char[]][System.Text.Encoding]::Unicode.GetString($message, 8, $len) | ForEach-Object {$securePassword.AppendChar($_)} + + return New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($credInfo.username, $securePassword) +} + $script:localizedData = Get-LocalizedData -ResourceName 'SqlServerDsc.Common' -ScriptRoot $PSScriptRoot Export-ModuleMember -Function @( @@ -2465,4 +2731,7 @@ Export-ModuleMember -Function @( 'New-InvalidResultException' 'New-NotImplementedException' 'Get-LocalizedData' + 'Get-MailServerCredentialId' + 'Get-ServiceMasterKey' + 'Get-SqlPSCredential' ) diff --git a/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 index 2d30b55a8d..82f7bcbd9f 100644 --- a/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 +++ b/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 @@ -56,4 +56,11 @@ ConvertFrom-StringData @' ClusterLoginPermissionsPresent = The cluster login '{0}' has the required permissions. (SQLCOMMON0053) ConnectingUsingCredentials = Connecting using the credential '{0}' and the login type '{1}'. (SQLCOMMON0054) CredentialsNotSpecified = The Logon type of '{0}' was specified which requires credentials, but the credentials parameter was not specified. (SQLCOMMON0055) + DisconnectFromSQLInstance = Disconnected from '{0}' instance. (SQLCOMMON0056) + GetMailServerCredentialId = Getting credential Id used by SMTP mail server '{0}' on instance '{1}'. (SQLCOMMON0057) + GetServiceMasterKey = Getting service master key on instance '{0}'. (SQLCOMMON0058) + GetEntropyForSqlInstance = Getting entropy information from the registry for instance '{0}'. (SQLCOMMON0059) + GetEncryptedCredential = Getting encrypted password for credential with id '{0}' on instance '{1}'. (SQLCOMMON0060) + UnknownSmkSize = Unknown size '{0}' of Service Master Key for instance '{1}'. Valid values are '16' and '32'. (SQLCOMMON0061) + FailedCredentialDecryption = Decryption possibly failed or encrypted string was modfiied as magic number doesn't match. (SQLCOMMON0062) '@ diff --git a/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 index 32633eb8db..38f4f4d471 100644 --- a/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 +++ b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 @@ -62,4 +62,11 @@ ConvertFrom-StringData @' ClusterLoginPermissionsPresent = The cluster login '{0}' has the required permissions. (SQLCOMMON0053) ConnectingUsingCredentials = Connecting using the credential '{0}' and the login type '{1}'. (SQLCOMMON0054) CredentialsNotSpecified = The Logon type of '{0}' was specified which requires credentials, but the credentials parameter was not specified. (SQLCOMMON0055) + DisconnectFromSQLInstance = Disconnected from '{0}' instance. (SQLCOMMON0056) + GetMailServerCredentialId = Getting credential Id used by SMTP mail server '{0}' on instance '{1}'. (SQLCOMMON0057) + GetServiceMasterKey = Getting service master key on instance '{0}'. (SQLCOMMON0058) + GetEntropyForSqlInstance = Getting entropy information from the registry for instance '{0}'. (SQLCOMMON0059) + GetEncryptedCredential = Getting encrypted password for credential with id '{0}' on instance '{1}'. (SQLCOMMON0060) + UnknownSmkSize = Unknown size '{0}' of Service Master Key for instance '{1}'. Valid values are '16' and '32'. (SQLCOMMON0061) + FailedCredentialDecryption = Decryption possibly failed or encrypted string was modfiied as magic number doesn't match. (SQLCOMMON0062) '@ diff --git a/README.md b/README.md index 3fedb632b8..e333b6ae54 100644 --- a/README.md +++ b/README.md @@ -1195,6 +1195,12 @@ Resource to manage SQL Server Database Mail. * **`[String]` LoggingLevel** _(Write)_: The logging level that the Database Mail will use. If not specified the default logging level is 'Extended'. { Normal | *Extended* | Verbose }. +* **`[Boolean]` EnableSsl** _(Write)_: Specifies whether to encrypt communication using Secure Sockets Layer or not. +* **`[String]` Authentication** _(Write)_: SMTP authentication mode to be used. Default value is 'Anonymous'. Valid values are: + * *Windows* : Credentials of the SQL Server Database Engine will be used to authenticate against SMTP server. + * *Basic* : Credentials specified using parameter **SMTPAccount** will be used to authenticate against SMTP server. + * *Anonymous* : No credentials will be used for authentication. +* **`[PSCredential]` SMTPAccount** _(Write)_: Account used for 'Basic' SMTP authentication setup. #### Examples diff --git a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 index 781aa30a54..dd568c7e49 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 @@ -78,16 +78,21 @@ try -and $_.ResourceId -eq $resourceId } - $resourceCurrentState.Ensure | Should -Be 'Present' - $resourceCurrentState.AccountName | Should -Be $ConfigurationData.AllNodes.AccountName - $resourceCurrentState.ProfileName | Should -Be $ConfigurationData.AllNodes.ProfileName - $resourceCurrentState.EmailAddress | Should -Be $ConfigurationData.AllNodes.EmailAddress + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.AccountName | Should -Be $ConfigurationData.AllNodes.AccountName + $resourceCurrentState.ProfileName | Should -Be $ConfigurationData.AllNodes.ProfileName + $resourceCurrentState.EmailAddress | Should -Be $ConfigurationData.AllNodes.EmailAddress $resourceCurrentState.ReplyToAddress | Should -Be $ConfigurationData.AllNodes.EmailAddress - $resourceCurrentState.DisplayName | Should -Be $ConfigurationData.AllNodes.MailServerName + $resourceCurrentState.DisplayName | Should -Be $ConfigurationData.AllNodes.MailServerName $resourceCurrentState.MailServerName | Should -Be $ConfigurationData.AllNodes.MailServerName - $resourceCurrentState.Description | Should -Be $ConfigurationData.AllNodes.Description - $resourceCurrentState.LoggingLevel | Should -Be $ConfigurationData.AllNodes.LoggingLevel - $resourceCurrentState.TcpPort | Should -Be $ConfigurationData.AllNodes.TcpPort + $resourceCurrentState.Description | Should -Be $ConfigurationData.AllNodes.Description + $resourceCurrentState.LoggingLevel | Should -Be $ConfigurationData.AllNodes.LoggingLevel + $resourceCurrentState.TcpPort | Should -Be $ConfigurationData.AllNodes.TcpPort + $resourceCurrentState.EnableSsl | Should -Be $ConfigurationData.AllNodes.EnableSsl + $resourceCurrentState.Authentication | Should -Be $ConfigurationData.AllNodes.Authentication + + $resourceCurrentState.SMTPAccount.UserName | Should -BeNullOrEmpty + $resourceCurrentState.SMTPAccount.GetNetworkCredential().Password | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { @@ -133,16 +138,19 @@ try -and $_.ResourceId -eq $resourceId } - $resourceCurrentState.Ensure | Should -Be 'Absent' - $resourceCurrentState.AccountName | Should -BeNullOrEmpty - $resourceCurrentState.ProfileName | Should -BeNullOrEmpty - $resourceCurrentState.EmailAddress | Should -BeNullOrEmpty + $resourceCurrentState.Ensure | Should -Be 'Absent' + $resourceCurrentState.AccountName | Should -BeNullOrEmpty + $resourceCurrentState.ProfileName | Should -BeNullOrEmpty + $resourceCurrentState.EmailAddress | Should -BeNullOrEmpty $resourceCurrentState.ReplyToAddress | Should -BeNullOrEmpty - $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty $resourceCurrentState.MailServerName | Should -BeNullOrEmpty - $resourceCurrentState.Description | Should -BeNullOrEmpty - $resourceCurrentState.LoggingLevel | Should -BeNullOrEmpty - $resourceCurrentState.TcpPort | Should -BeNullOrEmpty + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.LoggingLevel | Should -BeNullOrEmpty + $resourceCurrentState.TcpPort | Should -BeNullOrEmpty + $resourceCurrentState.EnableSsl | Should -BeNullOrEmpty + $resourceCurrentState.Authentication | Should -BeNullOrEmpty + $resourceCurrentState.SMTPAccount | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { diff --git a/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 index 4083b13701..170c9e8e8c 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 @@ -31,6 +31,11 @@ else Description = 'Default mail account and profile.' LoggingLevel = 'Normal' TcpPort = 25 + EnableSsl = $true + Authentication = 'Basic' + + SMTPUser = 'testUser' + SMTPPassword = 'testP@$sw0rd' CertificateFile = $env:DscPublicCertificatePath } @@ -74,6 +79,12 @@ Configuration MSFT_SqlServerDatabaseMail_Add_Config Description = $Node.Description LoggingLevel = $Node.LoggingLevel TcpPort = $Node.TcpPort + EnableSsl = $Node.EnableSsl + Authentication = $Node.Authentication + + SMTPAccount = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.SMTPUser, (ConvertTo-SecureString -String $Node.SMTPPassword -AsPlainText -Force)) PsDscRunAsCredential = New-Object ` -TypeName System.Management.Automation.PSCredential ` diff --git a/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 index 5d1c9eb269..aed2533bc1 100644 --- a/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 @@ -62,6 +62,41 @@ try $mockDisplayName = $mockMailServerName $mockDescription = 'My mail description' $mockTcpPort = 25 + $mockEnableSsl = $true + + $mockAuthenticationWindows = 'Windows' + $mockAuthenticationWindowsDisabled = $false + $mockAuthenticationBasic = 'Basic' + $mockAuthenticationBasicDisabled = '' + $mockAuthenticationAnonymous = 'Anonymous' + + $mockSMTPAccountAbsent = $null + $mockSMTPAccountPresent = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @('mockUser', ` + (ConvertTo-SecureString -String 'mockPassword' ` + -AsPlainText ` + -Force + ) + ) + + $mockSMTPAccountPresentDifferentUser = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @('mockAnotherUser', ` + (ConvertTo-SecureString -String 'mockPassword' ` + -AsPlainText ` + -Force + ) + ) + + $mockSMTPAccountPresentDifferentPassword = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @('mockUser', ` + (ConvertTo-SecureString -String 'mockAnotherPassword' ` + -AsPlainText ` + -Force + ) + ) $mockDatabaseMailDisabledConfigValue = 0 $mockDatabaseMailEnabledConfigValue = 1 @@ -101,12 +136,21 @@ try New-Object -TypeName Object | Add-Member -MemberType NoteProperty -Name 'Name' -Value $mockMailServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'Port' -Value $mockTcpPort -PassThru | + Add-Member -MemberType NoteProperty -Name 'EnableSsl' -Value $mockEnableSsl -PassThru | + Add-Member -MemberType NoteProperty -Name 'UseDefaultCredentials' -Value $mockDynamicAuthenticationValue -PassThru | + Add-Member -MemberType NoteProperty -Name 'UserName' -Value $mockDynamicAuthenticationAccountValue -PassThru | Add-Member -MemberType ScriptMethod -Name 'Rename' -Value { - $script:MailServerRenameMethodCallCount += 1 - } -PassThru | + $script:MailServerRenameMethodCallCount += 1 + } -PassThru | + Add-Member -MemberType ScriptMethod -Name 'SetAccount' -Value { + $script:MailServerSetAccountMethodCallCount += 1 + } -PassThru | + Add-Member -MemberType ScriptMethod -Name 'SetPassword' -Value { + $script:MailServerSetPasswordMethodCallCount += 1 + } -PassThru | Add-Member -MemberType ScriptMethod -Name 'Alter' -Value { - $script:MailServerAlterMethodCallCount += 1 - } -PassThru -Force + $script:MailServerAlterMethodCallCount += 1 + } -PassThru -Force ) } -PassThru | Add-Member -MemberType ScriptMethod -Name 'Create' -Value { @@ -199,6 +243,8 @@ try $mockDynamicDescription = $mockDescription $mockDynamicAgentMailType = $mockAgentMailTypeDatabaseMail $mockDynamicDatabaseMailProfile = $mockProfileName + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockAuthenticationBasicDisabled } BeforeEach { @@ -239,6 +285,9 @@ try $getTargetResourceResult.ReplyToAddress | Should -BeNullOrEmpty $getTargetResourceResult.Description | Should -BeNullOrEmpty $getTargetResourceResult.TcpPort | Should -BeNullOrEmpty + $getTargetResourceResult.EnableSsl | Should -BeNullOrEmpty + $getTargetResourceResult.Authentication | Should -BeNullOrEmpty + $getTargetResourceResult.SMTPAccount | Should -BeNullOrEmpty Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope It } @@ -271,11 +320,67 @@ try $getTargetResourceResult.ReplyToAddress | Should -Be $mockReplyToAddress $getTargetResourceResult.Description | Should -Be $mockDescription $getTargetResourceResult.TcpPort | Should -Be $mockTcpPort + $getTargetResourceResult.EnableSsl | Should -Be $mockEnableSsl + $getTargetResourceResult.Authentication | Should -Be $mockAuthenticationAnonymous + $getTargetResourceResult.SMTPAccount | Should -Be $mockSMTPAccountAbsent Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope It } } + Context 'When the current authentication is ''Windows''' { + BeforeAll { + $mockDynamicAuthenticationValue = -not $mockAuthenticationWindowsDisabled + } + + It 'Should return the correct value for property Authentication' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult.Authentication | Should -Be $mockAuthenticationWindows + $getTargetResourceResult.SMTPAccount | Should -Be $mockSMTPAccountAbsent + } + } + + Context 'When the current authentication is ''Basic''' { + BeforeAll { + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockSMTPAccountPresent.UserName + + Mock -CommandName Get-MailServerCredentialId ` + -ParameterFilter {$MailServerName -eq $mockMailServerName} + Mock -CommandName Get-SqlPSCredential ` + -MockWith { return $mockSMTPAccountPresent } + } + + It 'Should return the correct value for property Authentication' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + $getTargetResourceResult.Authentication | Should -Be $mockAuthenticationBasic + $getTargetResourceResult.SMTPAccount.UserName | Should -Be $mockSMTPAccountPresent.UserName + $getTargetResourceResult.SMTPAccount.GetNetworkCredential().Password | ` + Should -Be $mockSMTPAccountPresent.GetNetworkCredential().Password + + Assert-MockCalled -CommandName Get-MailServerCredentialId ` + -ParameterFilter {$MailServerName -eq $mockMailServerName} ` + -Exactly ` + -Times 1 ` + -Scope It + Assert-MockCalled -CommandName Get-SqlPSCredential -Exactly -Times 1 -Scope It + } + } + + Context 'When the current authentication is ''Anonymous''' { + BeforeAll { + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockAuthenticationBasicDisabled + } + + It 'Should return the correct value for property Authentication' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult.Authentication | Should -Be $mockAuthenticationAnonymous + $getTargetResourceResult.SMTPAccount | Should -Be $mockSMTPAccountAbsent + } + } + Context 'When the current logging level is ''Normal''' { BeforeAll { $mockDynamicLoggingLevelValue = $mockLoggingLevelNormalValue @@ -343,10 +448,16 @@ try $mockDynamicDescription = $mockDescription $mockDynamicAgentMailType = $mockAgentMailTypeDatabaseMail $mockDynamicDatabaseMailProfile = $mockProfileName + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockSMTPAccountPresent.UserName } BeforeEach { Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable + Mock -CommandName Get-MailServerCredentialId -Verifiable + Mock -CommandName Get-SqlPSCredential ` + -MockWith { return $mockSMTPAccountPresent } ` + -Verifiable $testTargetResourceParameters = $mockDefaultParameters.Clone() } @@ -373,6 +484,9 @@ try $testTargetResourceParameters['Description'] = $mockDescription $testTargetResourceParameters['LoggingLevel'] = $mockLoggingLevelExtended $testTargetResourceParameters['TcpPort'] = $mockTcpPort + $testTargetResourceParameters['EnableSsl'] = $mockEnableSsl + $testTargetResourceParameters['Authentication'] = $mockAuthenticationBasic + $testTargetResourceParameters['SMTPAccount'] = $mockSMTPAccountPresent } It 'Should return the state as $true' { @@ -409,6 +523,9 @@ try Description = $mockDescription LoggingLevel = $mockLoggingLevelExtended TcpPort = $mockTcpPort + EnableSsl = $mockEnableSsl + Authentication = $mockAuthenticationBasic + SMTPAccount = $mockSMTPAccountPresent } $testCaseAccountNameIsMissing = $defaultTestCase.Clone() @@ -447,6 +564,23 @@ try $testCaseTcpPortIsWrong['TestName'] = 'TcpPort is wrong' $testCaseTcpPortIsWrong['TcpPort'] = 2525 + $testCaseEnableSslIsWrong = $defaultTestCase.Clone() + $testCaseEnableSslIsWrong['TestName'] = 'EnableSsl is wrong' + $testCaseEnableSslIsWrong['EnableSsl'] = $false + + $testCaseAuthenticationIsWrong = $defaultTestCase.Clone() + $testCaseAuthenticationIsWrong['TestName'] = 'Authentication is wrong' + $testCaseAuthenticationIsWrong['Authentication'] = 'Windows' + + $testCaseSMTPAccountIsWrong = $defaultTestCase.Clone() + $testCaseSMTPAccountIsWrong['TestName'] = 'SMTP account is wrong' + $testCaseSMTPAccountIsWrong['Authentication'] = 'Basic' + $testCaseSMTPAccountIsWrong['SMTPAccount'] = $mockSMTPAccountPresentDifferentUser + + $testCaseSMTPAccountPasswordIsWrong = $defaultTestCase.Clone() + $testCaseSMTPAccountPasswordIsWrong['TestName'] = 'password for SMTP account is wrong' + $testCaseSMTPAccountPasswordIsWrong['SMTPAccount'] = $mockSMTPAccountPresentDifferentPassword + $testCases = @( $testCaseAccountNameIsMissing $testCaseEmailAddressIsWrong @@ -456,7 +590,11 @@ try $testCaseReplyToAddressIsWrong $testCaseDescriptionIsWrong $testCaseLoggingLevelIsWrong - $testCaseTcpPortIsWrong + $testCaseTcpPortIsWrong, + $testCaseEnableSslIsWrong, + $testCaseAuthenticationIsWrong, + $testCaseSMTPAccountIsWrong, + $testCaseSMTPAccountPasswordIsWrong ) It 'Should return the state as $false when ' -TestCases $testCases { @@ -470,7 +608,10 @@ try $ReplyToAddress, $Description, $LoggingLevel, - $TcpPort + $TcpPort, + $EnableSsl, + $Authentication, + $SMTPAccount ) $testTargetResourceParameters['AccountName'] = $AccountName @@ -482,11 +623,14 @@ try $testTargetResourceParameters['Description'] = $Description $testTargetResourceParameters['LoggingLevel'] = $LoggingLevel $testTargetResourceParameters['TcpPort'] = $TcpPort + $testTargetResourceParameters['EnableSsl'] = $EnableSsl + $testTargetResourceParameters['Authentication'] = $Authentication + $testTargetResourceParameters['SMTPAccount'] = $SMTPAccount $testTargetResourceResult = Test-TargetResource @testTargetResourceParameters $testTargetResourceResult | Should -Be $false - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Connect-SQL -Exactly -Times 1 -Scope It } } } @@ -501,6 +645,8 @@ try $mockDynamicDescription = $mockDescription $mockDynamicAgentMailType = $mockAgentMailTypeDatabaseMail $mockDynamicDatabaseMailProfile = $mockProfileName + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockSMTPAccountPresent.UserName } BeforeEach { @@ -513,10 +659,17 @@ try $TypeName -eq 'Microsoft.SqlServer.Management.SMO.Mail.MailProfile' } -Verifiable + Mock -CommandName Get-MailServerCredentialId -Verifiable + Mock -CommandName Get-SqlPSCredential ` + -MockWith { return $mockSMTPAccountPresent } ` + -Verifiable + $setTargetResourceParameters = $mockDefaultParameters.Clone() $script:MailAccountCreateMethodCallCount = 0 $script:MailServerRenameMethodCallCount = 0 + $script:MailServerSetAccountMethodCallCount = 0 + $script:MailServerSetPasswordMethodCallCount = 0 $script:MailServerAlterMethodCallCount = 0 $script:MailAccountAlterMethodCallCount = 0 $script:MailProfileCreateMethodCallCount = 0 @@ -546,6 +699,8 @@ try { Set-TargetResource @setTargetResourceParameters } | Should -Not -Throw $script:MailAccountCreateMethodCallCount | Should -Be 0 $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 0 $script:MailAccountAlterMethodCallCount | Should -Be 0 $script:MailProfileCreateMethodCallCount | Should -Be 0 @@ -568,12 +723,17 @@ try $setTargetResourceParameters['Description'] = $mockDescription $setTargetResourceParameters['LoggingLevel'] = $mockLoggingLevelExtended $setTargetResourceParameters['TcpPort'] = $mockTcpPort + $setTargetResourceParameters['EnableSsl'] = $mockEnableSsl + $setTargetResourceParameters['Authentication'] = $mockAuthenticationBasic + $setTargetResourceParameters['SMTPAccount'] = $mockSMTPAccountPresent } It 'Should call the correct methods without throwing' { { Set-TargetResource @setTargetResourceParameters } | Should -Not -Throw $script:MailAccountCreateMethodCallCount | Should -Be 0 $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 0 $script:MailAccountAlterMethodCallCount | Should -Be 0 $script:MailProfileCreateMethodCallCount | Should -Be 0 @@ -625,10 +785,14 @@ try $setTargetResourceParameters['Description'] = $mockDescription $setTargetResourceParameters['LoggingLevel'] = $mockLoggingLevelExtended $setTargetResourceParameters['TcpPort'] = $mockTcpPort + $setTargetResourceParameters['EnableSsl'] = $mockEnableSsl + $setTargetResourceParameters['Authentication'] = $mockAuthenticationBasic + $setTargetResourceParameters['SMTPAccount'] = $mockSMTPAccountPresent { Set-TargetResource @setTargetResourceParameters } | Should -Not -Throw $script:MailAccountCreateMethodCallCount | Should -Be 1 $script:MailServerRenameMethodCallCount | Should -Be 1 + $script:MailServerSetAccountMethodCallCount | Should -Be 1 $script:MailServerAlterMethodCallCount | Should -Be 1 $script:MailAccountAlterMethodCallCount | Should -Be 0 @@ -647,6 +811,9 @@ try Description = $mockDescription LoggingLevel = $mockLoggingLevelExtended TcpPort = $mockTcpPort + EnableSsl = $mockEnableSsl + Authentication = $mockAuthenticationBasic + SMTPAccount = $mockSMTPAccountPresent } $testCaseEmailAddressIsWrong = $defaultTestCase.Clone() @@ -685,6 +852,24 @@ try $testCaseTcpPortIsWrong['TestName'] = 'TcpPort is wrong' $testCaseTcpPortIsWrong['TcpPort'] = 2525 + $testCaseEnableSslIsWrong = $defaultTestCase.Clone() + $testCaseEnableSslIsWrong['TestName'] = 'EnableSsl is wrong' + $testCaseEnableSslIsWrong['EnableSsl'] = $false + + $testCaseAuthenticationIsWrong = $defaultTestCase.Clone() + $testCaseAuthenticationIsWrong['TestName'] = 'Authentication is wrong' + $testCaseAuthenticationIsWrong['Authentication'] = 'Windows' + + $testCaseSMTPAccountIsWrong = $defaultTestCase.Clone() + $testCaseSMTPAccountIsWrong['TestName'] = 'SMTP account is wrong' + $testCaseSMTPAccountIsWrong['Authentication'] = 'Basic' + $testCaseSMTPAccountIsWrong['SMTPAccount'] = $mockSMTPAccountPresentDifferentUser + + $testCaseSMTPAccountPasswordIsWrong = $defaultTestCase.Clone() + $testCaseSMTPAccountPasswordIsWrong['TestName'] = 'password for SMTP account is wrong' + $testCaseSMTPAccountPasswordIsWrong['SMTPAccount'] = $mockSMTPAccountPresentDifferentPassword + + $testCases = @( $testCaseEmailAddressIsWrong $testCaseMailServerNameIsWrong @@ -694,7 +879,11 @@ try $testCaseDescriptionIsWrong $testCaseLoggingLevelIsWrong_Normal $testCaseLoggingLevelIsWrong_Verbose - $testCaseTcpPortIsWrong + $testCaseTcpPortIsWrong, + $testCaseEnableSslIsWrong, + $testCaseAuthenticationIsWrong, + $testCaseSMTPAccountIsWrong, + $testCaseSMTPAccountPasswordIsWrong ) It 'Should return the state as $false when ' -TestCases $testCases { @@ -709,7 +898,10 @@ try $ReplyToAddress, $Description, $LoggingLevel, - $TcpPort + $TcpPort, + $EnableSsl, + $Authentication, + $SMTPAccount ) $setTargetResourceParameters['AccountName'] = $AccountName @@ -721,6 +913,9 @@ try $setTargetResourceParameters['Description'] = $Description $setTargetResourceParameters['LoggingLevel'] = $LoggingLevel $setTargetResourceParameters['TcpPort'] = $TcpPort + $setTargetResourceParameters['EnableSsl'] = $EnableSsl + $setTargetResourceParameters['Authentication'] = $Authentication + $setTargetResourceParameters['SMTPAccount'] = $SMTPAccount { Set-TargetResource @setTargetResourceParameters } | Should -Not -Throw @@ -729,6 +924,8 @@ try if ($TestName -like '*MailServerName*') { $script:MailServerRenameMethodCallCount | Should -Be 1 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 1 $script:MailAccountAlterMethodCallCount | Should -Be 0 $script:MailProfileCreateMethodCallCount | Should -Be 0 @@ -738,9 +935,11 @@ try $script:JobServerAlterMethodCallCount | Should -Be 0 $script:LoggingLevelAlterMethodCallCount | Should -Be 0 } - elseif ($TestName -like '*TcpPort*') + elseif ($TestName -match 'TcpPort|EnableSsl|Authentication') { $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 1 $script:MailAccountAlterMethodCallCount | Should -Be 0 $script:MailProfileCreateMethodCallCount | Should -Be 0 @@ -750,9 +949,39 @@ try $script:JobServerAlterMethodCallCount | Should -Be 0 $script:LoggingLevelAlterMethodCallCount | Should -Be 0 } + elseif ($TestName -like 'SMTP account*') + { + $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 1 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 + $script:MailServerAlterMethodCallCount | Should -Be 0 + $script:MailAccountAlterMethodCallCount | Should -Be 0 + $script:MailProfileCreateMethodCallCount | Should -Be 0 + $script:MailProfileAlterMethodCallCount | Should -Be 0 + $script:MailProfileAddPrincipalMethodCallCount | Should -Be 0 + $script:MailProfileAddAccountMethodCallCount | Should -Be 0 + $script:JobServerAlterMethodCallCount | Should -Be 0 + $script:LoggingLevelAlterMethodCallCount | Should -Be 0 + } + elseif ($TestName -like 'password*') + { + $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 1 + $script:MailServerAlterMethodCallCount | Should -Be 0 + $script:MailAccountAlterMethodCallCount | Should -Be 0 + $script:MailProfileCreateMethodCallCount | Should -Be 0 + $script:MailProfileAlterMethodCallCount | Should -Be 0 + $script:MailProfileAddPrincipalMethodCallCount | Should -Be 0 + $script:MailProfileAddAccountMethodCallCount | Should -Be 0 + $script:JobServerAlterMethodCallCount | Should -Be 0 + $script:LoggingLevelAlterMethodCallCount | Should -Be 0 + } elseif ($TestName -like '*ProfileName*') { $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 0 $script:MailAccountAlterMethodCallCount | Should -Be 0 $script:MailProfileCreateMethodCallCount | Should -Be 1 @@ -765,6 +994,8 @@ try elseif ($TestName -like '*LoggingLevel*') { $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 0 $script:MailAccountAlterMethodCallCount | Should -Be 0 $script:MailProfileCreateMethodCallCount | Should -Be 0 @@ -777,6 +1008,8 @@ try else { $script:MailServerRenameMethodCallCount | Should -Be 0 + $script:MailServerSetAccountMethodCallCount | Should -Be 0 + $script:MailServerSetPasswordMethodCallCount | Should -Be 0 $script:MailServerAlterMethodCallCount | Should -Be 0 $script:MailAccountAlterMethodCallCount | Should -Be 1 $script:MailProfileCreateMethodCallCount | Should -Be 0 @@ -787,7 +1020,9 @@ try $script:LoggingLevelAlterMethodCallCount | Should -Be 0 } - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Connect-SQL -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Get-MailServerCredentialId -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Get-SqlPSCredential -Exactly -Times 1 -Scope It } } } @@ -801,4 +1036,3 @@ finally { Invoke-TestCleanup } - diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 3ca5efd7a1..77da4b952b 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -31,6 +31,31 @@ Import-Module -Name (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath ' InModuleScope 'SqlServerDsc.Common' { Describe 'SqlServerDsc.Common\Test-DscParameterState' -Tag 'TestDscParameterState' { Context -Name 'When passing values' -Fixture { + + $mockUser = New-Object -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @('mockUser', ` + (ConvertTo-SecureString -String 'mockPassword' ` + -AsPlainText ` + -Force + ) + ) + $mockAnotherUser = New-Object -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @('mockAnotherUser', ` + (ConvertTo-SecureString -String 'mockPassword' ` + -AsPlainText ` + -Force + ) + ) + + $mockUserDifferentPassword = New-Object -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @('mockUser', ` + (ConvertTo-SecureString ` + -String 'mockDifferentPassword' ` + -AsPlainText ` + -Force + ) + ) + It 'Should return true for two identical tables' { $mockDesiredValues = @{ Example = 'test' } @@ -42,6 +67,17 @@ InModuleScope 'SqlServerDsc.Common' { Test-DscParameterState @testParameters | Should -Be $true } + It 'Should return true for two identical PSCredential objects' { + $mockDesiredValues = @{ Example = $mockUser } + + $testParameters = @{ + CurrentValues = $mockDesiredValues + DesiredValues = $mockDesiredValues + } + + Test-DscParameterState @testParameters | Should -Be $true + } + It 'Should return false when a value is different for [System.String]' { $mockCurrentValues = @{ Example = [System.String] 'something' } $mockDesiredValues = @{ Example = [System.String] 'test' } @@ -90,6 +126,30 @@ InModuleScope 'SqlServerDsc.Common' { Test-DscParameterState @testParameters | Should -Be $false } + It 'Should return false when a UserName is different for [PSCredential]' { + $mockCurrentValues = @{ Example = $mockUser } + $mockDesiredValues = @{ Example = $mockAnotherUser } + + $testParameters = @{ + CurrentValues = $mockCurrentValues + DesiredValues = $mockDesiredValues + } + + Test-DscParameterState @testParameters | Should -Be $false + } + + It 'Should return false when a Password is different for [PSCredential]' { + $mockCurrentValues = @{ Example = $mockUser } + $mockDesiredValues = @{ Example = $mockUserDifferentPassword } + + $testParameters = @{ + CurrentValues = $mockCurrentValues + DesiredValues = $mockDesiredValues + } + + Test-DscParameterState @testParameters | Should -Be $false + } + It 'Should return false when a value is different for [Boolean]' { $mockCurrentValues = @{ Example = [System.Boolean] $true } $mockDesiredValues = @{ Example = [System.Boolean] $false } @@ -1389,12 +1449,15 @@ InModuleScope 'SqlServerDsc.Common' { Describe 'SqlServerDsc.Common\Invoke-Query' -Tag 'InvokeQuery' { BeforeAll { $mockExpectedQuery = '' + $mockExpectedInstance = 'MSSQLSERVER' $mockSetupCredentialUserName = 'TestUserName12345' $mockSetupCredentialPassword = 'StrongOne7.' $mockSetupCredentialSecurePassword = ConvertTo-SecureString -String $mockSetupCredentialPassword -AsPlainText -Force $mockSetupCredential = New-Object -TypeName PSCredential -ArgumentList ($mockSetupCredentialUserName, $mockSetupCredentialSecurePassword) + + $masterDatabaseObject = New-Object -TypeName PSObject $masterDatabaseObject | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'master' $masterDatabaseObject | Add-Member -MemberType ScriptMethod -Name 'ExecuteNonQuery' -Value { @@ -1427,17 +1490,27 @@ InModuleScope 'SqlServerDsc.Common' { return New-Object -TypeName System.Data.DataSet } + $connectionContextObject = New-Object -TypeName PSObject + $connectionContextObject | Add-Member -MemberType NoteProperty -Name 'ServerInstance' -Value 'mockSQLInstance' + $connectionContextObject | Add-Member -MemberType ScriptMethod -Name 'Disconnect' -Value { + $script:ConnectionContextDisconnectMethodCallCount += 1 + } + $databasesObject = New-Object -TypeName PSObject + $databasesObject | Add-Member -MemberType NoteProperty -Name 'ConnectionContext' -Value $connectionContextObject $databasesObject | Add-Member -MemberType NoteProperty -Name 'Databases' -Value @{ 'master' = $masterDatabaseObject } + $mockSMOServer = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' $mockSMOServer | Add-Member -MemberType NoteProperty -Name 'Databases' -Value @{ 'master' = $masterDatabaseObject } -Force $mockConnectSql = { + $databasesObject.ConnectionContext.ServerInstance = @('','ADMIN:')[$DAC.IsPresent] + $ServerName + ` + @('',"\$InstanceName")[$InstanceName -ne 'MSSQLSERVER'] return @($databasesObject) } @@ -1447,6 +1520,8 @@ InModuleScope 'SqlServerDsc.Common' { } BeforeEach { + $script:ConnectionContextDisconnectMethodCallCount = 0 + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable } @@ -1460,9 +1535,9 @@ InModuleScope 'SqlServerDsc.Common' { } $queryParametersWithSMO = @{ - Query = '' SqlServerObject = $mockSMOServer - Database = 'master' + Database = 'master' + Query = '' } Context 'Execute a query with no results' { @@ -1507,6 +1582,30 @@ InModuleScope 'SqlServerDsc.Common' { } } + Context 'Execute a query with results using Dedicated Admin Connection and close the connection' { + It 'Should execute the query and return a result set' { + $queryParams.Query = 'SELECT name FROM sys.databases' + $mockExpectedQuery = $queryParams.Query.Clone() + + Invoke-Query @queryParams -WithResults -UsingDAC | Should -Not -BeNullOrEmpty + $script:ConnectionContextDisconnectMethodCallCount | Should -Be 1 + + Assert-MockCalled -CommandName Connect-SQL ` + -ParameterFilter { $DAC -eq $true } ` + -Scope It ` + -Times 1 ` + -Exactly + } + + It 'Should throw the correct error, ExecuteQueryWithResultsFailed, when executing the query fails' { + $queryParams.Query = 'BadQuery' + + { Invoke-Query @queryParams -WithResults -UsingDAC } | Should -Throw ($script:localizedData.ExecuteQueryWithResultsFailed -f $queryParams.Database) + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + } + Context 'Pass in an SMO Server Object' { Context 'Execute a query with no results' { It 'Should execute the query silently' { @@ -2290,7 +2389,7 @@ InModuleScope 'SqlServerDsc.Common' { But since the mock New-Object will also be called without arguments, we first have to evaluate if $ArgumentList contains values. #> - if( $ArgumentList.Count -gt 0) + if ($ArgumentList.Count -gt 0) { $serverInstance = $ArgumentList[0] } @@ -2299,11 +2398,11 @@ InModuleScope 'SqlServerDsc.Common' { Add-Member -MemberType ScriptProperty -Name Status -Value { if ($mockExpectedDatabaseEngineInstance -eq 'MSSQLSERVER') { - $mockExpectedServiceInstance = $mockExpectedDatabaseEngineServer + $mockExpectedServiceInstance = @('','ADMIN:')[$mockDAC] + $mockExpectedDatabaseEngineServer } else { - $mockExpectedServiceInstance = "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + $mockExpectedServiceInstance = @('','ADMIN:')[$mockDAC] + "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" } if ( $this.ConnectionContext.ServerInstance -eq $mockExpectedServiceInstance ) @@ -2329,14 +2428,15 @@ InModuleScope 'SqlServerDsc.Common' { Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | + Add-Member -MemberType NoteProperty -Name NonPooledConnection -Value $false -PassThru | Add-Member -MemberType ScriptMethod -Name Connect -Value { if ($mockExpectedDatabaseEngineInstance -eq 'MSSQLSERVER') { - $mockExpectedServiceInstance = $mockExpectedDatabaseEngineServer + $mockExpectedServiceInstance = @('','ADMIN:')[$mockDAC] + $mockExpectedDatabaseEngineServer } else { - $mockExpectedServiceInstance = "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + $mockExpectedServiceInstance = @('','ADMIN:')[$mockDAC] + "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" } if ($this.serverInstance -ne $mockExpectedServiceInstance) @@ -2379,6 +2479,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should return the correct service instance' { $mockExpectedDatabaseEngineServer = 'TestServer' $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + $mockDAC = $false $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly $mockExpectedDatabaseEngineServer @@ -2393,6 +2494,7 @@ InModuleScope 'SqlServerDsc.Common' { $mockExpectedDatabaseEngineServer = 'TestServer' $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' $mockExpectedDatabaseEngineLoginSecure = $false + $mockDAC = $false $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -SetupCredential $mockSetupCredential -LoginType 'SqlLogin' $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -Be $false @@ -2405,10 +2507,26 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'When connecting to the named instance using integrated Windows Authentication' { + Context 'When establishing non-pooled Dedicated Admin Connection to the default instance using Windows Authentication' { + It 'Should return the correct service instance' { + $mockExpectedDatabaseEngineServer = 'TestServer' + $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + $mockDAC = $true + + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -DAC + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "ADMIN:$mockExpectedDatabaseEngineServer" + $databaseEngineServerObject.ConnectionContext.NonPooledConnection | Should -BeExactly 'True' + + Assert-MockCalled -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When connecting to the named instance integrated using Windows Authentication' { It 'Should return the correct service instance' { $mockExpectedDatabaseEngineServer = $env:COMPUTERNAME $mockExpectedDatabaseEngineInstance = $mockInstanceName + $mockDAC = $false $databaseEngineServerObject = Connect-SQL -InstanceName $mockExpectedDatabaseEngineInstance $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" @@ -2423,6 +2541,7 @@ InModuleScope 'SqlServerDsc.Common' { $mockExpectedDatabaseEngineServer = $env:COMPUTERNAME $mockExpectedDatabaseEngineInstance = $mockInstanceName $mockExpectedDatabaseEngineLoginSecure = $false + $mockDAC = $false $databaseEngineServerObject = Connect-SQL -InstanceName $mockExpectedDatabaseEngineInstance -SetupCredential $mockSetupCredential -LoginType 'SqlLogin' $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -Be $false @@ -2439,6 +2558,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should return the correct service instance' { $mockExpectedDatabaseEngineServer = 'SERVER' $mockExpectedDatabaseEngineInstance = $mockInstanceName + $mockDAC = $false $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName $mockExpectedDatabaseEngineInstance $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" @@ -2452,6 +2572,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should return the correct service instance' { $mockExpectedDatabaseEngineServer = $env:COMPUTERNAME $mockExpectedDatabaseEngineInstance = $mockInstanceName + $mockDAC = $false $testParameters = @{ ServerName = $mockExpectedDatabaseEngineServer @@ -2476,6 +2597,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should throw the correct error' { $mockExpectedDatabaseEngineServer = $env:COMPUTERNAME $mockExpectedDatabaseEngineInstance = $mockInstanceName + $mockDAC = $false Mock -CommandName New-Object ` -MockWith $mockNewObject_MicrosoftDatabaseEngine ` @@ -3144,5 +3266,265 @@ InModuleScope 'SqlServerDsc.Common' { Assert-VerifiableMock } -} + Describe 'DscResource.Common\Get-MailServerCredentialId' -Tag 'GetMailServerCredentialId' { + BeforeAll { + $mockConnectSql = { + return @( + ( + New-Object -TypeName PSObject -Property @{ + Databases = @{ + 'master' = ( + New-Object -TypeName PSObject -Property @{ Name = 'master' } | + Add-Member -MemberType ScriptMethod -Name ExecuteWithResults -Value { + param + ( + [Parameter()] + [System.String] + $sqlCommand + ) + + if ( $sqlCommand -notlike $mockExpectedQuery ) + { + throw + } + + return [pscustomobject] @{ Tables = @{ credential_id = $mockCredentialId } } + } -PassThru + ) + } + } + ) + ) + } + + $mockThrowLocalizedMessage = { + throw $Message + } + + $queryParams = @{ + SQLServer = 'Server1' + SQLInstanceName = 'MSSQLSERVER' + MailServerName = 'smtp.account.local' + } + + $mockCredentialId = 65555 + } + + BeforeEach { + $mockExpectedQuery = "*SELECT credential_id* FROM msdb.dbo.sysmail_server* WHERE servername = '$($queryParams.MailServerName)'*" + + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable + Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable + } + + Context 'Get credential id' { + It 'Should execute the query and return a credential id' { + Get-MailServerCredentialId @queryParams | Should -Be $mockCredentialId + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + + It 'Should throw the correct error, ExecuteQueryWithResultsFailed, when executing the query fails' { + $blockQueryParams = $queryParams.Clone() + $blockQueryParams.MailServerName = 'Nonexistent mail server' + + { Get-MailServerCredentialId @blockQueryParams } | Should -Throw ($script:localizedData.ExecuteQueryWithResultsFailed -f 'master') + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + } + } + + Describe 'DscResource.Common\Get-ServiceMasterKey' -Tag 'GetServiceMasterKey' { + BeforeAll { + $mockExpectedQuery = '*FROM sys.key_encryptions* WHERE key_id=102 and (thumbprint=0x03 or thumbprint=0x0300000001)*' + + $mockConnectSql = { + return @( + ( + New-Object -TypeName PSObject -Property @{ + Databases = @{ + 'master' = ( + New-Object -TypeName PSObject -Property @{ Name = 'master' } | + Add-Member -MemberType ScriptMethod -Name ExecuteWithResults -Value { + param + ( + [Parameter()] + [System.String] + $sqlCommand + ) + + if ( $sqlCommand -notlike $mockExpectedQuery ) + { + throw + } + + return [pscustomobject] @{ Tables = @( @{ ServiceMasterKey = $mockEncryptedSMK } ) } + } -PassThru + ) + } + } + ) + ) + } + + $queryParams = @{ + SQLServer = 'Server1' + SQLInstanceName = 'MSSQLSERVER' + } + + $mockServiceInstanceId = 'MSSQL14.MSSQLSERVER' + + Add-Type -AssemblyName System.Security + $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('123456789') + $mockEntropy = [System.Text.Encoding]::UTF8.GetBytes('Entropy') + $mockEncryptedSMK = [System.Security.Cryptography.ProtectedData]::Protect($mockSMK, $mockEntropy, 'LocalMachine') + } + + BeforeEach { + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable + Mock -CommandName Get-ItemProperty ` + -MockWith { return [pscustomobject]@{ $queryParams.SQLInstanceName = $mockServiceInstanceId} } ` + -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' } ` + -Verifiable + + Mock -CommandName Get-ItemProperty ` + -MockWith { return [pscustomobject]@{ Entropy = $mockEntropy } } ` + -ParameterFilter { $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$mockServiceInstanceId\Security" } ` + -Verifiable + } + + Context 'Get unencrypted service master key' { + It 'Should execute the query, decrypt and return a key' { + Get-ServiceMasterKey @queryParams | Should -Be $mockSMK + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Get-ItemProperty -Scope It -Times 2 -Exactly + } + } + } + + Describe 'DscResource.Common\Get-SqlPSCredential' -Tag 'Get-SqlPSCredential' { + BeforeAll { + $mockConnectSql = { + return @( + ( + New-Object -TypeName PSObject -Property @{ + Databases = @{ + 'master' = ( + New-Object -TypeName PSObject -Property @{ Name = 'master' } | + Add-Member -MemberType ScriptMethod -Name ExecuteWithResults -Value { + param + ( + [Parameter()] + [System.String] + $sqlCommand + ) + + if ( $sqlCommand -notlike $mockExpectedQuery ) + { + throw + } + + return [pscustomobject] @{ Tables = @( + @{ + username = $mockUser + iv = $mockIV + enc_message = $mockEnc_message + } + ) + } + } -PassThru + ) + } + } + ) + ) + } + + $mockThrowLocalizedMessage = { + throw $Message + } + + $queryParams = @{ + SQLServer = 'Server1' + SQLInstanceName = 'MSSQLSERVER' + CredentialId = 65555 + } + + $mockUser = 'mockUser' + $mockPassword = 'VerySecurePa$$w0rd' + $mockUnicodePassword = [System.Text.Encoding]::Unicode.GetBytes($mockPassword) + } + + BeforeEach { + Mock -CommandName Get-ServiceMasterKey -MockWith { return $mockSMK } -Verifiable + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable + Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable + + $mockExpectedQuery = "*FROM sys.credentials as cred* INNER JOIN sys.sysobjvalues AS obj* ON cred.credential_id = obj.objid* WHERE valclass=28 and valnum=2 and objid=$($queryParams.CredentialId)*" + } + + Context 'Get SQL credential as PSCredential' { + It 'Should execute the query, decrypt using 3DES and return a PSCredential object' { + $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOP') + $cryptoProvider = New-Object -TypeName System.Security.Cryptography.TripleDESCryptoServiceProvider + $cryptoProvider.Padding = 'PKCS7' + $cryptoProvider.Mode = 'CBC' + $cryptoProvider.Key = $mockSMK + $mockIV = $cryptoProvider.IV + $encryptor = $cryptoProvider.CreateEncryptor() + $memoryStream = New-Object System.IO.MemoryStream + $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) + $innerMessage = [byte[]]('0x0D','0xF0', '0xAD', '0xBA') + [BitConverter]::GetBytes([int16]0) + [BitConverter]::GetBytes([int16]$mockUnicodePassword.Length) + $mockUnicodePassword + $cryptoStream.Write($innerMessage, 0, $innerMessage.Length) + $cryptoStream.Close() + $memoryStream.Close() + $cryptoProvider.Clear() + $mockEnc_message = $memoryStream.ToArray() + + $result = Get-SqlPSCredential @queryParams + + $result.UserName | Should -Be $mockUser + $result.GetNetworkCredential().Password | Should -Be $mockPassword + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + + It 'Should execute the query, decrypt using AES and return a PSCredential object' { + $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345') + $cryptoProvider = New-Object System.Security.Cryptography.AESCryptoServiceProvider + $cryptoProvider.Padding = 'PKCS7' + $cryptoProvider.Mode = 'CBC' + $cryptoProvider.Key = $mockSMK + $mockIV = $cryptoProvider.IV + $encryptor = $cryptoProvider.CreateEncryptor() + $memoryStream = New-Object System.IO.MemoryStream + $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) + $innerMessage = [byte[]]('0x0D','0xF0', '0xAD', '0xBA') + [BitConverter]::GetBytes([int16]0) + [BitConverter]::GetBytes([int16]$mockUnicodePassword.Length) + $mockUnicodePassword + $cryptoStream.Write($innerMessage, 0, $innerMessage.Length) + $cryptoStream.Close() + $memoryStream.Close() + $cryptoProvider.Clear() + $mockEnc_message = $memoryStream.ToArray() + + $result = Get-SqlPSCredential @queryParams + + $result.UserName | Should -Be $mockUser + $result.GetNetworkCredential().Password | Should -Be $mockPassword + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + + It 'Should throw the correct error' { + $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('012345') + + { Get-SqlPSCredential @queryParams} | Should -Throw ($script:localizedData.UnknownSmkSize -f $mockSMK.Length, $queryParams.SQLInstanceName) + + Assert-MockCalled -CommandName Get-ServiceMasterKey -Scope It -Exactly 1 + Assert-MockCalled -CommandName Connect-SQL -Scope It -Exactly 0 + } + } + } +} From dee4f50acbf37d72d04b7638e1d4a665721d4e7f Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Thu, 6 Jun 2019 23:40:59 +0300 Subject: [PATCH 02/10] 2 --- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 77da4b952b..20f07f1b01 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -3364,6 +3364,11 @@ InModuleScope 'SqlServerDsc.Common' { } -PassThru ) } + ConnectionContext = New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value $result -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { + $script:ConnectionContextDisconnectMethodCallCount += 1 + } -PassThru -Force } ) ) @@ -3408,6 +3413,16 @@ InModuleScope 'SqlServerDsc.Common' { Describe 'DscResource.Common\Get-SqlPSCredential' -Tag 'Get-SqlPSCredential' { BeforeAll { $mockConnectSql = { + + if ($InstanceName -eq 'MSSQLSERVER') + { + $result = @('','ADMIN:')[$DAC.IsPresent] + $ServerName + } + else + { + $result = @('','ADMIN:')[$DAC.IsPresent] + "$ServerName\$InstanceName" + } + return @( ( New-Object -TypeName PSObject -Property @{ From 2e5623dbbbd67f6d3ddb39683bef9cbfb4dbbeb3 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Fri, 7 Jun 2019 00:42:47 +0300 Subject: [PATCH 03/10] fix tests --- CHANGELOG.md | 26 +++++++++++++------ .../SqlServerDsc.Common.psm1 | 10 ++++++- README.md | 15 +++++++---- ...qlServerDatabaseMail.Integration.Tests.ps1 | 2 +- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 376508bad2..1a9f6a75b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,14 +87,24 @@ name list may have been returned as a string instead of as a string array ([issue #1368](https://github.com/PowerShell/SqlServerDsc/issues/1368)). - Changes to SqlServerDatabaseMail - - Added new parameter `EnableSsl` which controls encryption of communication using Secure Sockets Layer. - - Added new parameter `Authentication` and `SMTPAccount` for configuration of SMTP authentication mode and credential used [issue #1215](https://github.com/PowerShell/SqlServerDsc/issues/1215). - - Added new helper function `Get-MailServerCredentialId` which gets credential Id used by mail server. - - Added new helper function `Get-ServiceMasterKey` which gets unencrypted Service Master Key for specified SQL Instance. - - Added new helper function `Get-SqlPSCredential` which decrypts and returns PSCredential object of SQL Credential by Id. - - Added DAC switch to function 'Connect-SQL' with default value $false. Used to specify that non-pooled Dedicated Admin Connection should be established. - - Added UsingDAC switch to function 'Invoke-Query' with default value $false. Used to specify that query should be executed using Dedicated Admin Connection. After execution DAC connection will be closed. - - Added PSCredential option to function 'Test-DscParameterState' which will compare PSCredential objects using username/password keys. + - Added new parameter `EnableSsl` which controls encryption of communication + using Secure Sockets Layer. + - Added new parameter `Authentication` and `SMTPAccount` for configuration of + SMTP authentication mode and credential used [issue #1215](https://github.com/PowerShell/SqlServerDsc/issues/1215). + - Added new helper function `Get-MailServerCredentialId` which gets + credential Id used by mail server. + - Added new helper function `Get-ServiceMasterKey` which gets unencrypted + Service Master Key for specified SQL Instance. + - Added new helper function `Get-SqlPSCredential` which decrypts and returns + PSCredential object of SQL Credential by Id. + - Added DAC switch to function 'Connect-SQL' with default value $false. + Used to specify that non-pooled Dedicated Admin Connection should be + established. + - Added UsingDAC switch to function 'Invoke-Query' with default value $false. + Used to specify that query should be executed using Dedicated Admin + Connection. After execution DAC connection will be closed. + - Added PSCredential option to function 'Test-DscParameterState' which will + compare PSCredential objects using username/password keys. ## 12.5.0.0 diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index 2117be30c4..649ee0c3f3 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -1746,7 +1746,15 @@ function Invoke-Query } catch { - $errorMessage = $script:localizedData.$(@('ExecuteNonQueryFailed', 'ExecuteQueryWithResultsFailed')[$WithResults.IsPresent]) -f $Database + if ($WithResults.IsPresent) + { + $errorMessage = $script:localizedData.ExecuteQueryWithResultsFailed -f $Database + } + else + { + $errorMessage = $script:localizedData.ExecuteNonQueryFailed -f $Database + } + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } finally diff --git a/README.md b/README.md index e333b6ae54..cfe8060844 100644 --- a/README.md +++ b/README.md @@ -1195,12 +1195,17 @@ Resource to manage SQL Server Database Mail. * **`[String]` LoggingLevel** _(Write)_: The logging level that the Database Mail will use. If not specified the default logging level is 'Extended'. { Normal | *Extended* | Verbose }. -* **`[Boolean]` EnableSsl** _(Write)_: Specifies whether to encrypt communication using Secure Sockets Layer or not. -* **`[String]` Authentication** _(Write)_: SMTP authentication mode to be used. Default value is 'Anonymous'. Valid values are: - * *Windows* : Credentials of the SQL Server Database Engine will be used to authenticate against SMTP server. - * *Basic* : Credentials specified using parameter **SMTPAccount** will be used to authenticate against SMTP server. +* **`[Boolean]` EnableSsl** _(Write)_: Specifies whether to encrypt + communication using Secure Sockets Layer or not. +* **`[String]` Authentication** _(Write)_: SMTP authentication mode to be used. + Default value is 'Anonymous'. Valid values are: + * *Windows* : Credentials of the SQL Server Database Engine will be used to + authenticate against SMTP server. + * *Basic* : Credentials specified using parameter **SMTPAccount** will be + used to authenticate against SMTP server. * *Anonymous* : No credentials will be used for authentication. -* **`[PSCredential]` SMTPAccount** _(Write)_: Account used for 'Basic' SMTP authentication setup. +* **`[PSCredential]` SMTPAccount** _(Write)_: Account used for 'Basic' SMTP + authentication setup. #### Examples diff --git a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 index dd568c7e49..3914bab28c 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 @@ -92,7 +92,7 @@ try $resourceCurrentState.Authentication | Should -Be $ConfigurationData.AllNodes.Authentication $resourceCurrentState.SMTPAccount.UserName | Should -BeNullOrEmpty - $resourceCurrentState.SMTPAccount.GetNetworkCredential().Password | Should -BeNullOrEmpty + $resourceCurrentState.SMTPAccount.Password | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { From 584ea4f8e086afe55f73f2f71fde61db309a246a Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Tue, 11 Jun 2019 11:20:20 +0300 Subject: [PATCH 04/10] fixes #1 --- .../MSFT_SqlServerDatabaseMail.psm1 | 16 ++- .../1-EnableDatabaseMail.ps1 | 12 +- .../2-EnableDatabaseMailWithSsl.ps1 | 69 +++++++++ ...3-EnableDatabaseMailWithAuthentication.ps1 | 75 ++++++++++ ...baseMail.ps1 => 4-DisableDatabaseMail.ps1} | 7 +- .../SqlServerDsc.Common.psm1 | 80 ++++++----- .../en-US/SqlServerDsc.Common.strings.psd1 | 4 +- README.md | 4 +- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 133 +++++++----------- 9 files changed, 256 insertions(+), 144 deletions(-) create mode 100644 Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 create mode 100644 Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 rename Examples/Resources/SqlServerDatabaseMail/{2-DisableDatabaseMail.ps1 => 4-DisableDatabaseMail.ps1} (91%) diff --git a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 index 4502307e63..ab67446796 100644 --- a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 +++ b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 @@ -442,12 +442,13 @@ function Set-TargetResource if ($PSBoundParameters.ContainsKey('Authentication')) { # Default Authentication is Anonymous so it's absent in the selection. - switch($Authentication) + switch ($Authentication) { 'Basic' { $mailServer.SetAccount($SMTPAccount.UserName, $SMTPAccount.Password) } + 'Windows' { $mailServer.UseDefaultCredentials = $true @@ -605,10 +606,17 @@ function Set-TargetResource ) ) - $mailServer.UseDefaultCredentials = switch($Authentication) + $mailServer.UseDefaultCredentials = switch ($Authentication) { - 'Windows' { $true } - Default { $false } + 'Windows' + { + $true + } + + Default + { + $false + } } if ($Authentication -ne 'Basic' -and $currentSMTPAccount.UserName) diff --git a/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 b/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 index da307adebc..ceed036ee1 100644 --- a/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 @@ -18,8 +18,6 @@ $ConfigurationData = @{ Description = 'Default mail account and profile.' LoggingLevel = 'Normal' TcpPort = 25 - EnableSsl = $true - Authentication = 'Basic' } ) } @@ -31,12 +29,7 @@ Configuration Example [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] - $SqlInstallCredential, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] - $SMTPAccountCredential + $SqlInstallCredential ) Import-DscResource -ModuleName 'SqlServerDsc' @@ -66,9 +59,6 @@ Configuration Example Description = $Node.Description LoggingLevel = $Node.LoggingLevel TcpPort = $Node.TcpPort - EnableSsl = $Node.EnableSsl - Authentication = $Node.Authentication - SMTPAccount = $SMTPAccountCredential PsDscRunAsCredential = $SqlInstallCredential } diff --git a/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 b/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 new file mode 100644 index 0000000000..d969da9fee --- /dev/null +++ b/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 @@ -0,0 +1,69 @@ +<# + .EXAMPLE + This example will enable Database Mail on a SQL Server instance and + create a mail account with a default public profile. + +#> +$ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + ServerName = $env:COMPUTERNAME + InstanceName = 'DSCSQLTEST' + + MailServerName = 'mail.company.local' + AccountName = 'MyMail' + ProfileName = 'MyMailProfile' + EmailAddress = 'NoReply@company.local' + Description = 'Default mail account and profile.' + LoggingLevel = 'Normal' + TcpPort = 25 + EnableSsl = $true + } + ) +} + +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SqlInstallCredential + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost { + + SqlServerConfiguration 'EnableDatabaseMailXPs' + { + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + OptionName = 'Database Mail XPs' + OptionValue = 1 + RestartService = $false + } + + SqlServerDatabaseMail 'EnableDatabaseMail' + { + Ensure = 'Present' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + AccountName = $Node.AccountName + ProfileName = $Node.ProfileName + EmailAddress = $Node.EmailAddress + ReplyToAddress = $Node.EmailAddress + DisplayName = $Node.MailServerName + MailServerName = $Node.MailServerName + Description = $Node.Description + LoggingLevel = $Node.LoggingLevel + TcpPort = $Node.TcpPort + EnableSsl = $Node.EnableSsl + + PsDscRunAsCredential = $SqlInstallCredential + } + } +} + diff --git a/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 b/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 new file mode 100644 index 0000000000..e24c3dddb9 --- /dev/null +++ b/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 @@ -0,0 +1,75 @@ +<# + .EXAMPLE + This example will enable Database Mail on a SQL Server instance and + create a mail account with a default public profile. + +#> +$ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + ServerName = $env:COMPUTERNAME + InstanceName = 'DSCSQLTEST' + + MailServerName = 'mail.company.local' + AccountName = 'MyMail' + ProfileName = 'MyMailProfile' + EmailAddress = 'NoReply@company.local' + Description = 'Default mail account and profile.' + LoggingLevel = 'Normal' + TcpPort = 25 + Authentication = 'Basic' + } + ) +} + +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SqlInstallCredential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SMTPAccountCredential + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost { + + SqlServerConfiguration 'EnableDatabaseMailXPs' + { + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + OptionName = 'Database Mail XPs' + OptionValue = 1 + RestartService = $false + } + + SqlServerDatabaseMail 'EnableDatabaseMail' + { + Ensure = 'Present' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + AccountName = $Node.AccountName + ProfileName = $Node.ProfileName + EmailAddress = $Node.EmailAddress + ReplyToAddress = $Node.EmailAddress + DisplayName = $Node.MailServerName + MailServerName = $Node.MailServerName + Description = $Node.Description + LoggingLevel = $Node.LoggingLevel + TcpPort = $Node.TcpPort + Authentication = $Node.Authentication + SMTPAccount = $SMTPAccountCredential + + PsDscRunAsCredential = $SqlInstallCredential + } + } +} + diff --git a/Examples/Resources/SqlServerDatabaseMail/2-DisableDatabaseMail.ps1 b/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 similarity index 91% rename from Examples/Resources/SqlServerDatabaseMail/2-DisableDatabaseMail.ps1 rename to Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 index ab58510a11..96a1b2d93a 100644 --- a/Examples/Resources/SqlServerDatabaseMail/2-DisableDatabaseMail.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 @@ -15,9 +15,6 @@ $ConfigurationData = @{ AccountName = 'MyMail' ProfileName = 'MyMailProfile' EmailAddress = 'NoReply@company.local' - Description = 'Default mail account and profile.' - LoggingLevel = 'Normal' - TcpPort = 25 } ) } @@ -34,7 +31,8 @@ Configuration Example Import-DscResource -ModuleName 'SqlServerDsc' - node localhost { + Node localhost { + SqlServerDatabaseMail 'DisableDatabaseMail' { Ensure = 'Absent' @@ -54,7 +52,6 @@ Configuration Example #> SqlServerConfiguration 'DisableDatabaseMailXPs' { - ServerName = $Node.ServerName InstanceName = $Node.InstanceName OptionName = 'Database Mail XPs' diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index 649ee0c3f3..b1fa55e205 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -2548,9 +2548,8 @@ function Get-MailServerCredentialId String containing the SQL Server Database Engine instance to connect to. .NOTES - This function is based on Antti Rantasaari's script at https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1 - Antti Rantasaari 2014, NetSPI - License: BSD 3-Clause http://opensource.org/licenses/BSD-3-Clause + Details on Service Master Key protection could be found at + https://blogs.msdn.microsoft.com/stuartpa/2005/09/25/protecting-the-sql-server-2005-service-master-key-the-root-of-all-encryption/ #> function Get-ServiceMasterKey { @@ -2578,20 +2577,19 @@ function Get-ServiceMasterKey } $queryToGetServiceMasterKey = " - SELECT substring(crypt_property,9,len(crypt_property)-8) AS ServiceMasterKey + SELECT crypt_property AS 'key' FROM sys.key_encryptions - WHERE key_id=102 and (thumbprint=0x03 or thumbprint=0x0300000001) + WHERE key_id=102 and thumbprint<>0x01 " Write-Verbose -Message ($script:localizedData.GetServiceMasterKey -f $SQLInstanceName) -Verbose - $smkEncryptedBytes = (Invoke-Query @invokeQueryParameters -Query $queryToGetServiceMasterKey).Tables[0].ServiceMasterKey + $encryptedServiceMasterKey = (Invoke-Query @invokeQueryParameters -Query $queryToGetServiceMasterKey).Tables[0].key | Select-Object -Skip 8 Write-Verbose -Message ($script:localizedData.GetEntropyForSqlInstance -f $SQLInstanceName) -Verbose - $sqlInstanceId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -ErrorAction Stop).$SQLInstanceName - [byte[]]$entropy = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$sqlInstanceId\Security" -ErrorAction Stop).Entropy + $instanceId = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -Name $SQLInstanceName + $entropy = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceId\Security" -Name 'Entropy' - # Decrypt the service master key - $serviceMasterKey = [System.Security.Cryptography.ProtectedData]::Unprotect($smkEncryptedBytes, $entropy, 'LocalMachine') + $serviceMasterKey = [System.Security.Cryptography.ProtectedData]::Unprotect($encryptedServiceMasterKey, $entropy, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) return $serviceMasterKey } @@ -2608,9 +2606,9 @@ function Get-ServiceMasterKey .PARAMETER CredentialId Specifies Id of the credential for which MSFT_Credential should be returned. - .NOTES - This function is partialy based on Antti Rantasaari's script at https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1 + Details on cryptographic message description could be found at + https://blogs.msdn.microsoft.com/sqlsecurity/2009/03/30/sql-server-encryptbykey-cryptographic-message-description/ #> function Get-SqlPSCredential { @@ -2643,56 +2641,60 @@ function Get-SqlPSCredential $smk = Get-ServiceMasterKey -SQLServer $SQLServer -SQLInstanceName $SQLInstanceName - <# - Choose the encryption algorithm based on the Service Master Key length - 3DES for 2008, AES for 2012+ - Choose initialization vector (IV) length based on the algorithm - #> - switch($smk.Length) + switch ($smk.Length) { - 16 { $cryptoProvider = New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider } + 16 + { + $typeName = 'System.Security.Cryptography.TripleDESCryptoServiceProvider' + } - 32 { $cryptoProvider = New-Object System.Security.Cryptography.AESCryptoServiceProvider } + 32 + { + $typeName = 'System.Security.Cryptography.AESCryptoServiceProvider' + } default { - $errorMessage = $script:localizedData.UnknownSmkSize -f $smk.Length, $SQLInstanceName + $errorMessage = $script:localizedData.SmkSizeNotImplemented -f $smk.Length, $SQLInstanceName New-InvalidResultException -Message $errorMessage } } - $cryptoProvider.Padding = 'PKCS7' - $cryptoProvider.Mode = 'CBC' - $ivLen = $cryptoProvider.IV.length - - $queryToGetEncryptedCredential = " - SELECT credential_identity AS username,substring(imageval,5,$ivLen) AS iv, substring(imageval,$($ivLen + 5),len(imageval)-$($ivLen + 4)) AS enc_message - FROM sys.credentials as cred - INNER JOIN sys.sysobjvalues AS obj - ON cred.credential_id = obj.objid + # Get encrypted message without encryption header which is 4 bytes + $queryToGetEncryptedMessage = " + SELECT credential_identity AS username, substring(imageval, 5, len(imageval)-4) AS enc_message + FROM sys.credentials as c + INNER JOIN sys.sysobjvalues AS o + ON c.credential_id = o.objid WHERE valclass=28 and valnum=2 and objid=$CredentialId " - Write-Verbose -Message ($script:localizedData.GetEncryptedCredential -f $CredentialId, $SQLInstanceName) -Verbose - $credInfo = (Invoke-Query @invokeQueryParameters -Query $queryToGetEncryptedCredential).Tables[0] + Write-Verbose -Message ($script:localizedData.GetEncryptedMessage -f $CredentialId, $SQLInstanceName) -Verbose + $credInfo = (Invoke-Query @invokeQueryParameters -Query $queryToGetEncryptedMessage).Tables[0] - $message = New-Object Byte[]($credInfo.enc_message.Length) - $decryptor = $cryptoProvider.CreateDecryptor($smk, $credInfo.iv) - $memoryStream = New-Object System.IO.MemoryStream (,$credInfo.enc_message) + $cryptoProvider = New-Object -TypeName $typeName + $cryptoProvider.Key = $smk + # Extract and set Initialization Vector (IV) + $cryptoProvider.IV = $credInfo.enc_message[0..$($cryptoProvider.IV.Length - 1)] + $cryptoProvider.Padding = 'PKCS7' + $cryptoProvider.Mode = 'CBC' + + # Set length of the inner message + $message = New-Object Byte[]($credInfo.enc_message.Length - $cryptoProvider.IV.Length) + $decryptor = $cryptoProvider.CreateDecryptor() + $memoryStream = New-Object System.IO.MemoryStream (,$credInfo.enc_message[$cryptoProvider.IV.Length..($credInfo.enc_message.Length - 1)]) $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read) $messageLength = $cryptoStream.Read($message, 0, $message.Length) $cryptoStream.Close() $memoryStream.Close() $cryptoProvider.Clear() - <# - verifying magic number using bytes from 0 to 3 - according to https://blogs.msdn.microsoft.com/sqlsecurity/2009/03/30/sql-server-encryptbykey-cryptographic-message-description/ - #> + # Verifying magic number using bytes from 0 to 3 if ([System.BitConverter]::ToString($message[3..0]) -ne 'BA-AD-F0-0D') { Write-Warning -Message $script:localizedData.FailedCredentialDecryption } - # getting password length using bytes at 6 and 7 + # Getting password length using bytes 6 and 7 $len = [System.BitConverter]::ToInt16($message[6..7],0) # Convert password to secure string diff --git a/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 index 82f7bcbd9f..bd6442f1d4 100644 --- a/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 +++ b/Modules/SqlServerDsc.Common/en-US/SqlServerDsc.Common.strings.psd1 @@ -60,7 +60,7 @@ ConvertFrom-StringData @' GetMailServerCredentialId = Getting credential Id used by SMTP mail server '{0}' on instance '{1}'. (SQLCOMMON0057) GetServiceMasterKey = Getting service master key on instance '{0}'. (SQLCOMMON0058) GetEntropyForSqlInstance = Getting entropy information from the registry for instance '{0}'. (SQLCOMMON0059) - GetEncryptedCredential = Getting encrypted password for credential with id '{0}' on instance '{1}'. (SQLCOMMON0060) - UnknownSmkSize = Unknown size '{0}' of Service Master Key for instance '{1}'. Valid values are '16' and '32'. (SQLCOMMON0061) + GetEncryptedMessage = Getting encrypted message for credential with id '{0}' on instance '{1}'. (SQLCOMMON0060) + SmkSizeNotImplemented = Unknown size '{0}' of Service Master Key for instance '{1}'. Valid values are '16' and '32'. (SQLCOMMON0061) FailedCredentialDecryption = Decryption possibly failed or encrypted string was modfiied as magic number doesn't match. (SQLCOMMON0062) '@ diff --git a/README.md b/README.md index cfe8060844..d536255354 100644 --- a/README.md +++ b/README.md @@ -1210,7 +1210,9 @@ Resource to manage SQL Server Database Mail. #### Examples * [Enable Database Mail](/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1) -* [Disable Database Mail](/Examples/Resources/SqlServerDatabaseMail/2-DisableDatabaseMail.ps1) +* [Enable Database Mail with SSL](/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1) +* [Enable Database Mail with Basic Authentication](/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1) +* [Disable Database Mail](/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1) #### Known issues diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 20f07f1b01..3e8dbbf1db 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -1004,7 +1004,7 @@ InModuleScope 'SqlServerDsc.Common' { } Describe 'SqlServerDsc.Common\Restart-SqlService' -Tag 'RestartSqlService' { - Context 'Restart-SqlService standalone instance' { + Context 'When Restart-SqlService operates on a standalone instance' { BeforeEach { Mock -CommandName Connect-SQL -MockWith { return @{ @@ -1210,7 +1210,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Restart-SqlService clustered instance' { + Context 'When Restart-SqlService operates on a clustered instance' { BeforeEach { Mock -CommandName Connect-SQL -MockWith { return @{ @@ -1340,10 +1340,6 @@ InModuleScope 'SqlServerDsc.Common' { $TypeName -eq 'Microsoft.AnalysisServices.Server' } - $mockThrowLocalizedMessage = { - throw $Message - } - $mockSetupCredentialUserName = 'TestUserName12345' $mockSetupCredentialPassword = 'StrongOne7.' $mockSetupCredentialSecurePassword = ConvertTo-SecureString -String $mockSetupCredentialPassword -AsPlainText -Force @@ -1351,7 +1347,6 @@ InModuleScope 'SqlServerDsc.Common' { } BeforeEach { - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable Mock -CommandName New-Object ` -MockWith $mockNewObject_MicrosoftAnalysisServicesServer ` -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter ` @@ -1513,17 +1508,12 @@ InModuleScope 'SqlServerDsc.Common' { @('',"\$InstanceName")[$InstanceName -ne 'MSSQLSERVER'] return @($databasesObject) } - - $mockThrowLocalizedMessage = { - throw $Message - } } BeforeEach { $script:ConnectionContextDisconnectMethodCallCount = 0 Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable } $queryParams = @{ @@ -1540,7 +1530,7 @@ InModuleScope 'SqlServerDsc.Common' { Query = '' } - Context 'Execute a query with no results' { + Context 'When a query is executed and nothing is returned' { It 'Should execute the query silently' { $queryParams.Query = "EXEC sp_configure 'show advanced option', '1'" $mockExpectedQuery = $queryParams.Query.Clone() @@ -1561,7 +1551,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Execute a query with results' { + Context 'When a query is executed and results are returned' { It 'Should execute the query and return a result set' { $queryParams.Query = 'SELECT name FROM sys.databases' $mockExpectedQuery = $queryParams.Query.Clone() @@ -1582,7 +1572,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Execute a query with results using Dedicated Admin Connection and close the connection' { + Context 'When a query is executed using Dedicated Admin Connection and connection was closed' { It 'Should execute the query and return a result set' { $queryParams.Query = 'SELECT name FROM sys.databases' $mockExpectedQuery = $queryParams.Query.Clone() @@ -1900,17 +1890,12 @@ InModuleScope 'SqlServerDsc.Common' { $mockGetModule_SQLPS_ParameterFilter = { $FullyQualifiedName.Name -eq 'SQLPS' -and $ListAvailable -eq $true } - - $mockThrowLocalizedMessage = { - throw $Message - } } BeforeEach { Mock -CommandName Push-Location -Verifiable Mock -CommandName Pop-Location -Verifiable Mock -CommandName Import-Module -MockWith $mockImportModule -Verifiable - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable } Context 'When module SqlServer is already loaded into the session' { @@ -2456,10 +2441,6 @@ InModuleScope 'SqlServerDsc.Common' { $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' } - $mockThrowLocalizedMessage = { - throw $Message - } - $mockSetupCredentialUserName = 'TestUserName12345' $mockSetupCredentialPassword = 'StrongOne7.' $mockSetupCredentialSecurePassword = ConvertTo-SecureString -String $mockSetupCredentialPassword -AsPlainText -Force @@ -2467,7 +2448,6 @@ InModuleScope 'SqlServerDsc.Common' { } BeforeEach { - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable Mock -CommandName Import-SQLPSModule Mock -CommandName New-Object ` -MockWith $mockNewObject_MicrosoftDatabaseEngine ` @@ -2932,7 +2912,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Invoke-SqlScript fails to import SQLPS module' { + Context 'When Invoke-SqlScript fails to import SQLPS module' { $throwMessage = "Failed to import SQLPS module." Mock -CommandName Import-SQLPSModule -MockWith { @@ -2944,7 +2924,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Invoke-SqlScript is called with credentials' { + Context 'When Invoke-SqlScript is called with credentials' { BeforeAll { $mockPasswordPlain = 'password' $mockUsername = 'User' @@ -2977,7 +2957,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Invoke-SqlScript fails to execute the SQL scripts' { + Context 'When Invoke-SqlScript fails to execute the SQL scripts' { $errorMessage = 'Failed to run SQL Script' Mock -CommandName Import-SQLPSModule -MockWith {} @@ -3298,10 +3278,6 @@ InModuleScope 'SqlServerDsc.Common' { ) } - $mockThrowLocalizedMessage = { - throw $Message - } - $queryParams = @{ SQLServer = 'Server1' SQLInstanceName = 'MSSQLSERVER' @@ -3315,10 +3291,9 @@ InModuleScope 'SqlServerDsc.Common' { $mockExpectedQuery = "*SELECT credential_id* FROM msdb.dbo.sysmail_server* WHERE servername = '$($queryParams.MailServerName)'*" Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable } - Context 'Get credential id' { + Context 'When credential id is returned' { It 'Should execute the query and return a credential id' { Get-MailServerCredentialId @queryParams | Should -Be $mockCredentialId @@ -3338,7 +3313,7 @@ InModuleScope 'SqlServerDsc.Common' { Describe 'DscResource.Common\Get-ServiceMasterKey' -Tag 'GetServiceMasterKey' { BeforeAll { - $mockExpectedQuery = '*FROM sys.key_encryptions* WHERE key_id=102 and (thumbprint=0x03 or thumbprint=0x0300000001)*' + $mockExpectedQuery = '*FROM sys.key_encryptions* WHERE key_id=102 and thumbprint<>0x01*' $mockConnectSql = { return @( @@ -3360,7 +3335,7 @@ InModuleScope 'SqlServerDsc.Common' { throw } - return [pscustomobject] @{ Tables = @( @{ ServiceMasterKey = $mockEncryptedSMK } ) } + return [pscustomobject] @{ Tables = @( @{ key = [BitConverter]::GetBytes([int64]0) + $mockEncryptedSMK } ) } } -PassThru ) } @@ -3389,23 +3364,23 @@ InModuleScope 'SqlServerDsc.Common' { BeforeEach { Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable - Mock -CommandName Get-ItemProperty ` - -MockWith { return [pscustomobject]@{ $queryParams.SQLInstanceName = $mockServiceInstanceId} } ` - -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' } ` + Mock -CommandName Get-ItemPropertyValue ` + -MockWith { return $mockServiceInstanceId } ` + -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -and $Name -eq $queryParams.SQLInstanceName } ` -Verifiable - Mock -CommandName Get-ItemProperty ` - -MockWith { return [pscustomobject]@{ Entropy = $mockEntropy } } ` + Mock -CommandName Get-ItemPropertyValue ` + -MockWith { return $mockEntropy } ` -ParameterFilter { $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$mockServiceInstanceId\Security" } ` -Verifiable } - Context 'Get unencrypted service master key' { + Context 'When unencrypted service master key is returned' { It 'Should execute the query, decrypt and return a key' { Get-ServiceMasterKey @queryParams | Should -Be $mockSMK Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly - Assert-MockCalled -CommandName Get-ItemProperty -Scope It -Times 2 -Exactly + Assert-MockCalled -CommandName Get-ItemPropertyValue -Scope It -Times 2 -Exactly } } } @@ -3442,11 +3417,38 @@ InModuleScope 'SqlServerDsc.Common' { throw } + switch ($mockSMK.Length) + { + 16 + { + $typeName = 'System.Security.Cryptography.TripleDESCryptoServiceProvider' + } + + 32 + { + $typeName = 'System.Security.Cryptography.AESCryptoServiceProvider' + } + } + + $cryptoProvider = New-Object -TypeName $typeName + $cryptoProvider.Padding = 'PKCS7' + $cryptoProvider.Mode = 'CBC' + $cryptoProvider.Key = $mockSMK + $mockIV = $cryptoProvider.IV + $encryptor = $cryptoProvider.CreateEncryptor() + $memoryStream = New-Object System.IO.MemoryStream + $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) + $innerMessage = [byte[]]('0x0D','0xF0', '0xAD', '0xBA') + [BitConverter]::GetBytes([int16]0) + [BitConverter]::GetBytes([int16]$mockUnicodePassword.Length) + $mockUnicodePassword + $cryptoStream.Write($innerMessage, 0, $innerMessage.Length) + $cryptoStream.Close() + $memoryStream.Close() + $cryptoProvider.Clear() + $mockEnc_message = $memoryStream.ToArray() + return [pscustomobject] @{ Tables = @( @{ username = $mockUser - iv = $mockIV - enc_message = $mockEnc_message + enc_message = $mockIV + $mockEnc_message } ) } @@ -3458,10 +3460,6 @@ InModuleScope 'SqlServerDsc.Common' { ) } - $mockThrowLocalizedMessage = { - throw $Message - } - $queryParams = @{ SQLServer = 'Server1' SQLInstanceName = 'MSSQLSERVER' @@ -3476,28 +3474,13 @@ InModuleScope 'SqlServerDsc.Common' { BeforeEach { Mock -CommandName Get-ServiceMasterKey -MockWith { return $mockSMK } -Verifiable Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable - $mockExpectedQuery = "*FROM sys.credentials as cred* INNER JOIN sys.sysobjvalues AS obj* ON cred.credential_id = obj.objid* WHERE valclass=28 and valnum=2 and objid=$($queryParams.CredentialId)*" + $mockExpectedQuery = "*FROM sys.credentials as c* INNER JOIN sys.sysobjvalues AS o* ON c.credential_id = o.objid* WHERE valclass=28 and valnum=2 and objid=$($queryParams.CredentialId)*" } - Context 'Get SQL credential as PSCredential' { + Context 'When SQL credential is returned as PSCredential' { It 'Should execute the query, decrypt using 3DES and return a PSCredential object' { - $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOP') - $cryptoProvider = New-Object -TypeName System.Security.Cryptography.TripleDESCryptoServiceProvider - $cryptoProvider.Padding = 'PKCS7' - $cryptoProvider.Mode = 'CBC' - $cryptoProvider.Key = $mockSMK - $mockIV = $cryptoProvider.IV - $encryptor = $cryptoProvider.CreateEncryptor() - $memoryStream = New-Object System.IO.MemoryStream - $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) - $innerMessage = [byte[]]('0x0D','0xF0', '0xAD', '0xBA') + [BitConverter]::GetBytes([int16]0) + [BitConverter]::GetBytes([int16]$mockUnicodePassword.Length) + $mockUnicodePassword - $cryptoStream.Write($innerMessage, 0, $innerMessage.Length) - $cryptoStream.Close() - $memoryStream.Close() - $cryptoProvider.Clear() - $mockEnc_message = $memoryStream.ToArray() + $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOP') $result = Get-SqlPSCredential @queryParams @@ -3508,21 +3491,7 @@ InModuleScope 'SqlServerDsc.Common' { } It 'Should execute the query, decrypt using AES and return a PSCredential object' { - $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345') - $cryptoProvider = New-Object System.Security.Cryptography.AESCryptoServiceProvider - $cryptoProvider.Padding = 'PKCS7' - $cryptoProvider.Mode = 'CBC' - $cryptoProvider.Key = $mockSMK - $mockIV = $cryptoProvider.IV - $encryptor = $cryptoProvider.CreateEncryptor() - $memoryStream = New-Object System.IO.MemoryStream - $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) - $innerMessage = [byte[]]('0x0D','0xF0', '0xAD', '0xBA') + [BitConverter]::GetBytes([int16]0) + [BitConverter]::GetBytes([int16]$mockUnicodePassword.Length) + $mockUnicodePassword - $cryptoStream.Write($innerMessage, 0, $innerMessage.Length) - $cryptoStream.Close() - $memoryStream.Close() - $cryptoProvider.Clear() - $mockEnc_message = $memoryStream.ToArray() + $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345') $result = Get-SqlPSCredential @queryParams From 0df529376fab8cce1c49bccad6fc30d0c137c054 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Tue, 11 Jun 2019 12:51:56 +0300 Subject: [PATCH 05/10] change se strings --- .../sv-SE/SqlServerDsc.Common.strings.psd1 | 4 ++-- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 index 38f4f4d471..dcbd17df40 100644 --- a/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 +++ b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 @@ -66,7 +66,7 @@ ConvertFrom-StringData @' GetMailServerCredentialId = Getting credential Id used by SMTP mail server '{0}' on instance '{1}'. (SQLCOMMON0057) GetServiceMasterKey = Getting service master key on instance '{0}'. (SQLCOMMON0058) GetEntropyForSqlInstance = Getting entropy information from the registry for instance '{0}'. (SQLCOMMON0059) - GetEncryptedCredential = Getting encrypted password for credential with id '{0}' on instance '{1}'. (SQLCOMMON0060) - UnknownSmkSize = Unknown size '{0}' of Service Master Key for instance '{1}'. Valid values are '16' and '32'. (SQLCOMMON0061) + GetEncryptedMessage = Getting encrypted message for credential with id '{0}' on instance '{1}'. (SQLCOMMON0060) + SmkSizeNotImplemented = Unknown size '{0}' of Service Master Key for instance '{1}'. Valid values are '16' and '32'. (SQLCOMMON0061) FailedCredentialDecryption = Decryption possibly failed or encrypted string was modfiied as magic number doesn't match. (SQLCOMMON0062) '@ diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 3e8dbbf1db..7e4798c18c 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -3504,7 +3504,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should throw the correct error' { $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('012345') - { Get-SqlPSCredential @queryParams} | Should -Throw ($script:localizedData.UnknownSmkSize -f $mockSMK.Length, $queryParams.SQLInstanceName) + { Get-SqlPSCredential @queryParams} | Should -Throw ($script:localizedData.SmkSizeNotImplemented -f $mockSMK.Length, $queryParams.SQLInstanceName) Assert-MockCalled -CommandName Get-ServiceMasterKey -Scope It -Exactly 1 Assert-MockCalled -CommandName Connect-SQL -Scope It -Exactly 0 From eec7a816c409139ef9c4b33d9f76ff957a9a6403 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Wed, 12 Jun 2019 19:19:10 +0300 Subject: [PATCH 06/10] added comments --- .../SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 | 2 +- .../2-EnableDatabaseMailWithSsl.ps1 | 7 +++---- .../3-EnableDatabaseMailWithAuthentication.ps1 | 8 ++++---- .../SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 | 2 +- .../MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 | 5 ++++- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 10 ++++++++++ 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 b/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 index ceed036ee1..b15fa79427 100644 --- a/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/1-EnableDatabaseMail.ps1 @@ -22,7 +22,7 @@ $ConfigurationData = @{ ) } -Configuration Example +Configuration EnableDatabaseMail { param ( diff --git a/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 b/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 index d969da9fee..fd6e55b1f8 100644 --- a/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 @@ -1,8 +1,7 @@ <# .EXAMPLE - This example will enable Database Mail on a SQL Server instance and - create a mail account with a default public profile. - + This example will enable Database Mail with SSL on a SQL Server + instance and create a mail account with a default public profile. #> $ConfigurationData = @{ AllNodes = @( @@ -23,7 +22,7 @@ $ConfigurationData = @{ ) } -Configuration Example +Configuration EnableDatabaseMailWithSsl { param ( diff --git a/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 b/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 index e24c3dddb9..8519ccd85e 100644 --- a/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 @@ -1,8 +1,8 @@ <# .EXAMPLE - This example will enable Database Mail on a SQL Server instance and - create a mail account with a default public profile. - + This example will enable Database Mail with Basic Authentication on a + SQL Server instance and create a mail account with a default public + profile. #> $ConfigurationData = @{ AllNodes = @( @@ -23,7 +23,7 @@ $ConfigurationData = @{ ) } -Configuration Example +Configuration EnableDatabaseMailWithAuthentication { param ( diff --git a/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 b/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 index 96a1b2d93a..da76787df5 100644 --- a/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 @@ -19,7 +19,7 @@ $ConfigurationData = @{ ) } -Configuration Example +Configuration DisableDatabaseMail { param ( diff --git a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 index 3914bab28c..956aab6a6d 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 @@ -90,7 +90,10 @@ try $resourceCurrentState.TcpPort | Should -Be $ConfigurationData.AllNodes.TcpPort $resourceCurrentState.EnableSsl | Should -Be $ConfigurationData.AllNodes.EnableSsl $resourceCurrentState.Authentication | Should -Be $ConfigurationData.AllNodes.Authentication - + <# + Possibly LCM protects credentials objects and instead of real values NULL is returned. + In the same time unit tests for Get() function shows correct results. + #> $resourceCurrentState.SMTPAccount.UserName | Should -BeNullOrEmpty $resourceCurrentState.SMTPAccount.Password | Should -BeNullOrEmpty } diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 7e4798c18c..b72deb7371 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -3417,6 +3417,12 @@ InModuleScope 'SqlServerDsc.Common' { throw } + <# + Code below will encrypt mock password so that function Get-SqlPSCredential() can + proceed with its decryption the same way it will work on SQL Server. As part of the + mocking process, key and IV which were used for password encryption will be passed + to the result along with encrypted password. + #> switch ($mockSMK.Length) { 16 @@ -3480,6 +3486,8 @@ InModuleScope 'SqlServerDsc.Common' { Context 'When SQL credential is returned as PSCredential' { It 'Should execute the query, decrypt using 3DES and return a PSCredential object' { + + # Generating 128 bit 3DES key which will be used for password encryption and decryption $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOP') $result = Get-SqlPSCredential @queryParams @@ -3491,6 +3499,8 @@ InModuleScope 'SqlServerDsc.Common' { } It 'Should execute the query, decrypt using AES and return a PSCredential object' { + + # Generating 256 bit AES key which will be used for password encryption and decryption $mockSMK = [System.Text.Encoding]::UTF8.GetBytes('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345') $result = Get-SqlPSCredential @queryParams From 5cc74d34dba39243061572befd0c957f3d61b5f5 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Wed, 12 Jun 2019 20:40:10 +0300 Subject: [PATCH 07/10] notice #1 --- Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index b1fa55e205..04bb8e14ae 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -2541,6 +2541,11 @@ function Get-MailServerCredentialId Gets unencrypted Service Master Key for specified SQL Instance which will be used for credential password decryption. + .DESCRIPTION + Submission containing materials of a third party: Antti Rantasaari 2014, NetSPI + License: BSD 3-Clause https://opensource.org/licenses/BSD-3-Clause + Source: https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1 + .PARAMETER SQLServer String containing the host name of the SQL Server to connect to. From 6bd943a03b7c2d52f96482b227ebca224ecc3546 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Thu, 13 Jun 2019 16:38:29 +0300 Subject: [PATCH 08/10] added key property 'accountId' into filter for mail server credential Id --- .../MSFT_SqlServerDatabaseMail.psm1 | 6 ++++-- .../SqlServerDsc.Common/SqlServerDsc.Common.psm1 | 14 +++++++++++--- Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 | 6 ++++-- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 5 +++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 index ab67446796..75ebf396fd 100644 --- a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 +++ b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 @@ -165,7 +165,8 @@ function Get-TargetResource $credentialId = Get-MailServerCredentialId -SQLServer $ServerName ` -SQLInstanceName $InstanceName ` - -MailServerName $mailServer.Name + -MailServerName $mailServer.Name ` + -AccountId $databaseMailAccount.ID $returnValue['SMTPAccount'] = Get-SqlPSCredential -SQLServer $ServerName ` -SQLInstanceName $InstanceName ` @@ -585,7 +586,8 @@ function Set-TargetResource $credentialId = Get-MailServerCredentialId -SQLServer $ServerName ` -SQLInstanceName $InstanceName ` - -MailServerName $MailServerName + -MailServerName $MailServerName ` + -AccountId $databaseMailAccount.ID $currentSMTPAccount = Get-SqlPSCredential -SQLServer $ServerName ` -SQLInstanceName $InstanceName ` diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index 04bb8e14ae..b740b96429 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -2496,6 +2496,9 @@ function Find-ExceptionByNumber .PARAMETER MailServerName Specifies name of the SMTP mail server for which credential Id should be returned. + + .PARAMETER AccountId + Specifies ID of the mail account in which SMTP server were created. #> function Get-MailServerCredentialId { @@ -2514,7 +2517,12 @@ function Get-MailServerCredentialId [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] - $MailServerName + $MailServerName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Int32] + $AccountId ) $invokeQueryParameters = @{ @@ -2527,11 +2535,11 @@ function Get-MailServerCredentialId $queryToGetCredentialId = " SELECT credential_id FROM msdb.dbo.sysmail_server - WHERE servername = '$MailServerName' + WHERE servername = '$MailServerName' and account_id = '$AccountId' " Write-Verbose -Message ($script:localizedData.GetMailServerCredentialId -f $MailServerName, $SQLInstanceName) -Verbose - $result = (Invoke-Query @invokeQueryParameters -Query $queryToGetCredentialId).Tables.credential_id + $result = (Invoke-Query @invokeQueryParameters -Query $queryToGetCredentialId).Tables[0].credential_id return $result } diff --git a/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 index aed2533bc1..3539ba08f9 100644 --- a/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 @@ -54,6 +54,7 @@ try InModuleScope $script:dscResourceName { $mockServerName = 'localhost' $mockInstanceName = 'MSSQLSERVER' + $mockAccountId = 11 $mockAccountName = 'MyMail' $mockEmailAddress = 'NoReply@company.local' $mockReplyToAddress = $mockEmailAddress @@ -126,6 +127,7 @@ try # Contains mocked object that is used between several mocks. $mailAccountObject = { New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'ID' -Value $mockAccountId -PassThru | Add-Member -MemberType NoteProperty -Name 'Name' -Value $mockAccountName -PassThru | Add-Member -MemberType NoteProperty -Name 'DisplayName' -Value $mockDisplayName -PassThru | Add-Member -MemberType NoteProperty -Name 'EmailAddress' -Value $mockEmailAddress -PassThru | @@ -346,7 +348,7 @@ try $mockDynamicAuthenticationAccountValue = $mockSMTPAccountPresent.UserName Mock -CommandName Get-MailServerCredentialId ` - -ParameterFilter {$MailServerName -eq $mockMailServerName} + -ParameterFilter {$MailServerName -eq $mockMailServerName -and $AccountId -eq $mockAccountId} Mock -CommandName Get-SqlPSCredential ` -MockWith { return $mockSMTPAccountPresent } } @@ -360,7 +362,7 @@ try Should -Be $mockSMTPAccountPresent.GetNetworkCredential().Password Assert-MockCalled -CommandName Get-MailServerCredentialId ` - -ParameterFilter {$MailServerName -eq $mockMailServerName} ` + -ParameterFilter {$MailServerName -eq $mockMailServerName -and $AccountId -eq $mockAccountId} ` -Exactly ` -Times 1 ` -Scope It diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index b72deb7371..92a8c4fadf 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -3269,7 +3269,7 @@ InModuleScope 'SqlServerDsc.Common' { throw } - return [pscustomobject] @{ Tables = @{ credential_id = $mockCredentialId } } + return [pscustomobject] @{ Tables = @( @{ credential_id = $mockCredentialId } ) } } -PassThru ) } @@ -3282,13 +3282,14 @@ InModuleScope 'SqlServerDsc.Common' { SQLServer = 'Server1' SQLInstanceName = 'MSSQLSERVER' MailServerName = 'smtp.account.local' + AccountId = 11 } $mockCredentialId = 65555 } BeforeEach { - $mockExpectedQuery = "*SELECT credential_id* FROM msdb.dbo.sysmail_server* WHERE servername = '$($queryParams.MailServerName)'*" + $mockExpectedQuery = "*SELECT credential_id* FROM msdb.dbo.sysmail_server* WHERE servername = '$($queryParams.MailServerName)' and account_id = '$($queryParams.AccountId)'*" Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable } From e27278109dbff16757de1edda16373fefce34aa2 Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Mon, 1 Jul 2019 16:43:45 +0300 Subject: [PATCH 09/10] fixes after rebase --- .../SqlServerDsc.Common.psm1 | 6 ++-- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 29 +++++++------------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index b740b96429..e33dfcd9f3 100644 --- a/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -2550,9 +2550,9 @@ function Get-MailServerCredentialId which will be used for credential password decryption. .DESCRIPTION - Submission containing materials of a third party: Antti Rantasaari 2014, NetSPI - License: BSD 3-Clause https://opensource.org/licenses/BSD-3-Clause - Source: https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1 + Inspired by the work of Antti Rantasaari 2014, NetSPI + (https://github.com/NetSPI/Powershell-Modules/blob/master/Get-MSSQLCredentialPasswords.psm1) + that is under BSD-3 license. .PARAMETER SQLServer String containing the host name of the SQL Server to connect to. diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 92a8c4fadf..596fd38f36 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -1444,7 +1444,6 @@ InModuleScope 'SqlServerDsc.Common' { Describe 'SqlServerDsc.Common\Invoke-Query' -Tag 'InvokeQuery' { BeforeAll { $mockExpectedQuery = '' - $mockExpectedInstance = 'MSSQLSERVER' $mockSetupCredentialUserName = 'TestUserName12345' $mockSetupCredentialPassword = 'StrongOne7.' @@ -1596,8 +1595,8 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Pass in an SMO Server Object' { - Context 'Execute a query with no results' { + Context 'When SMO Server Object was passed in' { + Context 'When a query is executed and nothing is returned' { It 'Should execute the query silently' { $queryParametersWithSMO.Query = "EXEC sp_configure 'show advanced option', '1'" $mockExpectedQuery = $queryParametersWithSMO.Query.Clone() @@ -1618,7 +1617,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Execute a query with results' { + Context 'When a query is executed and results are returned' { It 'Should execute the query and return a result set' { $queryParametersWithSMO.Query = 'SELECT name FROM sys.databases' $mockExpectedQuery = $queryParametersWithSMO.Query.Clone() @@ -1639,7 +1638,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Execute a query with piped SMO server object' { + Context 'When a query is executed with piped SMO server object' { It 'Should execute the query and return a result set' { $mockQuery = 'SELECT name FROM sys.databases' $mockExpectedQuery = $mockQuery @@ -3340,11 +3339,6 @@ InModuleScope 'SqlServerDsc.Common' { } -PassThru ) } - ConnectionContext = New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value $result -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { - $script:ConnectionContextDisconnectMethodCallCount += 1 - } -PassThru -Force } ) ) @@ -3386,18 +3380,12 @@ InModuleScope 'SqlServerDsc.Common' { } } - Describe 'DscResource.Common\Get-SqlPSCredential' -Tag 'Get-SqlPSCredential' { + Describe 'DscResource.Common\Get-SqlPSCredential' -Tag 'GetSqlPSCredential' { BeforeAll { $mockConnectSql = { - if ($InstanceName -eq 'MSSQLSERVER') - { - $result = @('','ADMIN:')[$DAC.IsPresent] + $ServerName - } - else - { - $result = @('','ADMIN:')[$DAC.IsPresent] + "$ServerName\$InstanceName" - } + $instance = @('','ADMIN:')[$DAC.IsPresent] + $ServerName + ` + @('',"\$InstanceName")[$InstanceName -ne 'MSSQLSERVER'] return @( ( @@ -3462,6 +3450,9 @@ InModuleScope 'SqlServerDsc.Common' { } -PassThru ) } + ConnectionContext = New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value $instance -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value {} -PassThru -Force } ) ) From 0653710c925ecaaee8284570da018a47b66ca05a Mon Sep 17 00:00:00 2001 From: Artem Chubar Date: Mon, 1 Jul 2019 18:03:19 +0300 Subject: [PATCH 10/10] small corrections --- CHANGELOG.md | 38 +++++++++---------- ...qlServerDatabaseMail.Integration.Tests.ps1 | 2 +- .../MSFT_SqlServerDatabaseMail.config.ps1 | 4 +- Tests/Unit/SqlServerDsc.Common.Tests.ps1 | 4 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9f6a75b7..9beda05f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,25 @@ - Can also pipe in 'Microsoft.SqlServer.Management.Smo.Server' object - Can pipe Connect-SQL | Invoke-Query - Added default vaules to Invoke-Query +- Changes to SqlServerDatabaseMail + - Added new parameter `EnableSsl` which controls encryption of communication + using Secure Sockets Layer. + - Added new parameter `Authentication` and `SMTPAccount` for configuration of + SMTP authentication mode and credential used [issue #1215](https://github.com/PowerShell/SqlServerDsc/issues/1215). + - Added new helper function `Get-MailServerCredentialId` which gets + credential Id used by mail server. + - Added new helper function `Get-ServiceMasterKey` which gets unencrypted + Service Master Key for specified SQL Instance. + - Added new helper function `Get-SqlPSCredential` which decrypts and returns + PSCredential object of SQL Credential by Id. + - Added DAC switch to function 'Connect-SQL' with default value $false. + Used to specify that non-pooled Dedicated Admin Connection should be + established. + - Added UsingDAC switch to function 'Invoke-Query' with default value $false. + Used to specify that query should be executed using Dedicated Admin + Connection. After execution DAC connection will be closed. + - Added PSCredential option to function 'Test-DscParameterState' which will + compare PSCredential objects using username/password keys. ## 13.0.0.0 @@ -86,25 +105,6 @@ - Fix issue where calling Get would return an error because the database name list may have been returned as a string instead of as a string array ([issue #1368](https://github.com/PowerShell/SqlServerDsc/issues/1368)). -- Changes to SqlServerDatabaseMail - - Added new parameter `EnableSsl` which controls encryption of communication - using Secure Sockets Layer. - - Added new parameter `Authentication` and `SMTPAccount` for configuration of - SMTP authentication mode and credential used [issue #1215](https://github.com/PowerShell/SqlServerDsc/issues/1215). - - Added new helper function `Get-MailServerCredentialId` which gets - credential Id used by mail server. - - Added new helper function `Get-ServiceMasterKey` which gets unencrypted - Service Master Key for specified SQL Instance. - - Added new helper function `Get-SqlPSCredential` which decrypts and returns - PSCredential object of SQL Credential by Id. - - Added DAC switch to function 'Connect-SQL' with default value $false. - Used to specify that non-pooled Dedicated Admin Connection should be - established. - - Added UsingDAC switch to function 'Invoke-Query' with default value $false. - Used to specify that query should be executed using Dedicated Admin - Connection. After execution DAC connection will be closed. - - Added PSCredential option to function 'Test-DscParameterState' which will - compare PSCredential objects using username/password keys. ## 12.5.0.0 diff --git a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 index 956aab6a6d..8ca1f0a435 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 @@ -92,7 +92,7 @@ try $resourceCurrentState.Authentication | Should -Be $ConfigurationData.AllNodes.Authentication <# Possibly LCM protects credentials objects and instead of real values NULL is returned. - In the same time unit tests for Get() function shows correct results. + At the same time unit tests for Get() function shows correct results. #> $resourceCurrentState.SMTPAccount.UserName | Should -BeNullOrEmpty $resourceCurrentState.SMTPAccount.Password | Should -BeNullOrEmpty diff --git a/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 index 170c9e8e8c..9a6d927d83 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.config.ps1 @@ -54,7 +54,7 @@ Configuration MSFT_SqlServerDatabaseMail_Add_Config { Import-DscResource -ModuleName 'SqlServerDsc' - node $AllNodes.NodeName + Node $AllNodes.NodeName { SqlServerConfiguration 'EnableDatabaseMailXPs' { @@ -104,7 +104,7 @@ Configuration MSFT_SqlServerDatabaseMail_Remove_Config { Import-DscResource -ModuleName 'SqlServerDsc' - node $AllNodes.NodeName + Node $AllNodes.NodeName { SqlServerDatabaseMail 'Integration_Test' { diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 596fd38f36..a70a32ac39 100644 --- a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -2591,12 +2591,12 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'When the logon type is WindowsUser or SqlLogin, but not credentials were passed' { + Context 'When the logon type is WindowsUser or SqlLogin, but no credentials were passed' { It 'Should throw the correct error' { $mockExpectedDatabaseEngineServer = 'TestServer' $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' - $connectSqlParameters = @{ + $connectSqlParameters = @{ ServerName = $mockExpectedDatabaseEngineServer LoginType = 'WindowsUser' }