diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6fc469f..b6c2e1a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added public commands `Start-SqlDscRSWindowsService`, `Stop-SqlDscRSWindowsService`, `Start-SqlDscRSWebService`, and `Stop-SqlDscRSWebService` to manage Reporting Services Windows and web services using the `SetServiceState` WMI method. +- Added public commands `Start-SqlDscRSWindowsService`, `Stop-SqlDscRSWindowsService`, + `Start-SqlDscRSWebService`, and `Stop-SqlDscRSWebService` to manage Reporting + Services Windows and web services using the `SetServiceState` WMI method. - SqlServerDsc - Added class `ReportServerUri` to represent URLs returned by the `GetReportServerUrls` CIM method on `MSReportServer_Instance`. @@ -222,6 +224,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 wrap the `ListSSLCertificateBindings`, `CreateSSLCertificateBinding`, and `RemoveSSLCertificateBinding` CIM methods. The `Set-SqlDscRSSslCertificateBinding` command provides a declarative approach to set SSL bindings to an exact list. +- Added public commands `Backup-SqlDscRSEncryptionKey` and + `Restore-SqlDscRSEncryptionKey` to backup and restore Reporting Services + encryption keys. These commands wrap the `BackupEncryptionKey` and + `RestoreEncryptionKey` CIM methods and require a password to secure the key. - Added public command `New-SqlDscRSEncryptionKey` to delete and regenerate the Reporting Services encryption key. Wraps the `DeleteEncryptionKey` CIM method. Warning: This operation cannot be undone and renders all encrypted content diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 069bee5df..b16f3bbd3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -798,6 +798,13 @@ stages: 'tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSSmtpConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' + # Group 8 - Service account change with encryption key backup/restore + 'tests/Integration/Commands/Pre.ServiceAccountChange.Secure.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Backup-SqlDscRSEncryptionKey.Integration.Tests.ps1' + 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Mid.ServiceAccountChange.Secure.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Restore-SqlDscRSEncryptionKey.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.ServiceAccountChange.Secure.RS.Integration.Tests.ps1' ) name: test displayName: 'Run Integration Test' diff --git a/source/Public/Backup-SqlDscRSEncryptionKey.ps1 b/source/Public/Backup-SqlDscRSEncryptionKey.ps1 new file mode 100644 index 000000000..2385bc1dc --- /dev/null +++ b/source/Public/Backup-SqlDscRSEncryptionKey.ps1 @@ -0,0 +1,217 @@ +<# + .SYNOPSIS + Backs up the encryption key for SQL Server Reporting Services. + + .DESCRIPTION + Backs up the encryption key for SQL Server Reporting Services or + Power BI Report Server by calling the `BackupEncryptionKey` method + on the `MSReportServer_ConfigurationSetting` CIM instance. + + The encryption key is essential for decrypting stored credentials + and connection strings in the report server database. This backup + should be stored securely and is required for disaster recovery + or migration scenarios. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER Path + Specifies the full path where the encryption key backup file will + be saved. The file extension should be .snk (Strong Name Key). + + .PARAMETER Password + Specifies the password to protect the encryption key backup file. + This password will be required when restoring the encryption key. + + .PARAMETER Credential + Specifies the credentials to use when accessing a UNC path. Use this + parameter when the Path is a network share that requires authentication. + + .PARAMETER DriveName + Specifies the name of the temporary PSDrive to create when accessing + a UNC path with credentials. Defaults to 'RSKeyBackup'. This parameter + can only be used together with the Credential parameter. + + .PARAMETER PassThru + If specified, returns the configuration CIM instance after backing + up the encryption key. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + $password = Read-Host -AsSecureString -Prompt 'Enter backup password' + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Backup-SqlDscRSEncryptionKey -Path 'C:\Backup\RSKey.snk' -Password $password + + Backs up the encryption key to a local file. + + .EXAMPLE + $password = ConvertTo-SecureString -String 'MyP@ssw0rd' -AsPlainText -Force + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Backup-SqlDscRSEncryptionKey -Configuration $config -Path '\\Server\Share\RSKey.snk' -Password $password -Credential (Get-Credential) -Force + + Backs up the encryption key to a UNC path with credentials and + without confirmation. + + .EXAMPLE + $password = Read-Host -AsSecureString -Prompt 'Enter backup password' + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Backup-SqlDscRSEncryptionKey -Path 'C:\Backup\RSKey.snk' -Password $password -PassThru + + Backs up the encryption key and returns the configuration CIM instance. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + None. By default, this command does not generate any output. + + .OUTPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + When PassThru is specified, returns the MSReportServer_ConfigurationSetting + CIM instance. + + .NOTES + Store the backup file and password securely. They are required to + restore the encryption key in disaster recovery scenarios or when + migrating to a new server. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-backupencryptionkey +#> +function Backup-SqlDscRSEncryptionKey +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.Security.SecureString] + $Password, + + [Parameter(ParameterSetName = 'ByCredential')] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(ParameterSetName = 'ByCredential')] + [ValidateNotNullOrEmpty()] + [System.String] + $DriveName = 'RSKeyBackup', + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $instanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Backup_SqlDscRSEncryptionKey_BackingUp -f $instanceName, $Path) + + $descriptionMessage = $script:localizedData.Backup_SqlDscRSEncryptionKey_ShouldProcessDescription -f $instanceName, $Path + $confirmationMessage = $script:localizedData.Backup_SqlDscRSEncryptionKey_ShouldProcessConfirmation -f $instanceName + $captionMessage = $script:localizedData.Backup_SqlDscRSEncryptionKey_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + # Convert SecureString to plain text for the WMI method + $passwordBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) + + try + { + $passwordPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordBstr) + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'BackupEncryptionKey' + Arguments = @{ + Password = $passwordPlainText + } + } + + $result = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + + # The WMI method returns the key as a byte array in the KeyFile property + $keyData = $result.KeyFile + + # Handle UNC path with credentials + $psDriveCreated = $false + $targetPath = $Path + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $parentPath = Split-Path -Path $Path -Parent + + if ($parentPath -match '^\\\\') + { + New-PSDrive -Name $DriveName -PSProvider 'FileSystem' -Root $parentPath -Credential $Credential -ErrorAction 'Stop' | Out-Null + + $psDriveCreated = $true + $fileName = Split-Path -Path $Path -Leaf + $targetPath = (Resolve-Path -LiteralPath "${DriveName}:\$fileName").ProviderPath + } + } + + try + { + # Write the key data to file + [System.IO.File]::WriteAllBytes($targetPath, $keyData) + } + finally + { + if ($psDriveCreated) + { + Remove-PSDrive -Name $DriveName -Force -ErrorAction 'SilentlyContinue' + } + } + } + catch + { + $errorMessage = $script:localizedData.Backup_SqlDscRSEncryptionKey_FailedToBackup -f $instanceName + + $exception = New-Exception -Message $errorMessage -ErrorRecord $_ + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'BSRSEK0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + finally + { + # Clear the plain text password from memory + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordBstr) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Restore-SqlDscRSEncryptionKey.ps1 b/source/Public/Restore-SqlDscRSEncryptionKey.ps1 new file mode 100644 index 000000000..1db09c532 --- /dev/null +++ b/source/Public/Restore-SqlDscRSEncryptionKey.ps1 @@ -0,0 +1,214 @@ +<# + .SYNOPSIS + Restores the encryption key for SQL Server Reporting Services. + + .DESCRIPTION + Restores the encryption key for SQL Server Reporting Services or + Power BI Report Server by calling the `RestoreEncryptionKey` method + on the `MSReportServer_ConfigurationSetting` CIM instance. + + This command restores a previously backed up encryption key to + the report server. This is required after migrating to a new server + or in disaster recovery scenarios to decrypt stored credentials + and connection strings in the report server database. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER Path + Specifies the full path to the encryption key backup file (.snk). + + .PARAMETER Password + Specifies the password that was used when backing up the encryption + key. + + .PARAMETER Credential + Specifies the credentials to use when accessing a UNC path. Use this + parameter when the Path is a network share that requires authentication. + + .PARAMETER DriveName + Specifies the name of the temporary PSDrive to create when accessing + a UNC path with credentials. Defaults to 'RSKeyRestore'. This parameter + can only be used together with the Credential parameter. + + .PARAMETER PassThru + If specified, returns the configuration CIM instance after restoring + the encryption key. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + $password = Read-Host -AsSecureString -Prompt 'Enter backup password' + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Restore-SqlDscRSEncryptionKey -Path 'C:\Backup\RSKey.snk' -Password $password + + Restores the encryption key from a local file. + + .EXAMPLE + $password = ConvertTo-SecureString -String 'MyP@ssw0rd' -AsPlainText -Force + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Restore-SqlDscRSEncryptionKey -Configuration $config -Path '\\Server\Share\RSKey.snk' -Password $password -Credential (Get-Credential) -Force + + Restores the encryption key from a UNC path with credentials and + without confirmation. + + .EXAMPLE + $password = Read-Host -AsSecureString -Prompt 'Enter backup password' + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Restore-SqlDscRSEncryptionKey -Path 'C:\Backup\RSKey.snk' -Password $password -PassThru + + Restores the encryption key and returns the configuration CIM instance. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + None. By default, this command does not generate any output. + + .OUTPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + When PassThru is specified, returns the MSReportServer_ConfigurationSetting + CIM instance. + + .NOTES + The Reporting Services service may need to be restarted after restoring + the encryption key. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-restoreencryptionkey +#> +function Restore-SqlDscRSEncryptionKey +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.Security.SecureString] + $Password, + + [Parameter(ParameterSetName = 'ByCredential')] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(ParameterSetName = 'ByCredential')] + [ValidateNotNullOrEmpty()] + [System.String] + $DriveName = 'RSKeyRestore', + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $instanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Restore_SqlDscRSEncryptionKey_Restoring -f $instanceName, $Path) + + $descriptionMessage = $script:localizedData.Restore_SqlDscRSEncryptionKey_ShouldProcessDescription -f $instanceName, $Path + $confirmationMessage = $script:localizedData.Restore_SqlDscRSEncryptionKey_ShouldProcessConfirmation -f $instanceName + $captionMessage = $script:localizedData.Restore_SqlDscRSEncryptionKey_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + # Convert SecureString to plain text for the WMI method + $passwordBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) + + try + { + $passwordPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordBstr) + + # Handle UNC path with credentials + $psDriveCreated = $false + $sourcePath = $Path + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $parentPath = Split-Path -Path $Path -Parent + + if ($parentPath -match '^\\\\') + { + New-PSDrive -Name $DriveName -PSProvider 'FileSystem' -Root $parentPath -Credential $Credential -ErrorAction 'Stop' | Out-Null + + $psDriveCreated = $true + $fileName = Split-Path -Path $Path -Leaf + $sourcePath = (Resolve-Path -LiteralPath "${DriveName}:\$fileName").ProviderPath + } + } + + try + { + # Read the key data from file + $keyData = [System.IO.File]::ReadAllBytes($sourcePath) + } + finally + { + if ($psDriveCreated) + { + Remove-PSDrive -Name $DriveName -Force -ErrorAction 'SilentlyContinue' + } + } + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'RestoreEncryptionKey' + Arguments = @{ + KeyFile = $keyData + Length = $keyData.Length + Password = $passwordPlainText + } + } + + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Restore_SqlDscRSEncryptionKey_FailedToRestore -f $instanceName + + $exception = New-Exception -Message $errorMessage -ErrorRecord $_ + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'RSRSEK0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + finally + { + # Clear the plain text password from memory + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordBstr) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 99c872c74..d1bdf9867 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -896,6 +896,22 @@ ConvertFrom-StringData @' Set_SqlDscRSSslCertificateBinding_Adding = Adding SSL certificate binding '{0}' for application '{1}' on Reporting Services instance '{2}'. Set_SqlDscRSSslCertificateBinding_AlreadyExists = SSL certificate binding '{0}' for application '{1}' already exists on Reporting Services instance '{2}'. + ## Backup-SqlDscRSEncryptionKey + Backup_SqlDscRSEncryptionKey_BackingUp = Backing up encryption key for Reporting Services instance '{0}' to '{1}'. + Backup_SqlDscRSEncryptionKey_ShouldProcessDescription = Backing up encryption key for Reporting Services instance '{0}' to '{1}'. + Backup_SqlDscRSEncryptionKey_ShouldProcessConfirmation = Are you sure you want to back up the encryption key for Reporting Services instance '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Backup_SqlDscRSEncryptionKey_ShouldProcessCaption = Back up encryption key for Reporting Services instance + Backup_SqlDscRSEncryptionKey_FailedToBackup = Failed to backup encryption key for Reporting Services instance '{0}'. (BSRSEK0001) + + ## Restore-SqlDscRSEncryptionKey + Restore_SqlDscRSEncryptionKey_Restoring = Restoring encryption key for Reporting Services instance '{0}' from '{1}'. + Restore_SqlDscRSEncryptionKey_ShouldProcessDescription = Restoring encryption key for Reporting Services instance '{0}' from '{1}'. + Restore_SqlDscRSEncryptionKey_ShouldProcessConfirmation = Are you sure you want to restore the encryption key for Reporting Services instance '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Restore_SqlDscRSEncryptionKey_ShouldProcessCaption = Restore encryption key for Reporting Services instance + Restore_SqlDscRSEncryptionKey_FailedToRestore = Failed to restore encryption key for Reporting Services instance '{0}'. (RSRSEK0001) + ## New-SqlDscRSEncryptionKey New_SqlDscRSEncryptionKey_Generating = Generating new encryption key for Reporting Services instance '{0}'. New_SqlDscRSEncryptionKey_ShouldProcessDescription = Generating new encryption key for Reporting Services instance '{0}'. This will invalidate existing encryption key backups. diff --git a/tests/Integration/Commands/Backup-SqlDscRSEncryptionKey.Integration.Tests.ps1 b/tests/Integration/Commands/Backup-SqlDscRSEncryptionKey.Integration.Tests.ps1 new file mode 100644 index 000000000..380646268 --- /dev/null +++ b/tests/Integration/Commands/Backup-SqlDscRSEncryptionKey.Integration.Tests.ps1 @@ -0,0 +1,82 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +Describe 'Backup-SqlDscRSEncryptionKey' { + + <# + .NOTES + This context is used in the SSL/TLS (Secure) stage service account + change workflow. It backs up the encryption key to a persistent + path that will be used by subsequent tests to restore the key + after the service account has been changed. + + This runs as part of the Integration_Test_Commands_BIReportServer_Secure + pipeline stage in the following order: + 1. Pre.ServiceAccountChange.Secure.RS (creates backup directory) + 2. Backup-SqlDscRSEncryptionKey (this context - backs up to persistent path) + 3. Set-SqlDscRSServiceAccount (changes service account) + 4. Get-SqlDscRSServiceAccount (verifies change) + 5. Mid.ServiceAccountChange.Secure.RS (grants database rights) + 6. Restore-SqlDscRSEncryptionKey (restores from persistent path) + 7. Post.ServiceAccountChange.Secure.RS (URL reservations, re-init, validation) + #> + Context 'When backing up encryption key for Power BI Report Server service account change workflow' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + + # Persistent path for backup file - shared across service account change workflow tests + $script:backupPath = 'C:\IntegrationTest\RSEncryptionKey.snk' + $script:securePassword = ConvertTo-SecureString -String 'P@ssw0rd123!' -AsPlainText -Force + + Write-Verbose -Message "Backing up encryption key for service account change workflow to: $script:backupPath" -Verbose + } + + It 'Should backup the encryption key to a persistent location for the service account change workflow' { + # Remove any existing backup file from previous test runs + if (Test-Path -Path $script:backupPath) + { + Remove-Item -Path $script:backupPath -Force -ErrorAction 'SilentlyContinue' + } + + $script:configuration | Backup-SqlDscRSEncryptionKey -Password $script:securePassword -Path $script:backupPath -Force -ErrorAction 'Stop' + + Test-Path -Path $script:backupPath | Should -BeTrue -Because 'the encryption key backup file should be created at the persistent location' + } + + It 'Should verify the backup file is not empty' { + $backupFile = Get-Item -Path $script:backupPath -ErrorAction 'Stop' + + $backupFile.Length | Should -BeGreaterThan 0 -Because 'the backup file should contain data' + } + } +} diff --git a/tests/Integration/Commands/Mid.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Mid.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..54ba671ba --- /dev/null +++ b/tests/Integration/Commands/Mid.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 @@ -0,0 +1,112 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +<# + .NOTES + This test file runs in the middle of the service account change workflow + for the SSL/TLS (Secure) stage. It grants database rights to the new + service account and restarts the service, which must happen BEFORE + the encryption key can be restored. + + This runs as part of the Integration_Test_Commands_BIReportServer_Secure + pipeline stage in the following order: + 1. Pre.ServiceAccountChange.Secure.RS (creates backup directory) + 2. Backup-SqlDscRSEncryptionKey (backs up to persistent path) + 3. Set-SqlDscRSServiceAccount (changes service account) + 4. Get-SqlDscRSServiceAccount (verifies change) + 5. Mid.ServiceAccountChange.Secure.RS (this file - grants database rights) + 6. Restore-SqlDscRSEncryptionKey (restores from persistent path) + 7. Post.ServiceAccountChange.Secure.RS (URL reservations, re-init, validation) +#> +Describe 'Mid.ServiceAccountChange.Secure.RS' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:instanceName = 'PBIRS' + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + # Get database name from configuration + $script:databaseName = $script:configuration.DatabaseName + + $script:computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $script:computerName + + Write-Verbose -Message "Instance: $script:instanceName, Database: $script:databaseName, ServiceAccount: $script:serviceAccount" -Verbose + } + + Context 'When granting database rights to the new service account' { + BeforeAll { + # Connect to the database engine for the RS database instance. + $script:serverObject = Connect-SqlDscDatabaseEngine -ServerName 'localhost' -InstanceName 'RSDB' -ErrorAction 'Stop' + } + + AfterAll { + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject -ErrorAction 'SilentlyContinue' + } + + It 'Should have the expected service account set' { + $script:serviceAccount | Should -BeExactly $script:expectedServiceAccount -Because 'the service account should have been changed by Set-SqlDscRSServiceAccount' + } + + It 'Should create a SQL Server login for the new service account' { + $null = New-SqlDscLogin -ServerObject $script:serverObject -Name $script:serviceAccount -WindowsUser -Force -ErrorAction 'Stop' + } + + It 'Should generate database rights script for the new service account' { + $script:databaseRightsScript = $script:configuration | + Request-SqlDscRSDatabaseRightsScript -DatabaseName $script:databaseName -UserName $script:serviceAccount -ErrorAction 'Stop' + + $script:databaseRightsScript | Should -Not -BeNullOrEmpty -Because 'the database rights script should be generated' + } + + It 'Should execute the database rights script against the database' { + $invokeSqlDscQueryParameters = @{ + ServerName = 'localhost' + InstanceName = 'RSDB' + DatabaseName = 'master' + Query = $script:databaseRightsScript + Force = $true + ErrorAction = 'Stop' + } + + $null = Invoke-SqlDscQuery @invokeSqlDscQueryParameters + } + + It 'Should restart the Reporting Services service after granting rights' { + $null = $script:configuration | Restart-SqlDscRSService -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Post.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..7f31d1f81 --- /dev/null +++ b/tests/Integration/Commands/Post.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 @@ -0,0 +1,141 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +<# + .NOTES + This test file performs the final post-service-account-change operations + for Power BI Report Server in the SSL/TLS (Secure) stage. + + At this point in the workflow: + - The encryption key has been backed up (by Backup-SqlDscRSEncryptionKey) + - The service account has been changed (by Set-SqlDscRSServiceAccount) + - Database rights have been granted (by Mid.ServiceAccountChange.Secure.RS) + - The encryption key has been restored (by Restore-SqlDscRSEncryptionKey) + + This file completes the workflow by: + 1. Recreating URL reservations + 2. Re-initializing Reporting Services + 3. Restarting the service + 4. Validating site accessibility + + This runs as part of the Integration_Test_Commands_BIReportServer_Secure + pipeline stage in the following order: + 1. Pre.ServiceAccountChange.Secure.RS (creates backup directory) + 2. Backup-SqlDscRSEncryptionKey (backs up to persistent path) + 3. Set-SqlDscRSServiceAccount (changes service account) + 4. Get-SqlDscRSServiceAccount (verifies change) + 5. Mid.ServiceAccountChange.Secure.RS (grants database rights) + 6. Restore-SqlDscRSEncryptionKey (restores from persistent path) + 7. Post.ServiceAccountChange.Secure.RS (this file - URL reservations, re-init, validation) +#> +Describe 'Post.ServiceAccountChange.Secure.RS' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:instanceName = 'PBIRS' + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $script:computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $script:computerName + + Write-Verbose -Message "Instance: $script:instanceName, ExpectedServiceAccount: $script:expectedServiceAccount" -Verbose + } + + Context 'When recreating URL reservations after service account change' { + It 'Should recreate all URL reservations' { + # Refresh configuration after encryption key restore + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $null = $script:configuration | Set-SqlDscRSUrlReservation -RecreateExisting -Force -ErrorAction 'Stop' + } + } + + Context 'When re-initializing Reporting Services after service account change' { + It 'Should re-initialize the Reporting Services instance' { + # Refresh configuration + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $null = $script:configuration | Initialize-SqlDscRS -Force -ErrorAction 'Stop' + } + + It 'Should have an initialized instance after re-initialization' { + $configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $isInitialized = $configuration | Test-SqlDscRSInitialized -ErrorAction 'Stop' + + $isInitialized | Should -BeTrue -Because 'the instance should be initialized after re-initialization' + } + + It 'Should restart the Reporting Services service' { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $null = $script:configuration | Restart-SqlDscRSService -Force -ErrorAction 'Stop' + } + } + + Context 'When validating Reporting Services accessibility after service account change' { + It 'Should have the expected service account set' { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $currentServiceAccount = $script:configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $currentServiceAccount | Should -BeExactly $script:expectedServiceAccount -Because 'the service account should have been changed' + } + + It 'Should have an initialized instance' { + $isInitialized = $script:configuration | Test-SqlDscRSInitialized -ErrorAction 'Stop' + + $isInitialized | Should -BeTrue -Because 'the instance should remain initialized after service account change' + } + + It 'Should have all configured sites accessible after service account change' { + $results = $script:configuration | Test-SqlDscRSAccessible -Detailed -TimeoutSeconds 240 -RetryIntervalSeconds 10 -ErrorAction 'Stop' -Verbose + + Write-Verbose -Message "Accessibility results: $($results | ConvertTo-Json -Compress)" -Verbose + + $urlReservations = $script:configuration | Get-SqlDscRSUrlReservation -ErrorAction 'Stop' + $expectedApplications = $urlReservations.Application | Select-Object -Unique + + $results | Should -Not -BeNullOrEmpty -Because 'the command should return site accessibility results' + + foreach ($application in $expectedApplications) + { + $siteResult = $results | Where-Object -FilterScript { $_.Site -eq $application } + + $siteResult | Should -Not -BeNullOrEmpty -Because "the '$application' site should have a result" + $siteResult.Accessible | Should -BeTrue -Because "the '$application' site should be accessible after service account change" + $siteResult.StatusCode | Should -Be 200 -Because "the '$application' site should return HTTP 200 after service account change" + } + } + } +} diff --git a/tests/Integration/Commands/Pre.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Pre.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..11fca048d --- /dev/null +++ b/tests/Integration/Commands/Pre.ServiceAccountChange.Secure.RS.Integration.Tests.ps1 @@ -0,0 +1,71 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +<# + .NOTES + This test file prepares for the service account change workflow in the + SSL/TLS (Secure) stage. It creates the backup directory that will be + used by subsequent tests. + + The actual encryption key backup is performed by + Backup-SqlDscRSEncryptionKey.Integration.Tests.ps1 which runs next + in the pipeline. +#> +Describe 'Pre.ServiceAccountChange.Secure.RS' -Tag @('Integration_PowerBI') { + Context 'When preparing for service account change with encryption key backup' { + BeforeAll { + $script:instanceName = 'PBIRS' + + # Persistent path for backup file - shared across service account change workflow tests + $script:backupDirectory = 'C:\IntegrationTest' + + Write-Verbose -Message "Instance: $script:instanceName, BackupDirectory: $script:backupDirectory" -Verbose + } + + It 'Should create the backup directory if it does not exist' { + if (-not (Test-Path -Path $script:backupDirectory)) + { + $null = New-Item -Path $script:backupDirectory -ItemType Directory -Force -ErrorAction 'Stop' + } + + Test-Path -Path $script:backupDirectory | Should -BeTrue -Because 'the backup directory should exist' + } + + It 'Should verify the Reporting Services instance is configured' { + $configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $configuration | Should -Not -BeNullOrEmpty -Because 'the RS instance should be configured' + $configuration.IsInitialized | Should -BeTrue -Because 'the RS instance should be initialized' + } + } +} diff --git a/tests/Integration/Commands/README.md b/tests/Integration/Commands/README.md index 36922499e..93674997c 100644 --- a/tests/Integration/Commands/README.md +++ b/tests/Integration/Commands/README.md @@ -171,16 +171,39 @@ Request-SqlDscRSDatabaseRightsScript | 2 | 1 (Install-SqlDscReportingService), 0 Get-SqlDscRSConfiguration | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Enable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Disable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - -Get-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Set-SqlDscRSVirtualDirectory | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Add-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - -Remove-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Set-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - -Restart-SqlDscRSService | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscReportingService), 0 (Prerequisites, Prerequisites.RSDB) | SSRS, RSDB | ReportServer, ReportServerTempDB databases Request-SqlDscRSDatabaseUpgradeScript | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Restart-SqlDscRSService | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Test-SqlDscRSInitialized | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSLogPath | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSConfigFile | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Initialize-SqlDscRS | 4 | 3 (Set-SqlDscRSDatabaseConnection), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - -Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscReportingService), 0 (Prerequisites, Prerequisites.RSDB) | SSRS, RSDB | ReportServer, ReportServerTempDB databases +Post.Initialization.RS | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSUrl | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSIPAddress | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSDatabaseInstallation | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSExecutionLog | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Set-SqlDscRSServiceAccount | 6 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | Changes service account to svc-RS +Get-SqlDscRSServiceAccount | 6 | 6 (Set-SqlDscRSServiceAccount), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Post.ServiceAccountChange.SQL2017.RS | 6 | 6 (Get-SqlDscRSServiceAccount), 6 (Set-SqlDscRSServiceAccount), 0 (Prerequisites) | SSRS | Validates sites accessible after service account change (SQL2017) +Post.ServiceAccountChange.SQL2019-2022.RS | 6 | 6 (Get-SqlDscRSServiceAccount), 6 (Set-SqlDscRSServiceAccount), 0 (Prerequisites) | SSRS | Validates sites accessible after service account change (SQL2019-2022) +Set-SqlDscRSUnattendedExecutionAccount | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Set-SqlDscRSSmtpConfiguration | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Remove-SqlDscRSUnattendedExecutionAccount | 7 | 7 (Set-SqlDscRSUnattendedExecutionAccount), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Repair-SqlDscReportingService | 8 | 1 (Install-SqlDscReportingService) | SSRS | - +Remove-SqlDscRSUrlReservation | 8 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Remove-SqlDscRSEncryptionKey | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | Removes encrypted content +New-SqlDscRSEncryptionKey | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | Destroys encrypted content +Remove-SqlDscRSEncryptedInformation | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Stop-SqlDscRSWindowsService | 9 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Start-SqlDscRSWindowsService | 9 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Stop-SqlDscRSWebService | 9 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Start-SqlDscRSWebService | 9 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Set-SqlDscRSDatabaseTimeout | 9 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Uninstall-SqlDscReportingService | 9 | 8 (Repair-SqlDscReportingService) | - | - @@ -205,35 +228,80 @@ Request-SqlDscRSDatabaseRightsScript | 2 | 1 (Install-SqlDscPowerBIReportServer) Get-SqlDscRSConfiguration | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Enable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Disable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Get-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSVirtualDirectory | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Add-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Remove-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Set-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Get-SqlDscRSSslCertificateBinding | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Add-SqlDscRSSslCertificateBinding | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Remove-SqlDscRSSslCertificateBinding | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Set-SqlDscRSSslCertificateBinding | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Get-SqlDscRSServiceAccount | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Set-SqlDscRSServiceAccount | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Restart-SqlDscRSService | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Get-SqlDscRSSslCertificate | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Get-SqlDscRSIPAddress | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Get-SqlDscRSDatabaseInstallation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites, Prerequisites.RSDB) | PBIRS, RSDB | ReportServer, ReportServerTempDB databases Request-SqlDscRSDatabaseUpgradeScript | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Set-SqlDscRSEmailConfiguration | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Set-SqlDscRSUnattendedExecutionAccount | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Remove-SqlDscRSUnattendedExecutionAccount | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Restart-SqlDscRSService | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Test-SqlDscRSInitialized | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSLogPath | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSConfigFile | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Initialize-SqlDscRS | 4 | 3 (Set-SqlDscRSDatabaseConnection), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Backup-SqlDscRSEncryptionKey | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -Restore-SqlDscRSEncryptionKey | 5 | 5 (Backup-SqlDscRSEncryptionKey), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - -New-SqlDscRSEncryptionKey | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Destroys encrypted content -Remove-SqlDscRSEncryptionKey | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Removes encrypted content -Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites, Prerequisites.RSDB) | PBIRS, RSDB | ReportServer, ReportServerTempDB databases +Post.Initialization.RS | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSUrl | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSIPAddress | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSDatabaseInstallation | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSExecutionLog | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSServiceAccount | 6 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Changes service account to svc-RS +Get-SqlDscRSServiceAccount | 6 | 6 (Set-SqlDscRSServiceAccount), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Post.ServiceAccountChange.PowerBI.RS | 6 | 6 (Get-SqlDscRSServiceAccount), 6 (Set-SqlDscRSServiceAccount), 0 (Prerequisites) | PBIRS | Validates sites accessible after service account change +Set-SqlDscRSUnattendedExecutionAccount | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSSmtpConfiguration | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Remove-SqlDscRSUnattendedExecutionAccount | 7 | 7 (Set-SqlDscRSUnattendedExecutionAccount), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Repair-SqlDscPowerBIReportServer | 8 | 1 (Install-SqlDscPowerBIReportServer) | PBIRS | - +Remove-SqlDscRSUrlReservation | 8 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Remove-SqlDscRSEncryptionKey | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Removes encrypted content +New-SqlDscRSEncryptionKey | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Destroys encrypted content +Remove-SqlDscRSEncryptedInformation | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Stop-SqlDscRSWindowsService | 9 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Start-SqlDscRSWindowsService | 9 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Stop-SqlDscRSWebService | 9 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Start-SqlDscRSWebService | 9 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSDatabaseTimeout | 9 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Uninstall-SqlDscPowerBIReportServer | 9 | 8 (Repair-SqlDscPowerBIReportServer) | - | - +### Integration_Test_Commands_BIReportServer_Secure + +Tests for Power BI Report Server SSL/TLS commands. This test suite focuses on +secure connection configuration including SSL certificate bindings and service +account changes with encryption key backup/restore. + + +Command | Run order # | Depends on # | Use instance | Creates persistent objects +--- | --- | --- | --- | --- +Prerequisites | 0 | - | - | Sets up dependencies +Prerequisites.RSDB | 0 | - | - | Installs RSDB SQL Server instance for RS database tests +Save-SqlDscSqlServerMediaFile | 0 | - | - | Downloads SQL Server media files +Import-SqlDscPreferredModule | 0 | - | - | - +Install-SqlDscPowerBIReportServer | 1 | 0 (Prerequisites) | - | PBIRS instance +Request-SqlDscRSDatabaseScript | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Request-SqlDscRSDatabaseRightsScript | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Enable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSVirtualDirectory | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Pre.Set-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Configures URL reservations for secure connection +Add-SqlDscRSSslCertificateBinding | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites, Prerequisites.RSDB) | PBIRS, RSDB | ReportServer, ReportServerTempDB databases +Restart-SqlDscRSService | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Initialize-SqlDscRS | 4 | 3 (Set-SqlDscRSDatabaseConnection), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Post.Certificate.RS | 5 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Validates SSL/TLS configuration +Get-SqlDscRSSslCertificate | 6 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSSslCertificateBinding | 6 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Remove-SqlDscRSSslCertificateBinding | 6 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSSslCertificateBinding | 6 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSUnattendedExecutionAccount | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSSmtpConfiguration | 7 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Remove-SqlDscRSUnattendedExecutionAccount | 7 | 7 (Set-SqlDscRSUnattendedExecutionAccount), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Pre.ServiceAccountChange.Secure.RS | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Prepares for service account change +Backup-SqlDscRSEncryptionKey | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSServiceAccount | 8 | 4 (Initialize-SqlDscRS), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Changes service account to svc-RS +Mid.ServiceAccountChange.Secure.RS | 8 | 8 (Set-SqlDscRSServiceAccount), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Validates state after service account change +Restore-SqlDscRSEncryptionKey | 8 | 8 (Backup-SqlDscRSEncryptionKey, Set-SqlDscRSServiceAccount), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Post.ServiceAccountChange.Secure.RS | 8 | 8 (Restore-SqlDscRSEncryptionKey), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | Validates sites accessible after service account change + + ## Integration Tests ### `Prerequisites`ยด @@ -347,6 +415,7 @@ User | Password | Permission | Description .\svc-SqlAgentPri | yig-C^Equ3 | Local Windows user. | Runs the SQL Server Agent service. .\svc-SqlSecondary | yig-C^Equ3 | Local Windows user. | Runs the SQL Server service in multi node scenarios. .\svc-SqlAgentSec | yig-C^Equ3 | Local Windows user. | Runs the SQL Server Agent service in multi node scenarios. +.\svc-RS | yig-C^Equ3 | Local Windows user. | Runs the Reporting Services service for integration testing. ### Groups diff --git a/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 index 217cc587e..4b9fc5ded 100644 --- a/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 @@ -54,47 +54,48 @@ Describe 'Remove-SqlDscRSEncryptedInformation' { log for an event in the command New-SqlDscRSEncryptionKey to determine when the service is fully operational again and not return until it is. #> - Write-Verbose -Message 'Workaround. Waiting 2 minutes for SQL Server Reporting Services to become fully operational...' + Write-Verbose -Message 'Workaround. Waiting 2 minutes for SQL Server Reporting Services to become fully operational...' -Verbose Start-Sleep -Seconds 120 #300 } Context 'When removing encrypted information for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') -Skip:$true { BeforeAll { - $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' -Verbose } It 'Should remove the encrypted information' { - $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' -Verbose } } Context 'When removing encrypted information for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { - $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' -Verbose } It 'Should remove the encrypted information' { - $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' -Verbose } } Context 'When removing encrypted information for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { - $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' -Verbose } It 'Should remove the encrypted information' { - $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' -Verbose } } Context 'When removing encrypted information for Power BI Report Server' -Tag @('Integration_PowerBI') { BeforeAll { - $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + Write-Verbose -Message 'Getting configuration for Power BI Report Server...' -Verbose + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' -Verbose } It 'Should remove the encrypted information' { - $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' -Verbose } } } diff --git a/tests/Integration/Commands/Restore-SqlDscRSEncryptionKey.Integration.Tests.ps1 b/tests/Integration/Commands/Restore-SqlDscRSEncryptionKey.Integration.Tests.ps1 new file mode 100644 index 000000000..8ed454b38 --- /dev/null +++ b/tests/Integration/Commands/Restore-SqlDscRSEncryptionKey.Integration.Tests.ps1 @@ -0,0 +1,73 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +Describe 'Restore-SqlDscRSEncryptionKey' { + <# + .NOTES + This context is used in the SSL/TLS (Secure) stage service account + change workflow. It restores the encryption key from the persistent + path that was backed up by the Backup-SqlDscRSEncryptionKey test + earlier in the workflow. + + This runs as part of the Integration_Test_Commands_BIReportServer_Secure + pipeline stage in the following order: + 1. Pre.ServiceAccountChange.Secure.RS (creates backup directory) + 2. Backup-SqlDscRSEncryptionKey (backs up to persistent path) + 3. Set-SqlDscRSServiceAccount (changes service account) + 4. Get-SqlDscRSServiceAccount (verifies change) + 5. Mid.ServiceAccountChange.Secure.RS (grants database rights) + 6. Restore-SqlDscRSEncryptionKey (this context - restores from persistent path) + 7. Post.ServiceAccountChange.Secure.RS (URL reservations, re-init, validation) + #> + Context 'When restoring encryption key for Power BI Report Server service account change workflow' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + + # Persistent path for backup file - shared across service account change workflow tests + $script:backupPath = 'C:\IntegrationTest\RSEncryptionKey.snk' + $script:securePassword = ConvertTo-SecureString -String 'P@ssw0rd123!' -AsPlainText -Force + + Write-Verbose -Message "Restoring encryption key for service account change workflow from: $script:backupPath" -Verbose + } + + It 'Should verify the encryption key backup file exists from previous backup test' { + Test-Path -Path $script:backupPath | Should -BeTrue -Because 'the encryption key backup should have been created by the Backup-SqlDscRSEncryptionKey test' + } + + It 'Should restore the encryption key from the persistent backup location' { + $result = $script:configuration | Restore-SqlDscRSEncryptionKey -Password $script:securePassword -Path $script:backupPath -Force -PassThru -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty -Because 'the restore should return the configuration object when using PassThru' + } + } +} diff --git a/tests/Unit/Public/Backup-SqlDscRSEncryptionKey.Tests.ps1 b/tests/Unit/Public/Backup-SqlDscRSEncryptionKey.Tests.ps1 new file mode 100644 index 000000000..7ad1376c4 --- /dev/null +++ b/tests/Unit/Public/Backup-SqlDscRSEncryptionKey.Tests.ps1 @@ -0,0 +1,231 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Backup-SqlDscRSEncryptionKey' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'ByCredential' + ExpectedParameters = '-Configuration -Path -Password [-Credential ] [-DriveName ] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Backup-SqlDscRSEncryptionKey').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + + It 'Should have Configuration as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Backup-SqlDscRSEncryptionKey').Parameters['Configuration'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Path as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Backup-SqlDscRSEncryptionKey').Parameters['Path'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Password as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Backup-SqlDscRSEncryptionKey').Parameters['Password'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + } + + Context 'When backing up encryption key successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + KeyFile = [System.Text.Encoding]::UTF8.GetBytes('MockKeyFileContent') + } + } + } + + It 'Should backup encryption key without errors' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + + $null = $mockCimInstance | Backup-SqlDscRSEncryptionKey -Password $mockPassword -Path $testPath -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'BackupEncryptionKey' + } -Exactly -Times 1 + + $testPath | Should -Exist + } + + It 'Should not return anything by default' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey2.snk' + + $result = $mockCimInstance | Backup-SqlDscRSEncryptionKey -Password $mockPassword -Path $testPath -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When backing up encryption key with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + KeyFile = [System.Text.Encoding]::UTF8.GetBytes('MockKeyFileContent') + } + } + } + + It 'Should return the configuration CIM instance' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + + $result = $mockCimInstance | Backup-SqlDscRSEncryptionKey -Password $mockPassword -Path $testPath -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When backing up encryption key with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + KeyFile = [System.Text.Encoding]::UTF8.GetBytes('MockKeyFileContent') + } + } + } + + It 'Should backup encryption key without confirmation' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + + $null = $mockCimInstance | Backup-SqlDscRSEncryptionKey -Password $mockPassword -Path $testPath -Force + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method BackupEncryptionKey() failed with an error.' + } + } + + It 'Should throw a terminating error' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + + { $mockCimInstance | Backup-SqlDscRSEncryptionKey -Password $mockPassword -Path $testPath -Confirm:$false } | Should -Throw -ErrorId 'BSRSEK0001,Backup-SqlDscRSEncryptionKey' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + + $null = $mockCimInstance | Backup-SqlDscRSEncryptionKey -Password $mockPassword -Path $testPath -WhatIf + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 + + $testPath | Should -Not -Exist + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + KeyFile = [System.Text.Encoding]::UTF8.GetBytes('MockKeyFileContent') + } + } + } + + It 'Should backup encryption key' { + $testPath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + + $null = Backup-SqlDscRSEncryptionKey -Configuration $mockCimInstance -Password $mockPassword -Path $testPath -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +} diff --git a/tests/Unit/Public/Restore-SqlDscRSEncryptionKey.Tests.ps1 b/tests/Unit/Public/Restore-SqlDscRSEncryptionKey.Tests.ps1 new file mode 100644 index 000000000..26f195e6a --- /dev/null +++ b/tests/Unit/Public/Restore-SqlDscRSEncryptionKey.Tests.ps1 @@ -0,0 +1,221 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Restore-SqlDscRSEncryptionKey' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'ByCredential' + ExpectedParameters = '-Configuration -Path -Password [-Credential ] [-DriveName ] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Restore-SqlDscRSEncryptionKey').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + + It 'Should have Configuration as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Restore-SqlDscRSEncryptionKey').Parameters['Configuration'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Path as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Restore-SqlDscRSEncryptionKey').Parameters['Path'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Password as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Restore-SqlDscRSEncryptionKey').Parameters['Password'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + } + + Context 'When restoring encryption key successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + # Create a test key file using TestDrive + $script:testKeyFilePath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + [System.IO.File]::WriteAllBytes($script:testKeyFilePath, [byte[]] @(1, 2, 3, 4)) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should restore encryption key without errors' { + $null = $mockCimInstance | Restore-SqlDscRSEncryptionKey -Password $mockPassword -Path $script:testKeyFilePath -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RestoreEncryptionKey' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Restore-SqlDscRSEncryptionKey -Password $mockPassword -Path $script:testKeyFilePath -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When restoring encryption key with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + # Create a test key file using TestDrive + $script:testKeyFilePath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + [System.IO.File]::WriteAllBytes($script:testKeyFilePath, [byte[]] @(1, 2, 3, 4)) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Restore-SqlDscRSEncryptionKey -Password $mockPassword -Path $script:testKeyFilePath -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When restoring encryption key with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + # Create a test key file using TestDrive + $script:testKeyFilePath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + [System.IO.File]::WriteAllBytes($script:testKeyFilePath, [byte[]] @(1, 2, 3, 4)) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should restore encryption key without confirmation' { + $null = $mockCimInstance | Restore-SqlDscRSEncryptionKey -Password $mockPassword -Path $script:testKeyFilePath -Force + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + # Create a test key file using TestDrive + $script:testKeyFilePath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + [System.IO.File]::WriteAllBytes($script:testKeyFilePath, [byte[]] @(1, 2, 3, 4)) + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method RestoreEncryptionKey() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Restore-SqlDscRSEncryptionKey -Password $mockPassword -Path $script:testKeyFilePath -Confirm:$false } | Should -Throw -ErrorId 'RSRSEK0001,Restore-SqlDscRSEncryptionKey' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + # Create a test key file using TestDrive + $script:testKeyFilePath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + [System.IO.File]::WriteAllBytes($script:testKeyFilePath, [byte[]] @(1, 2, 3, 4)) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $null = $mockCimInstance | Restore-SqlDscRSEncryptionKey -Password $mockPassword -Path $script:testKeyFilePath -WhatIf + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force + + # Create a test key file using TestDrive + $script:testKeyFilePath = Join-Path -Path $TestDrive -ChildPath 'RSKey.snk' + [System.IO.File]::WriteAllBytes($script:testKeyFilePath, [byte[]] @(1, 2, 3, 4)) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should restore encryption key' { + $null = Restore-SqlDscRSEncryptionKey -Configuration $mockCimInstance -Password $mockPassword -Path $script:testKeyFilePath -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +}