diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8f9a4378..af46e5e1a0 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 diff --git a/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 b/DSCResources/MSFT_SqlServerDatabaseMail/MSFT_SqlServerDatabaseMail.psm1 index d276f06d7a..75ebf396fd 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,37 @@ 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 ` + -AccountId $databaseMailAccount.ID + + $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 +258,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 +326,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 +435,28 @@ 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 +558,118 @@ 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 ` + -AccountId $databaseMailAccount.ID + + $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 +820,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 +885,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 +920,7 @@ function Test-TargetResource if ($Ensure -eq 'Present') { - $returnValue = Test-DscParameterState ` - -CurrentValues $getTargetResourceResult ` - -DesiredValues $PSBoundParameters ` - -ValuesToCheck @( + $valuesToCheck = @( 'AccountName' 'EmailAddress' 'MailServerName' @@ -715,7 +931,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..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 ( @@ -35,9 +35,9 @@ Configuration Example Import-DscResource -ModuleName 'SqlServerDsc' node localhost { + SqlServerConfiguration 'EnableDatabaseMailXPs' { - ServerName = $Node.ServerName InstanceName = $Node.InstanceName OptionName = 'Database Mail XPs' diff --git a/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 b/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 new file mode 100644 index 0000000000..fd6e55b1f8 --- /dev/null +++ b/Examples/Resources/SqlServerDatabaseMail/2-EnableDatabaseMailWithSsl.ps1 @@ -0,0 +1,68 @@ +<# + .EXAMPLE + 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 = @( + @{ + 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 EnableDatabaseMailWithSsl +{ + 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..8519ccd85e --- /dev/null +++ b/Examples/Resources/SqlServerDatabaseMail/3-EnableDatabaseMailWithAuthentication.ps1 @@ -0,0 +1,75 @@ +<# + .EXAMPLE + 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 = @( + @{ + 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 EnableDatabaseMailWithAuthentication +{ + 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 90% rename from Examples/Resources/SqlServerDatabaseMail/2-DisableDatabaseMail.ps1 rename to Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 index ab58510a11..da76787df5 100644 --- a/Examples/Resources/SqlServerDatabaseMail/2-DisableDatabaseMail.ps1 +++ b/Examples/Resources/SqlServerDatabaseMail/4-DisableDatabaseMail.ps1 @@ -15,14 +15,11 @@ $ConfigurationData = @{ AccountName = 'MyMail' ProfileName = 'MyMailProfile' EmailAddress = 'NoReply@company.local' - Description = 'Default mail account and profile.' - LoggingLevel = 'Normal' - TcpPort = 25 } ) } -Configuration Example +Configuration DisableDatabaseMail { param ( @@ -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 95aaf2c927..dd8edd54b4 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' @@ -1605,6 +1643,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). @@ -1625,14 +1666,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', @@ -1644,8 +1685,8 @@ function Invoke-Query [System.String] $Query, - [Alias("SetupCredential")] [Parameter()] + [Alias("SetupCredential")] [System.Management.Automation.PSCredential] $DatabaseCredential, @@ -1660,9 +1701,14 @@ function Invoke-Query $SqlServerObject, [Parameter()] - [Switch] + [System.Management.Automation.SwitchParameter] $WithResults, + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsingDAC, + + [Parameter()] [ValidateNotNull()] [System.Int32] $StatementTimeout = 600 @@ -1678,6 +1724,7 @@ function Invoke-Query ServerName = $SQLServer InstanceName = $SQLInstanceName LoginType = $LoginType + DAC = $UsingDAC.IsPresent StatementTimeout = $StatementTimeout } @@ -1689,28 +1736,36 @@ 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 + if ($WithResults.IsPresent) { - $serverObject.Databases[$Database].ExecuteNonQuery($Query) + $errorMessage = $script:localizedData.ExecuteQueryWithResultsFailed -f $Database } - catch + else { $errorMessage = $script:localizedData.ExecuteNonQueryFailed -f $Database - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + if ($UsingDAC.IsPresent) + { + $serverObject.ConnectionContext.Disconnect() + Write-Verbose -Message ($script:localizedData.DisconnectFromSQLInstance -f $serverObject.ConnectionContext.ServerInstance) -Verbose } } @@ -2431,6 +2486,240 @@ 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. + + .PARAMETER AccountId + Specifies ID of the mail account in which SMTP server were created. +#> +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, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Int32] + $AccountId + ) + + $invokeQueryParameters = @{ + SQLServer = $SQLServer + SQLInstanceName = $SQLInstanceName + Database = 'master' + WithResults = $true + } + + $queryToGetCredentialId = " + SELECT credential_id + FROM msdb.dbo.sysmail_server + WHERE servername = '$MailServerName' and account_id = '$AccountId' + " + + Write-Verbose -Message ($script:localizedData.GetMailServerCredentialId -f $MailServerName, $SQLInstanceName) -Verbose + $result = (Invoke-Query @invokeQueryParameters -Query $queryToGetCredentialId).Tables[0].credential_id + + return $result +} + +<# + .SYNOPSIS + Gets unencrypted Service Master Key for specified SQL Instance + which will be used for credential password decryption. + + .DESCRIPTION + 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. + + .PARAMETER SQLInstanceName + String containing the SQL Server Database Engine instance to connect to. + + .NOTES + 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 +{ + [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 crypt_property AS 'key' + FROM sys.key_encryptions + WHERE key_id=102 and thumbprint<>0x01 + " + + Write-Verbose -Message ($script:localizedData.GetServiceMasterKey -f $SQLInstanceName) -Verbose + $encryptedServiceMasterKey = (Invoke-Query @invokeQueryParameters -Query $queryToGetServiceMasterKey).Tables[0].key | Select-Object -Skip 8 + + Write-Verbose -Message ($script:localizedData.GetEntropyForSqlInstance -f $SQLInstanceName) -Verbose + $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' + + $serviceMasterKey = [System.Security.Cryptography.ProtectedData]::Unprotect($encryptedServiceMasterKey, $entropy, [System.Security.Cryptography.DataProtectionScope]::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 + 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 +{ + [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 + + switch ($smk.Length) + { + 16 + { + $typeName = 'System.Security.Cryptography.TripleDESCryptoServiceProvider' + } + + 32 + { + $typeName = 'System.Security.Cryptography.AESCryptoServiceProvider' + } + + default + { + $errorMessage = $script:localizedData.SmkSizeNotImplemented -f $smk.Length, $SQLInstanceName + New-InvalidResultException -Message $errorMessage + } + } + + # 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.GetEncryptedMessage -f $CredentialId, $SQLInstanceName) -Verbose + $credInfo = (Invoke-Query @invokeQueryParameters -Query $queryToGetEncryptedMessage).Tables[0] + + $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 + if ([System.BitConverter]::ToString($message[3..0]) -ne 'BA-AD-F0-0D') + { + Write-Warning -Message $script:localizedData.FailedCredentialDecryption + } + # Getting password length using bytes 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 @( @@ -2468,4 +2757,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..bd6442f1d4 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) + 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/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 b/Modules/SqlServerDsc.Common/sv-SE/SqlServerDsc.Common.strings.psd1 index 32633eb8db..dcbd17df40 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) + 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 3fedb632b8..d536255354 100644 --- a/README.md +++ b/README.md @@ -1195,11 +1195,24 @@ 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 * [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/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 index 781aa30a54..8ca1f0a435 100644 --- a/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_SqlServerDatabaseMail.Integration.Tests.ps1 @@ -78,16 +78,24 @@ 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 + <# + Possibly LCM protects credentials objects and instead of real values NULL is returned. + At the same time unit tests for Get() function shows correct results. + #> + $resourceCurrentState.SMTPAccount.UserName | Should -BeNullOrEmpty + $resourceCurrentState.SMTPAccount.Password | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { @@ -133,16 +141,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..9a6d927d83 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 } @@ -49,7 +54,7 @@ Configuration MSFT_SqlServerDatabaseMail_Add_Config { Import-DscResource -ModuleName 'SqlServerDsc' - node $AllNodes.NodeName + Node $AllNodes.NodeName { SqlServerConfiguration 'EnableDatabaseMailXPs' { @@ -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 ` @@ -93,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/MSFT_SqlServerDatabaseMail.Tests.ps1 b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 index 5d1c9eb269..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 @@ -62,6 +63,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 @@ -91,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 | @@ -101,12 +138,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 +245,8 @@ try $mockDynamicDescription = $mockDescription $mockDynamicAgentMailType = $mockAgentMailTypeDatabaseMail $mockDynamicDatabaseMailProfile = $mockProfileName + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockAuthenticationBasicDisabled } BeforeEach { @@ -239,6 +287,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 +322,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 -and $AccountId -eq $mockAccountId} + 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 -and $AccountId -eq $mockAccountId} ` + -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 +450,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 +486,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 +525,9 @@ try Description = $mockDescription LoggingLevel = $mockLoggingLevelExtended TcpPort = $mockTcpPort + EnableSsl = $mockEnableSsl + Authentication = $mockAuthenticationBasic + SMTPAccount = $mockSMTPAccountPresent } $testCaseAccountNameIsMissing = $defaultTestCase.Clone() @@ -447,6 +566,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 +592,11 @@ try $testCaseReplyToAddressIsWrong $testCaseDescriptionIsWrong $testCaseLoggingLevelIsWrong - $testCaseTcpPortIsWrong + $testCaseTcpPortIsWrong, + $testCaseEnableSslIsWrong, + $testCaseAuthenticationIsWrong, + $testCaseSMTPAccountIsWrong, + $testCaseSMTPAccountPasswordIsWrong ) It 'Should return the state as $false when ' -TestCases $testCases { @@ -470,7 +610,10 @@ try $ReplyToAddress, $Description, $LoggingLevel, - $TcpPort + $TcpPort, + $EnableSsl, + $Authentication, + $SMTPAccount ) $testTargetResourceParameters['AccountName'] = $AccountName @@ -482,11 +625,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 +647,8 @@ try $mockDynamicDescription = $mockDescription $mockDynamicAgentMailType = $mockAgentMailTypeDatabaseMail $mockDynamicDatabaseMailProfile = $mockProfileName + $mockDynamicAuthenticationValue = $mockAuthenticationWindowsDisabled + $mockDynamicAuthenticationAccountValue = $mockSMTPAccountPresent.UserName } BeforeEach { @@ -513,10 +661,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 +701,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 +725,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 +787,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 +813,9 @@ try Description = $mockDescription LoggingLevel = $mockLoggingLevelExtended TcpPort = $mockTcpPort + EnableSsl = $mockEnableSsl + Authentication = $mockAuthenticationBasic + SMTPAccount = $mockSMTPAccountPresent } $testCaseEmailAddressIsWrong = $defaultTestCase.Clone() @@ -685,6 +854,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 +881,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 +900,10 @@ try $ReplyToAddress, $Description, $LoggingLevel, - $TcpPort + $TcpPort, + $EnableSsl, + $Authentication, + $SMTPAccount ) $setTargetResourceParameters['AccountName'] = $AccountName @@ -721,6 +915,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 +926,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 +937,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 +951,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 +996,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 +1010,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 +1022,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 +1038,3 @@ finally { Invoke-TestCleanup } - diff --git a/Tests/Unit/SqlServerDsc.Common.Tests.ps1 b/Tests/Unit/SqlServerDsc.Common.Tests.ps1 index 3ca5efd7a1..a70a32ac39 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 } @@ -944,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 @{ @@ -1150,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 @{ @@ -1280,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 @@ -1291,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 ` @@ -1395,6 +1450,8 @@ InModuleScope 'SqlServerDsc.Common' { $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,28 +1484,35 @@ 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) } - - $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 = @{ @@ -1460,12 +1524,12 @@ InModuleScope 'SqlServerDsc.Common' { } $queryParametersWithSMO = @{ - Query = '' SqlServerObject = $mockSMOServer - Database = 'master' + Database = 'master' + 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() @@ -1486,7 +1550,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() @@ -1507,8 +1571,32 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Pass in an SMO Server Object' { - Context 'Execute a query with no results' { + 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() + + 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 '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() @@ -1529,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() @@ -1550,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 @@ -1801,17 +1889,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' { @@ -2290,7 +2373,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 +2382,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 +2412,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) @@ -2356,10 +2440,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 @@ -2367,7 +2447,6 @@ InModuleScope 'SqlServerDsc.Common' { } BeforeEach { - Mock -CommandName New-InvalidOperationException -MockWith $mockThrowLocalizedMessage -Verifiable Mock -CommandName Import-SQLPSModule Mock -CommandName New-Object ` -MockWith $mockNewObject_MicrosoftDatabaseEngine ` @@ -2379,6 +2458,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 +2473,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 +2486,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 +2520,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 +2537,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 +2551,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should return the correct service instance' { $mockExpectedDatabaseEngineServer = $env:COMPUTERNAME $mockExpectedDatabaseEngineInstance = $mockInstanceName + $mockDAC = $false $testParameters = @{ ServerName = $mockExpectedDatabaseEngineServer @@ -2476,6 +2576,7 @@ InModuleScope 'SqlServerDsc.Common' { It 'Should throw the correct error' { $mockExpectedDatabaseEngineServer = $env:COMPUTERNAME $mockExpectedDatabaseEngineInstance = $mockInstanceName + $mockDAC = $false Mock -CommandName New-Object ` -MockWith $mockNewObject_MicrosoftDatabaseEngine ` @@ -2490,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' } @@ -2810,7 +2911,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 { @@ -2822,7 +2923,7 @@ InModuleScope 'SqlServerDsc.Common' { } } - Context 'Invoke-SqlScript is called with credentials' { + Context 'When Invoke-SqlScript is called with credentials' { BeforeAll { $mockPasswordPlain = 'password' $mockUsername = 'User' @@ -2855,7 +2956,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 {} @@ -3144,5 +3245,272 @@ 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 + ) + } + } + ) + ) + } + + $queryParams = @{ + 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)' and account_id = '$($queryParams.AccountId)'*" + + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -ModuleName $script:dscResourceName -Verifiable + } + + Context 'When credential id is returned' { + 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<>0x01*' + + $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 = @( @{ key = [BitConverter]::GetBytes([int64]0) + $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-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-ItemPropertyValue ` + -MockWith { return $mockEntropy } ` + -ParameterFilter { $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$mockServiceInstanceId\Security" } ` + -Verifiable + } + + 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-ItemPropertyValue -Scope It -Times 2 -Exactly + } + } + } + + Describe 'DscResource.Common\Get-SqlPSCredential' -Tag 'GetSqlPSCredential' { + BeforeAll { + $mockConnectSql = { + + $instance = @('','ADMIN:')[$DAC.IsPresent] + $ServerName + ` + @('',"\$InstanceName")[$InstanceName -ne 'MSSQLSERVER'] + + 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 + } + + <# + 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 + { + $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 + enc_message = $mockIV + $mockEnc_message + } + ) + } + } -PassThru + ) + } + ConnectionContext = New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value $instance -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value {} -PassThru -Force + } + ) + ) + } + + $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 + + $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 '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 + + $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' { + + # 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 + + $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.SmkSizeNotImplemented -f $mockSMK.Length, $queryParams.SQLInstanceName) + + Assert-MockCalled -CommandName Get-ServiceMasterKey -Scope It -Exactly 1 + Assert-MockCalled -CommandName Connect-SQL -Scope It -Exactly 0 + } + } + } +}